/* Both And
   Palette sampled from the cover art: periwinkle line-drawing, driftwood
   brown, sunset blush. Shared by the album page and the per-song pages under
   songs/ — the two differ in markup, not in look. */

:root {
  --ink: #2c2320;
  --ink-soft: #6b5a55;
  --ink-faint: #9c8a85;

  --periwinkle: #6e5aa2;
  --periwinkle-lt: #a89ae3;
  --ember: #b33a22;

  --cream: #fbf6f4;
  /* Shows only until the wallpaper paints. Sampled between the blurred
     artwork's own top edge (#fcfafd) and bottom edge (#cfcdc6) and
     deliberately desaturated: a tinted value reads as a coloured bar against
     the art rather than as part of it. */
  --under: #fcfafd;
  /* Same colour at zero alpha — fading to a bare `transparent` would drift
     through grey on the way out. */
  --under-clear: rgba(252, 250, 253, 0);
  --card: rgba(255, 255, 255, 0.6);
  --card-line: rgba(255, 255, 255, 0.72);
  --rail: rgba(44, 35, 32, 0.12);

  --radius: 18px;
  --shadow: 0 22px 60px -20px rgba(44, 35, 32, 0.38);
}

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

/* Our display rules (grid, flex) would otherwise outrank the browser's
   [hidden] { display: none }. */
[hidden] {
  display: none !important;
}

/* The wallpaper lives on the root element because the root's background is
   painted across the entire canvas — behind the status bar, behind the home
   indicator, and into an overscroll bounce. A fixed element is confined to the
   safe area on iOS no matter how far its edges are pushed out, which left a
   band at each end.

   url() resolves against this stylesheet rather than the page, so the same
   declaration works from the album page and from songs/<slug>/ alike.

   The wash rides along as a gradient layer instead of a separate overlay, so
   it covers exactly the same ground and can't leave a seam. */
html {
  background-color: var(--under);
  background-image:
    /* iOS paints only the background *colour* into the safe areas — never the
       image — so the artwork is faded to that same colour at the top and bottom
       of the viewport. The strip and the artwork then meet at the same value
       and the seam disappears. This sits behind the content, so it costs
       nothing in legibility. */
    linear-gradient(
      to bottom,
      var(--under) 0,
      var(--under-clear) 7%,
      var(--under-clear) 93%,
      var(--under) 100%
    ),
    /* The wash, as a layer rather than a separate overlay element. */
    linear-gradient(rgba(246, 240, 250, 0.34), rgba(246, 240, 250, 0.34)),
    url("cover-blur.jpg");
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  /* Pinned: with `scroll` the wallpaper slides away and the foot of a long
     lyrics page goes flat. */
  background-attachment: fixed;
}

body {
  min-height: 100vh;
  /* Tracks the toolbar as it collapses, where supported. */
  min-height: 100dvh;
  font-family: "Helvetica Neue", Arial, sans-serif;
  background-color: transparent;
  color: var(--ink);
  -webkit-font-smoothing: antialiased;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

/* --- Layout -------------------------------------------------------------- */

.stage {
  max-width: 720px;
  margin: 0 auto;
  padding:
    calc(56px + env(safe-area-inset-top)) calc(20px + env(safe-area-inset-right))
    calc(72px + env(safe-area-inset-bottom)) calc(20px + env(safe-area-inset-left));
}

.masthead {
  text-align: center;
  margin-bottom: 32px;
}

/* No breadcrumbs above it any more, so the title itself has to clear the
   status bar and the Dynamic Island. */
.album-title {
  padding-top: 4px;
}

/* Breadcrumb trail. The last crumb is the page you're on, so it isn't a link. */
.crumbs {
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  margin-bottom: 14px;
  color: var(--ink-faint);
}

.crumbs ol {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0 8px;
}

/* Separator drawn in CSS so it can't be selected or read aloud as content. */
.crumbs li + li::before {
  content: "/";
  margin-right: 8px;
  color: var(--rail);
}

.crumbs a {
  color: var(--ink-faint);
  text-decoration: none;
  transition: color 0.18s ease;
}

.crumbs a:hover {
  color: var(--periwinkle);
}

.crumbs [aria-current="page"] {
  color: var(--periwinkle);
}

.album-title {
  font-size: 3.2rem;
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -0.03em;
}

/* The song pages lead with the song and name the record underneath, so their
   heading sits a size below the album's. */
.song-title {
  font-size: 2.4rem;
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: -0.02em;
}



/* --- Player card --------------------------------------------------------- */

.player {
  display: grid;
  grid-template-columns: minmax(0, 240px) 1fr;
  gap: 28px;
  align-items: center;

  padding: 24px;
  border: 1px solid var(--card-line);
  border-radius: var(--radius);
  background: var(--card);
  box-shadow: var(--shadow);
  backdrop-filter: blur(24px) saturate(140%);
  -webkit-backdrop-filter: blur(24px) saturate(140%);
}

.player.no-art {
  grid-template-columns: 1fr;
}

.art {
  position: relative;
  /* Explicit, because the frames inside are all absolutely positioned: with no
     in-flow content this box would otherwise collapse wherever it can't
     stretch — which is exactly what auto margins do to it on mobile. */
  width: 100%;
  aspect-ratio: 1;
  border-radius: 14px;
  overflow: hidden;
  box-shadow: 0 18px 40px -14px rgba(44, 35, 32, 0.55);
}

/* The stack holds one <img> per cover variant, laid on top of one another.
   Two independent motions compose here: the stack breathes as a whole while a
   track plays, and each frame inside drifts on its own timing as it fades in
   and out. Keeping them on separate elements means neither has to know about
   the other's transform. */
.art-stack {
  position: absolute;
  inset: 0;
}

.art-stack img {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

body.is-playing .art-stack {
  animation: breathe 14s ease-in-out infinite;
}

@keyframes breathe {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.035); }
}

/* Only the current frame is opaque; the rest wait underneath at zero. The fade
   is long and the hold is longer, so the cover reads as one image slowly
   becoming another rather than as a slideshow. */
.frame {
  opacity: 0;
  transition: opacity 4s ease-in-out;
  will-change: opacity, transform;
}

.frame.is-visible {
  opacity: 1;
}

/* Each frame drifts on a slightly different path. The resting scale sits above
   1 so a frame can translate or rotate without pulling its edge into view. */
/* Long, shallow, and prime-ish durations so the four paths don't fall into step
   with one another or with the cross-fade. */
.frame:nth-child(4n + 1) { animation: drift-a 71s ease-in-out infinite alternate; }
.frame:nth-child(4n + 2) { animation: drift-b 83s ease-in-out infinite alternate; }
.frame:nth-child(4n + 3) { animation: drift-c 67s ease-in-out infinite alternate; }
.frame:nth-child(4n + 4) { animation: drift-d 79s ease-in-out infinite alternate; }

@keyframes drift-a {
  from { transform: scale(1.03) translate(0, 0); }
  to   { transform: scale(1.055) translate(-0.7%, 0.5%); }
}

@keyframes drift-b {
  from { transform: scale(1.05) rotate(-0.28deg) translate(0.5%, 0); }
  to   { transform: scale(1.025) rotate(0.2deg) translate(-0.35%, -0.6%); }
}

@keyframes drift-c {
  from { transform: scale(1.025) translate(0.6%, 0.5%); }
  to   { transform: scale(1.06) translate(0, -0.45%); }
}

@keyframes drift-d {
  from { transform: scale(1.045) rotate(0.24deg) translate(-0.5%, -0.35%); }
  to   { transform: scale(1.02) rotate(-0.18deg) translate(0.45%, 0.5%); }
}

.console {
  display: flex;
  flex-direction: column;
  gap: 20px;
  min-width: 0;
}

/* --- Now playing --------------------------------------------------------- */

.eyebrow {
  font-size: 0.65rem;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: 6px;
}

.np-title {
  font-size: 1.6rem;
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: -0.01em;
}

.np-meta {
  margin-top: 4px;
  font-size: 0.9rem;
  color: var(--ink-soft);
  min-height: 1.2em;
}

/* --- Scrubber ------------------------------------------------------------ */

.scrub {
  display: flex;
  align-items: center;
  gap: 12px;
}

.time {
  font-size: 0.78rem;
  font-variant-numeric: tabular-nums;
  color: var(--ink-faint);
  min-width: 3ch;
}

.time:last-child {
  text-align: right;
}

/* Both range inputs share the same rail treatment. The fill is a gradient
   driven by --pct (played) and --buf (buffered), which player.js updates. */
input[type="range"] {
  -webkit-appearance: none;
  appearance: none;
  height: 20px;
  background: none;
  cursor: pointer;
  min-width: 0;
}

.seek {
  flex: 1;
  --pct: 0%;
  --buf: 0%;
  --rail-fill: linear-gradient(
    to right,
    var(--periwinkle) var(--pct),
    rgba(110, 90, 162, 0.28) var(--pct),
    rgba(110, 90, 162, 0.28) var(--buf),
    var(--rail) var(--buf)
  );
}

.vol {
  flex: 0 0 82px;
  --pct: 100%;
  --rail-fill: linear-gradient(
    to right,
    rgba(110, 90, 162, 0.55) var(--pct),
    var(--rail) var(--pct)
  );
}

input[type="range"]::-webkit-slider-runnable-track {
  height: 6px;
  border-radius: 999px;
  background: var(--rail-fill);
}

input[type="range"]::-moz-range-track {
  height: 6px;
  border-radius: 999px;
  background: var(--rail-fill);
}

input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 14px;
  height: 14px;
  margin-top: -4px; /* centre the thumb on the 6px track */
  border-radius: 50%;
  background: #fff;
  border: 2px solid var(--periwinkle);
  box-shadow: 0 2px 6px rgba(44, 35, 32, 0.3);
  transition: transform 0.15s ease;
}

input[type="range"]::-moz-range-thumb {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: #fff;
  border: 2px solid var(--periwinkle);
  box-shadow: 0 2px 6px rgba(44, 35, 32, 0.3);
}

input[type="range"]:hover::-webkit-slider-thumb,
input[type="range"]:active::-webkit-slider-thumb {
  transform: scale(1.18);
}

input[type="range"]:focus-visible {
  outline: 2px solid var(--periwinkle);
  outline-offset: 4px;
  border-radius: 999px;
}

/* --- Transport ----------------------------------------------------------- */

.controls {
  display: flex;
  align-items: center;
  gap: 8px;
}

.icon-btn {
  display: grid;
  place-items: center;
  width: 42px;
  height: 42px;
  flex-shrink: 0; /* a round button must never be squeezed into an oval */
  padding: 0;
  border: none;
  border-radius: 50%;
  background: transparent;
  color: var(--ink);
  cursor: pointer;
  transition: background-color 0.18s ease, transform 0.12s ease, color 0.18s ease;
}

.icon-btn svg {
  width: 22px;
  height: 22px;
}

.icon-btn:hover {
  background: rgba(110, 90, 162, 0.12);
  color: var(--periwinkle);
}

.icon-btn:active {
  transform: scale(0.94);
}

.icon-btn:focus-visible {
  outline: 2px solid var(--periwinkle);
  outline-offset: 2px;
}

.icon-btn.play {
  width: 58px;
  height: 58px;
  margin-right: 4px;
  color: #fff;
  background: linear-gradient(160deg, var(--periwinkle-lt), var(--periwinkle));
  box-shadow: 0 12px 26px -10px rgba(110, 90, 162, 0.9);
}

.icon-btn.play svg {
  width: 26px;
  height: 26px;
}

.icon-btn.play:hover {
  color: #fff;
  background: linear-gradient(160deg, var(--periwinkle-lt), var(--periwinkle));
  transform: translateY(-1px);
}

.i-pause,
body.is-playing .i-play {
  display: none;
}

body.is-playing .i-pause {
  display: block;
}

.i-muted,
body.is-muted .i-loud {
  display: none;
}

body.is-muted .i-muted {
  display: block;
}

.icon-btn.small {
  width: 34px;
  height: 34px;
  color: var(--ink-soft);
}

.icon-btn.small svg {
  width: 18px;
  height: 18px;
}

.volume {
  display: flex;
  align-items: center;
  gap: 4px;
  margin-left: auto;
  min-width: 0;
}

/* --- Playlist ------------------------------------------------------------ */

/* The card is the row, not the button: each row carries a play button *and* a
   link to the song's own page, and a <button> may not contain an <a>. So the
   hover/current treatment lives on the wrapper and both children sit flat
   inside it. */
.playlist {
  list-style: none;
  margin-top: 28px;
}

.track-row {
  display: flex;
  align-items: stretch;
  margin-bottom: 8px;

  border: 1px solid transparent;
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.52);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  transition: background-color 0.18s ease, border-color 0.18s ease, transform 0.12s ease;
}

.track-row:hover {
  background: rgba(255, 255, 255, 0.8);
  transform: translateX(2px);
}

.track-row.is-current {
  background: rgba(110, 90, 162, 0.1);
  border-color: rgba(110, 90, 162, 0.35);
}

.track {
  display: flex;
  align-items: center;
  gap: 14px;
  flex: 1;
  min-width: 0;
  padding: 13px 8px 13px 18px;

  font: inherit;
  text-align: left;
  color: var(--ink);
  cursor: pointer;

  border: none;
  border-radius: 12px;
  background: none;
}


.track:focus-visible,
.track-open:focus-visible {
  outline: 2px solid var(--periwinkle);
  outline-offset: -2px;
}

.track[aria-current="true"] .track-title {
  color: var(--periwinkle);
  font-weight: 700;
}

/* The chevron out to the song's own page. */
.track-open {
  display: grid;
  place-items: center;
  width: 44px;
  flex-shrink: 0;
  color: var(--ink-faint);
  border-radius: 12px;
  transition: color 0.18s ease, background-color 0.18s ease;
}

.track-open svg {
  width: 20px;
  height: 20px;
}

.track-open:hover {
  color: var(--periwinkle);
  background: rgba(110, 90, 162, 0.12);
}

.track-marker {
  display: grid;
  place-items: center;
  width: 1.6em;
  flex-shrink: 0;
}

/* The number and the equaliser stack in the same cell; only one shows. */
.track-num,
.eq {
  grid-area: 1 / 1;
}

.track-num {
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
  font-size: 0.9rem;
}

.eq {
  display: none;
  align-items: flex-end;
  gap: 2px;
  height: 14px;
}

/* Resting heights make a readable little bar glyph on their own, so a paused
   track — or a reader with reduced motion — never sees three flat dots. */
.eq i {
  width: 3px;
  border-radius: 1px;
  background: var(--periwinkle);
  transform-origin: bottom;
}

.eq i:nth-child(1) { height: 60%; }
.eq i:nth-child(2) { height: 100%; }
.eq i:nth-child(3) { height: 75%; }

.track[aria-current="true"] .track-num { display: none; }
.track[aria-current="true"] .eq { display: flex; }

body.is-playing .eq i {
  animation: bounce 0.9s ease-in-out infinite;
}

body.is-playing .eq i:nth-child(2) { animation-delay: 0.18s; }
body.is-playing .eq i:nth-child(3) { animation-delay: 0.36s; }

@keyframes bounce {
  0%, 100% { transform: scaleY(0.5); }
  50% { transform: scaleY(1); }
}

.track-title {
  flex-grow: 1;
  font-weight: 600;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.track-duration {
  color: var(--ink-faint);
  font-size: 0.82rem;
  font-variant-numeric: tabular-nums;
  flex-shrink: 0;
}

.empty-note {
  text-align: center;
  color: var(--ink-soft);
  padding: 40px 20px;
}

/* --- Song page: take-it-with-you panel ----------------------------------- */

.takeaway {
  margin-top: 24px;
  padding: 20px 22px;
  border: 1px solid var(--card-line);
  border-radius: var(--radius);
  background: var(--card);
  box-shadow: var(--shadow);
  backdrop-filter: blur(24px) saturate(140%);
  -webkit-backdrop-filter: blur(24px) saturate(140%);

  display: flex;
  flex-direction: column;
  gap: 16px;
}


.download-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  align-self: flex-start;

  padding: 12px 22px;
  border-radius: 999px;
  font-size: 0.95rem;
  font-weight: 700;
  text-decoration: none;
  color: #fff;
  background: linear-gradient(160deg, var(--periwinkle-lt), var(--periwinkle));
  box-shadow: 0 12px 26px -12px rgba(110, 90, 162, 0.9);
  transition: transform 0.12s ease, box-shadow 0.18s ease;
}

.download-btn svg {
  width: 18px;
  height: 18px;
}

.download-btn:hover {
  transform: translateY(-1px);
  box-shadow: 0 16px 30px -12px rgba(110, 90, 162, 0.95);
}

.download-btn:active {
  transform: scale(0.98);
}

.download-btn:focus-visible {
  outline: 2px solid var(--periwinkle);
  outline-offset: 3px;
}


/* The same URL the button points at, spelled out so it can be copied into a
   message rather than clicked. */
.raw-link {
  display: flex;
  align-items: center;
  gap: 8px;
}

.raw-link input {
  flex: 1;
  min-width: 0;
  padding: 9px 12px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.76rem;
  color: var(--ink-soft);
  background: rgba(255, 255, 255, 0.6);
  border: 1px solid var(--rail);
  border-radius: 9px;
}

.raw-link input:focus-visible {
  outline: 2px solid var(--periwinkle);
  outline-offset: 1px;
}

.copy-btn {
  flex-shrink: 0;
  padding: 9px 14px;
  font: inherit;
  font-size: 0.76rem;
  font-weight: 700;
  color: var(--periwinkle);
  cursor: pointer;
  background: rgba(110, 90, 162, 0.1);
  border: 1px solid rgba(110, 90, 162, 0.28);
  border-radius: 9px;
  transition: background-color 0.18s ease;
}

.copy-btn:hover {
  background: rgba(110, 90, 162, 0.2);
}

.copy-btn:focus-visible {
  outline: 2px solid var(--periwinkle);
  outline-offset: 2px;
}

/* --- Song page: lyrics ---------------------------------------------------- */

/* Lifted straight out of the file's own lyrics tag by generate_tracks.sh, so
   the page can't drift from what's embedded in the audio. */
.lyrics {
  margin-top: 24px;
  padding: 28px 30px;
  border: 1px solid var(--card-line);
  border-radius: var(--radius);
  background: var(--card);
  box-shadow: var(--shadow);
  backdrop-filter: blur(24px) saturate(140%);
  -webkit-backdrop-filter: blur(24px) saturate(140%);
}


/* One <p> per stanza, <br> between lines: the blank lines in the tag are the
   song's own structure, so they get real spacing rather than an empty line. */
.lyrics p {
  font-size: 1.02rem;
  line-height: 1.75;
  color: var(--ink);
}

.lyrics p + p {
  margin-top: 1.4em;
}



/* --- Colophon ------------------------------------------------------------ */

.colophon {
  margin-top: 40px;
  text-align: center;
  font-size: 0.72rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  display: flex;
  justify-content: center;
  gap: 20px;
  flex-wrap: wrap;
}

.colophon a {
  color: var(--ink-faint);
  text-decoration: none;
  transition: color 0.18s ease;
}

.colophon a:hover {
  color: var(--periwinkle);
}

/* --- Mobile -------------------------------------------------------------- */

@media screen and (max-width: 680px) {
  .stage {
    padding:
      calc(46px + env(safe-area-inset-top)) calc(16px + env(safe-area-inset-right))
      calc(56px + env(safe-area-inset-bottom)) calc(16px + env(safe-area-inset-left));
  }

  .album-title {
    font-size: 2.4rem;
  }

  .song-title {
    font-size: 1.9rem;
  }

  .player {
    grid-template-columns: 1fr;
    gap: 22px;
    padding: 18px;
  }

  .art {
    width: 100%;
    max-width: 260px;
    margin: 0 auto;
  }

  .console {
    text-align: center;
  }

  .np-title {
    font-size: 1.4rem;
  }

  .controls {
    justify-content: center;
  }

  /* Volume is a no-op on iOS and eats width; hide it. */
  .volume {
    display: none;
  }

  .track {
    padding: 12px 6px 12px 14px;
  }

  .download-btn {
    align-self: stretch;
  }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation: none !important;
    transition: none !important;
  }
}
