/* ---------------------------------------------------------------- reset -- */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }
html { -webkit-text-size-adjust: 100%; }
body { min-height: 100svh; }
img, svg { display: block; max-width: 100%; }
button, input { font: inherit; color: inherit; }
a { color: inherit; }

/* --------------------------------------------------------------- tokens -- */
:root {
  --hue: 205;
  --bg:        hsl(232 16% 6.5%);
  --bg-raised: hsl(232 14% 10%);
  --fg:        hsl(40 14% 93%);
  --fg-dim:    hsl(40 7% 62%);
  --fg-faint:  hsl(40 5% 44%);
  --line:      hsl(232 12% 17%);
  --line-soft: hsl(232 12% 13%);
  --accent:    hsl(var(--hue) 68% 68%);
  --accent-dim:hsl(var(--hue) 40% 40%);
  --glow-1:    hsl(var(--hue) 70% 42%);
  --glow-2:    hsl(calc(var(--hue) - 40) 52% 34%);
  --shadow:    0 24px 60px -30px hsl(232 40% 2% / .9);

  --radius: 14px;
  --step: clamp(1.1rem, 3.4vw, 1.6rem);
  --font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Inter, Roboto,
          "Helvetica Neue", Arial, sans-serif;

  --ease: cubic-bezier(.22, .61, .36, 1);
  color-scheme: dark;
}

:root[data-theme="light"] {
  --bg:        hsl(42 32% 96.5%);
  --bg-raised: hsl(42 28% 99%);
  --fg:        hsl(232 22% 13%);
  --fg-dim:    hsl(232 10% 40%);
  --fg-faint:  hsl(232 8% 56%);
  --line:      hsl(40 16% 84%);
  --line-soft: hsl(40 18% 89%);
  --accent:    hsl(var(--hue) 62% 40%);
  --accent-dim:hsl(var(--hue) 40% 62%);
  --glow-1:    hsl(var(--hue) 78% 72%);
  --glow-2:    hsl(calc(var(--hue) - 40) 62% 76%);
  --shadow:    0 22px 50px -32px hsl(232 30% 30% / .35);
  color-scheme: light;
}

body {
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font);
  font-size: 16px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
  transition: background-color .6s var(--ease), color .4s var(--ease);
}

/* ------------------------------------------------------- ambient backdrop -- */
/* Pure CSS so nothing burns CPU while the tab sits in the background. */
.glow { position: fixed; inset: 0; z-index: 0; overflow: hidden; pointer-events: none; }
.glow span {
  position: absolute;
  width: 60vmax; height: 60vmax;
  border-radius: 50%;
  filter: blur(100px);
  opacity: .16;
  transition: background-color 1.6s var(--ease);
  will-change: transform;
}
:root[data-theme="light"] .glow span { opacity: .28; filter: blur(90px); }
.glow-a { background: var(--glow-1); top: -26vmax; left: -18vmax; animation: driftA 46s ease-in-out infinite alternate; }
.glow-b { background: var(--glow-2); bottom: -30vmax; right: -20vmax; animation: driftB 58s ease-in-out infinite alternate; }

@keyframes driftA { from { transform: translate3d(0,0,0) scale(1); } to { transform: translate3d(9vw, 7vh, 0) scale(1.14); } }
@keyframes driftB { from { transform: translate3d(0,0,0) scale(1.08); } to { transform: translate3d(-8vw,-6vh,0) scale(.94); } }

/* ---------------------------------------------------------------- shell -- */
.shell {
  position: relative; z-index: 1;
  display: flex; flex-direction: column;
  min-height: 100svh;
  max-width: 64rem;
  margin-inline: auto;
  padding: clamp(1rem, 3vw, 1.75rem) clamp(1.15rem, 5vw, 2rem)
           max(1.25rem, env(safe-area-inset-bottom));
  gap: var(--step);
}

/* ----------------------------------------------------------------- head -- */
.site-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }

.wordmark {
  display: inline-flex; align-items: center; gap: .6rem;
  font-size: .82rem; font-weight: 600; letter-spacing: .17em; text-transform: uppercase;
  text-decoration: none; color: var(--fg);
}
.wordmark-mark { display: flex; align-items: center; gap: 2.5px; height: 15px; }
.wordmark-mark i {
  display: block; width: 2px; border-radius: 2px; background: var(--accent);
  height: 5px; transition: height .5s var(--ease), background-color 1s var(--ease);
}
.wordmark-mark i:nth-child(2) { height: 10px; }
.wordmark-mark i:nth-child(3) { height: 15px; }
.wordmark-mark i:nth-child(4) { height: 10px; }
/* the mark quietly breathes while audio is playing */
body.is-playing .wordmark-mark i { animation: bar 1.9s var(--ease) infinite; }
body.is-playing .wordmark-mark i:nth-child(1) { animation-delay: -.9s; }
body.is-playing .wordmark-mark i:nth-child(2) { animation-delay: -.45s; }
body.is-playing .wordmark-mark i:nth-child(4) { animation-delay: -.65s; }
body.is-playing .wordmark-mark i:nth-child(5) { animation-delay: -1.2s; }
@keyframes bar { 0%,100% { height: 4px; } 50% { height: 14px; } }

.site-nav { display: flex; align-items: center; gap: .35rem; }

.link {
  color: var(--fg-dim); text-decoration: none;
  font-size: .84rem;
  border-bottom: 1px solid transparent;
  transition: color .2s var(--ease), border-color .2s var(--ease);
}
.link:hover { color: var(--fg); border-bottom-color: var(--accent-dim); }

.icon-btn {
  display: grid; place-items: center;
  width: 34px; height: 34px; padding: 8px;
  background: none; border: 1px solid transparent; border-radius: 10px;
  color: var(--fg-dim); cursor: pointer;
  transition: color .2s var(--ease), background-color .2s var(--ease), border-color .2s var(--ease);
}
.icon-btn:hover { color: var(--fg); background: color-mix(in oklab, var(--fg) 7%, transparent); }

/* ---------------------------------------------------------------- stage -- */
.stage {
  flex: 1;
  display: flex; flex-direction: column; justify-content: center;
  padding-block: clamp(1rem, 4vh, 2.5rem);
  /* Tall enough for the player that replaces the loading skeleton, or the swap
     drags the whole page up behind it. 19rem was short of the real 350px and
     cost a visible shift on a cold load, where the skeleton actually gets
     painted before the catalogue answers. Box-sizing is border-box, so this
     has to cover the padding above as well — at its 2.5rem ceiling that is
     another 5rem on top of the player itself. */
  min-height: 27rem;
}
.state { display: flex; flex-direction: column; gap: .55rem; }

.eyebrow {
  font-size: .72rem; font-weight: 600; letter-spacing: .18em; text-transform: uppercase;
  color: var(--accent);
  transition: color 1s var(--ease);
}
.track-title {
  font-size: clamp(1.7rem, 6.4vw, 2.5rem);
  font-weight: 500; letter-spacing: -.022em; line-height: 1.14;
  text-wrap: balance;
}
.track-meta { color: var(--fg-dim); font-size: .9rem; }
.track-meta a {
  text-decoration: none; color: var(--fg-dim);
  border-bottom: 1px solid transparent;
  transition: color .2s var(--ease), border-color .2s var(--ease);
}
.track-meta a:hover { border-bottom-color: var(--accent-dim); color: var(--fg); }

/* scrubber */
.scrubber { margin-top: 1.4rem; display: flex; flex-direction: column; gap: .5rem; }
.bar {
  position: relative; height: 22px; display: flex; align-items: center;
  cursor: pointer; touch-action: none;
}
.bar::before {
  content: ""; position: absolute; inset-inline: 0; height: 3px; border-radius: 3px;
  background: var(--line); transition: background-color .3s var(--ease);
}
.bar-fill {
  position: relative; height: 3px; border-radius: 3px; background: var(--accent);
  width: 0%; transition: background-color 1s var(--ease);
}
.bar-fill::after {
  content: ""; position: absolute; right: -5px; top: 50%;
  width: 10px; height: 10px; border-radius: 50%; background: var(--accent);
  transform: translateY(-50%) scale(0);
  transition: transform .18s var(--ease);
}
.bar:hover .bar-fill::after, .bar:focus-visible .bar-fill::after { transform: translateY(-50%) scale(1); }
.bar:focus-visible { outline: 2px solid var(--accent); outline-offset: 6px; border-radius: 4px; }

.times {
  display: flex; justify-content: space-between;
  font-size: .74rem; color: var(--fg-faint);
  font-variant-numeric: tabular-nums; letter-spacing: .03em;
}

/* transport */
.transport { margin-top: 1.5rem; display: flex; align-items: center; gap: clamp(.5rem, 3vw, 1rem); }

.tbtn {
  display: grid; place-items: center;
  background: none; border: none; border-radius: 50%;
  color: var(--fg-dim); cursor: pointer;
  width: 46px; height: 46px; padding: 12px;
  transition: color .2s var(--ease), background-color .2s var(--ease), transform .12s var(--ease);
}
.tbtn:hover { color: var(--fg); background: color-mix(in oklab, var(--fg) 7%, transparent); }
.tbtn:active { transform: scale(.93); }
.tbtn:disabled { opacity: .3; cursor: default; }
.tbtn:disabled:hover { background: none; color: var(--fg-dim); }

.tbtn-play {
  width: 62px; height: 62px; padding: 19px;
  color: var(--bg);
  background: var(--accent);
  box-shadow: 0 10px 34px -12px var(--accent);
  transition: background-color 1s var(--ease), box-shadow 1s var(--ease), transform .12s var(--ease);
}
.tbtn-play:hover { background: var(--accent); color: var(--bg); filter: brightness(1.08); }
:root[data-theme="light"] .tbtn-play { color: hsl(42 32% 98%); }

.transport-spacer { flex: 1; }

/* volume */
.volume { display: flex; align-items: center; gap: .4rem; }
.volume input[type="range"] {
  -webkit-appearance: none; appearance: none;
  width: clamp(66px, 22vw, 108px); height: 22px; background: none; cursor: pointer;
}
.volume input[type="range"]::-webkit-slider-runnable-track {
  height: 3px; border-radius: 3px;
  background: linear-gradient(to right, var(--accent) var(--pct, 70%), var(--line) var(--pct, 70%));
}
.volume input[type="range"]::-moz-range-track {
  height: 3px; border-radius: 3px;
  background: linear-gradient(to right, var(--accent) var(--pct, 70%), var(--line) var(--pct, 70%));
}
.volume input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 11px; height: 11px; margin-top: -4px; border-radius: 50%;
  background: var(--accent); border: none;
  transform: scale(0); transition: transform .18s var(--ease);
}
.volume input[type="range"]::-moz-range-thumb {
  width: 11px; height: 11px; border-radius: 50%; border: none; background: var(--accent);
  transform: scale(0); transition: transform .18s var(--ease);
}
.volume:hover input::-webkit-slider-thumb,
.volume input:focus-visible::-webkit-slider-thumb { transform: scale(1); }
.volume:hover input::-moz-range-thumb,
.volume input:focus-visible::-moz-range-thumb { transform: scale(1); }

.up-next {
  margin-top: 1.4rem; font-size: .78rem; color: var(--fg-faint);
  display: flex; align-items: center; gap: .5rem; min-height: 1.2em;
}
.up-next b { font-weight: 500; color: var(--fg-dim); }
.up-next .dot { width: 3px; height: 3px; border-radius: 50%; background: var(--fg-faint); flex: none; }

.status {
  display: inline-flex; align-items: center; gap: .4rem;
  font-size: .74rem; color: var(--fg-faint);
}
.spinner {
  width: 11px; height: 11px; flex: none;
  border: 1.6px solid var(--line); border-top-color: var(--accent);
  border-radius: 50%; animation: spin .8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ------------------------------------------------------------- channels -- */
.channels {
  /* Lays out its own heading and grid (or a playlist detail view), so it must
     not impose a column grid on those children. Unlike the player it uses the
     full shell width: at the old 40rem only two covers fit per row. */
  display: block;
}

/* The player, and the notices that replace it, keep the narrower reading
   measure the shell used to impose on everything. */
.stage > .state,
.site-foot { width: 100%; max-width: 40rem; margin-inline: auto; }
.site-head { max-width: 64rem; }
.channel {
  --hue: 205;
  position: relative; overflow: hidden;
  display: flex; flex-direction: column; gap: .18rem;
  text-align: left; cursor: pointer;
  padding: .85rem .95rem;
  background: color-mix(in oklab, var(--bg-raised) 82%, transparent);
  border: 1px solid var(--line-soft);
  border-radius: var(--radius);
  transition: border-color .25s var(--ease), background-color .25s var(--ease), transform .12s var(--ease);
}
.channel::before {
  content: ""; position: absolute; inset: 0;
  background: radial-gradient(120% 100% at 0% 0%, hsl(var(--hue) 70% 55% / .16), transparent 62%);
  opacity: 0; transition: opacity .3s var(--ease);
}
.channel:hover { border-color: color-mix(in oklab, hsl(var(--hue) 60% 55%) 40%, var(--line)); }
.channel:hover::before { opacity: .7; }
.channel:active { transform: scale(.99); }
.channel[aria-current="true"] {
  border-color: hsl(var(--hue) 60% 55% / .6);
  background: color-mix(in oklab, hsl(var(--hue) 60% 50%) 9%, var(--bg-raised));
}
.channel[aria-current="true"]::before { opacity: 1; }

.channel-name {
  position: relative; display: flex; align-items: center; gap: .45rem;
  font-size: .95rem; font-weight: 500; letter-spacing: -.01em;
}
.channel-name::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%; flex: none;
  background: hsl(var(--hue) 65% 62%);
  box-shadow: 0 0 0 0 hsl(var(--hue) 65% 62% / .5);
}
.channel[aria-current="true"] .channel-name::before { animation: pulse 3.4s var(--ease) infinite; }
@keyframes pulse {
  0%, 100% { box-shadow: 0 0 0 0 hsl(var(--hue) 65% 62% / .45); }
  50%      { box-shadow: 0 0 0 5px hsl(var(--hue) 65% 62% / 0); }
}
.channel-tagline { position: relative; font-size: .78rem; color: var(--fg-faint); line-height: 1.42; text-wrap: pretty; }
.channel-count { position: relative; font-size: .68rem; color: var(--fg-faint); letter-spacing: .04em; margin-top: .2rem; }

/* ---------------------------------------------------- loading / errors -- */
.skeleton {
  border-radius: 6px;
  background: linear-gradient(90deg, var(--line-soft) 25%, var(--line) 37%, var(--line-soft) 63%);
  background-size: 320% 100%;
  animation: shimmer 1.9s ease-in-out infinite;
}
@keyframes shimmer { from { background-position: 100% 0; } to { background-position: -60% 0; } }
.skeleton-eyebrow { width: 6rem;  height: .62rem; }
.skeleton-title   { width: 88%;   height: 2.1rem; margin-top: .5rem; }
.skeleton-line    { width: 11rem; height: .8rem;  margin-top: .35rem; }

.notice {
  display: flex; gap: .85rem; align-items: flex-start;
  padding: 1.1rem 1.15rem;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: color-mix(in oklab, var(--bg-raised) 80%, transparent);
}
.notice-icon { width: 20px; height: 20px; flex: none; color: var(--accent); margin-top: 2px; }
.notice h2 { font-size: 1rem; font-weight: 500; letter-spacing: -.01em; }
.notice p { font-size: .86rem; color: var(--fg-dim); margin-top: .2rem; text-wrap: pretty; }
.notice-actions { margin-top: .85rem; display: flex; gap: .5rem; flex-wrap: wrap; }

.btn {
  padding: .48rem .9rem; border-radius: 9px; cursor: pointer;
  font-size: .84rem; font-weight: 500;
  background: var(--accent); color: var(--bg); border: 1px solid transparent;
  transition: filter .2s var(--ease), transform .12s var(--ease);
}
.btn:hover { filter: brightness(1.08); }
.btn:active { transform: scale(.97); }
.btn-ghost { background: none; color: var(--fg-dim); border-color: var(--line); }
.btn-ghost:hover { color: var(--fg); border-color: var(--fg-faint); filter: none; }

/* ----------------------------------------------------------------- foot -- */
.site-foot {
  border-top: 1px solid var(--line-soft);
  padding-top: 1rem;
  display: flex; flex-direction: column; gap: .5rem;
  font-size: .76rem; color: var(--fg-faint); line-height: 1.6;
  text-wrap: pretty;
}
.site-foot .link { font-size: inherit; }
.shortcuts { display: flex; flex-wrap: wrap; gap: .3rem; align-items: center; }
kbd {
  font: inherit; font-size: .69rem;
  padding: .08rem .34rem; border-radius: 4px;
  border: 1px solid var(--line); background: var(--bg-raised); color: var(--fg-dim);
}
@media (max-width: 30rem) { .shortcuts { display: none; } }

/* --------------------------------------------------------------- credits -- */
.prose { display: flex; flex-direction: column; gap: 1.5rem; padding-block: 1rem 2rem; }
.prose h1 { font-size: clamp(1.5rem, 5vw, 2rem); font-weight: 500; letter-spacing: -.02em; }
.prose h2 { font-size: 1.02rem; font-weight: 600; letter-spacing: -.005em; margin-bottom: .5rem; }
.prose p  { color: var(--fg-dim); font-size: .9rem; text-wrap: pretty; }
.prose p + p { margin-top: .7rem; }
.prose a { color: var(--fg); text-decoration: none; border-bottom: 1px solid var(--line); }
.prose a:hover { border-bottom-color: var(--accent); }

.credit-group { border-top: 1px solid var(--line-soft); padding-top: 1.1rem; }
.credit-list { list-style: none; padding: 0; display: flex; flex-direction: column; gap: .3rem; }
.credit-list li {
  display: flex; justify-content: space-between; gap: 1rem; align-items: baseline;
  font-size: .86rem; padding: .18rem 0;
}
.credit-list .by { color: var(--fg-faint); font-size: .78rem; flex: none; }

/* ------------------------------------------------------------------ a11y -- */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
}
:where(a, button, input, [tabindex]):focus-visible {
  outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 6px;
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important; animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
}

.timer-btn {
  width: auto;
  padding: 0 .6rem;
  font: inherit;
  font-size: .78rem;
  letter-spacing: .02em;
  font-variant-numeric: tabular-nums;
  border-radius: 999px;
  white-space: nowrap;
}
.timer-btn.is-on {
  color: var(--accent);
  background: color-mix(in oklab, var(--accent) 14%, transparent);
}

/* Transport controls must never shrink: squeezing them to fit an extra button
   is what took the play target from 62px to 47px on a phone. */
.transport .tbtn,
.transport .icon-btn { flex: 0 0 auto; }

@media (max-width: 34rem) {
  .transport { flex-wrap: wrap; row-gap: .9rem; }
  /* Full-width spacer forces volume and timer onto their own row. */
  .transport-spacer { flex: 0 0 100%; height: 0; }
  .volume { flex: 1 1 auto; }
}

/* An unending source cannot be scrubbed: there is no position to seek to. */
.scrubber .bar.is-endless { opacity: .35; pointer-events: none; }
.tbtn:disabled { opacity: .3; pointer-events: none; }

/* ------------------------------------------------------- transport rows */

.transport.secondary { margin-top: .85rem; }
.transport.secondary .icon-btn.is-on {
  color: var(--accent);
  background: color-mix(in oklab, var(--accent) 14%, transparent);
}

/* Buffering belongs on the control being watched, not in a line of text that
   shifts the layout. A ring around the play button, nothing else moves. */
.tbtn-play { position: relative; }
.tbtn-play.is-buffering::after,
body.is-busy .tbtn-play::after {
  content: '';
  position: absolute;
  inset: -5px;
  border-radius: 50%;
  border: 2px solid transparent;
  border-top-color: currentColor;
  opacity: .85;
  animation: lw-spin .9s linear infinite;
}

/* While a channel or track is being prepared the whole grid is inert, not just
   the card that was clicked: every other card is another way into the same
   race. Dimmed rather than hidden, so the listener can see what they picked. */
body.is-busy .playlist-card,
body.is-busy .track-row {
  opacity: .45;
  pointer-events: none;
  transition: opacity .18s ease;
}
body.is-busy .playlist-card[aria-current="true"] { opacity: .8; }
body.is-busy .playlist-card[aria-current="true"] .cover { outline-color: var(--accent); }

/* Playing but inaudible is a real state on iOS until the first gesture lands.
   The transport says "press play" rather than showing a pause button over
   silence. */
body.is-blocked .tbtn-play { box-shadow: 0 0 0 3px color-mix(in oklab, var(--accent) 40%, transparent); }
@keyframes lw-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .tbtn-play.is-buffering::after { animation-duration: 2.4s; }
}

/* ------------------------------------------------------------ timer menu */

.timer-wrap { position: relative; }
.timer-btn { display: inline-flex; align-items: center; gap: .4rem; }
.timer-ico { width: 16px; height: 16px; display: block; }

.timer-menu {
  position: absolute;
  right: 0;
  bottom: calc(100% + .5rem);
  z-index: 20;
  width: min(19rem, calc(100vw - 2.5rem));
  padding: .85rem;
  border-radius: 14px;
  border: 1px solid var(--line);
  background: var(--bg-raised);
  box-shadow: 0 18px 44px rgb(0 0 0 / .38);
}
.timer-menu-title {
  margin: 0 0 .55rem;
  font-size: .72rem;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--fg-dim);
}
.timer-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: .35rem;
  margin-bottom: .5rem;
}
.timer-opt {
  font: inherit;
  font-size: .82rem;
  padding: .45rem .3rem;
  border-radius: 9px;
  border: 1px solid var(--line);
  background: transparent;
  color: var(--fg);
  cursor: pointer;
  white-space: nowrap;
}
.timer-opt:hover { background: color-mix(in oklab, var(--fg) 8%, transparent); }
.timer-opt.is-active {
  border-color: var(--accent);
  color: var(--accent);
  background: color-mix(in oklab, var(--accent) 13%, transparent);
}
.timer-menu > .timer-opt { width: 100%; margin-top: .35rem; }
.timer-row { display: flex; gap: .35rem; }
.timer-custom {
  flex: 1;
  font: inherit;
  font-size: .82rem;
  padding: .45rem .55rem;
  border-radius: 9px;
  border: 1px solid var(--line);
  background: transparent;
  color: var(--fg);
  min-width: 0;
}

/* The menu is wider than the space to the left of the timer button, so
   anchoring it there hangs it off the left edge of a phone screen; capping the
   width cannot fix that, it is the anchor that overflows. Below 40rem the
   player is one centred column, so the menu anchors to the utility bar instead
   — that bar is already centred and never wider than the viewport. */
.state.player > .utils { position: relative; }

@media (max-width: 40rem) {
  .timer-wrap { position: static; }
  .timer-menu { left: 0; right: 0; width: auto; }
}

/* ------------------------------------------------------------- playlists */

.section-title {
  margin: 0 0 .9rem;
  font-size: .74rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--fg-dim);
  /* The placeholder copy of this in index.html carries no text, and an empty
     block would measure zero and under-reserve the grid by its own height. */
  line-height: 1.55; min-height: 1.55em;
}
.playlist-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
  gap: 1rem;
}
.playlist-card {
  display: flex;
  flex-direction: column;
  gap: .7rem;
  padding: 0;
  text-align: left;
  background: none;
  border: none;
  cursor: pointer;
  color: inherit;
}
.cover {
  display: block;
  aspect-ratio: 1;
  border-radius: 12px;
  overflow: hidden;
  background: color-mix(in oklab, hsl(var(--hue) 40% 40%) 22%, var(--bg));
  box-shadow: 0 8px 22px rgb(0 0 0 / .3);
  transition: transform .22s var(--ease), box-shadow .22s var(--ease);
}
.cover svg { display: block; width: 100%; height: 100%; }
.playlist-card:hover .cover { transform: translateY(-3px); box-shadow: 0 14px 30px rgb(0 0 0 / .38); }
.playlist-card[aria-current="true"] .cover { outline: 2px solid var(--accent); outline-offset: 3px; }
.playlist-meta { display: flex; flex-direction: column; gap: .18rem; min-width: 0; }
/* Every line here is pinned to an exact height, because the placeholder grid in
   index.html reserves this card's space before the real one renders and any
   difference between the two is a layout shift. Left free, a tagline ran to
   three lines at 228px columns and five at 150px, so the reservation could not
   have matched at both widths — or in a language whose copy is longer. */
.playlist-name {
  font-weight: 600;
  line-height: 1.4; min-height: 1.4em;
  display: -webkit-box; -webkit-box-orient: vertical;
  -webkit-line-clamp: 1; line-clamp: 1; overflow: hidden;
}
.playlist-tagline {
  font-size: .82rem; color: var(--fg-dim);
  line-height: 1.35; min-height: calc(1.35em * 3);
  display: -webkit-box; -webkit-box-orient: vertical;
  -webkit-line-clamp: 3; line-clamp: 3; overflow: hidden;
}
.playlist-count {
  font-size: .74rem; color: var(--fg-dim); opacity: .8;
  line-height: 1.4; min-height: 1.4em;
}

/* ---------------------------------------------------- playlist detail */

.back-btn {
  display: inline-flex; align-items: center; gap: .35rem;
  font: inherit; font-size: .85rem;
  padding: .4rem .6rem .4rem .35rem;
  margin-bottom: 1rem;
  border-radius: 999px; border: 1px solid var(--line);
  background: transparent; color: var(--fg-dim); cursor: pointer;
}
.back-btn:hover { color: var(--fg); }
.back-ico { width: 16px; height: 16px; display: block; }

.detail-hero { display: flex; gap: 1.1rem; align-items: flex-end; margin-bottom: 1.4rem; }
.cover-lg { width: clamp(110px, 26vw, 168px); flex: none; }
.detail-name { margin: 0 0 .3rem; font-size: clamp(1.4rem, 4vw, 2rem); }
.detail-tagline { margin: 0 0 .8rem; color: var(--fg-dim); max-width: 42ch; }
.play-all {
  display: inline-flex; align-items: center; gap: .45rem;
  font: inherit; font-weight: 600; font-size: .88rem;
  padding: .5rem 1rem; border-radius: 999px; border: none;
  background: var(--accent); color: var(--bg); cursor: pointer;
}
.play-all-ico { width: 15px; height: 15px; display: block; }

.track-list { list-style: none; margin: 0; padding: 0; }
.track-row {
  display: flex; align-items: center; gap: .4rem;
  border-radius: 10px;
}
.track-row:hover { background: color-mix(in oklab, var(--fg) 6%, transparent); }
.track-main {
  flex: 1; min-width: 0;
  display: flex; align-items: center; gap: .85rem;
  padding: .6rem .7rem;
  font: inherit; text-align: left;
  background: none; border: none; color: inherit; cursor: pointer;
}
.track-num { width: 1.6rem; flex: none; color: var(--fg-dim); font-size: .82rem; font-variant-numeric: tabular-nums; }
.track-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: .1rem; }
.track-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* The channel's own accent hue, so the badge reads as part of the playlist
   rather than as a notification. `flex: none` keeps it at full width while the
   title beside it ellipsises, which is the right way round on a narrow phone:
   a truncated title is still recognisable, half a badge is not. */
.track-line { display: flex; align-items: baseline; gap: .45rem; min-width: 0; }
.track-line .track-name { min-width: 0; }
.track-new {
  flex: none;
  padding: .06rem .34rem;
  border: 1px solid color-mix(in oklab, var(--accent) 45%, transparent);
  border-radius: 999px;
  background: color-mix(in oklab, var(--accent) 16%, transparent);
  color: color-mix(in oklab, var(--accent) 72%, var(--fg));
  font-size: .62rem; font-weight: 600; letter-spacing: .04em; text-transform: uppercase;
}
.track-by { font-size: .76rem; color: var(--fg-dim); }
.track-time { flex: none; color: var(--fg-dim); font-size: .82rem; font-variant-numeric: tabular-nums; }
.track-loading { padding: .8rem .7rem; color: var(--fg-dim); font-size: .9rem; }

.row-btn {
  flex: none;
  display: grid; place-items: center;
  width: 38px; height: 38px; margin-right: .4rem;
  border-radius: 50%; border: none;
  background: none; color: var(--fg-dim); cursor: pointer;
}
.row-btn:hover { color: var(--fg); background: color-mix(in oklab, var(--fg) 9%, transparent); }
.row-btn svg { width: 18px; height: 18px; }
/* A cross-origin download is fetched before it is saved, so the button has a
   wait the same button never used to have. */
.row-btn.is-busy { pointer-events: none; opacity: .5; }
.row-btn.is-busy svg { animation: row-btn-pulse 1s ease-in-out infinite; }
@keyframes row-btn-pulse { 50% { opacity: .35; } }
@media (prefers-reduced-motion: reduce) { .row-btn.is-busy svg { animation: none; } }

@media (max-width: 34rem) {
  .playlist-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: .8rem; }
  .detail-hero { gap: .8rem; }
}

/* Placeholder cards, in index.html, holding the grid's space until the real one
   renders. They wear the same classes as a real card rather than carrying their
   own measurements, so the space they hold follows the column width and the
   line heights above without anyone having to keep two numbers in step. */
.is-skeleton { pointer-events: none; }
.is-skeleton .cover {
  background: var(--line-soft);
  box-shadow: none;
}
.is-skeleton .playlist-name,
.is-skeleton .playlist-tagline,
.is-skeleton .playlist-count { opacity: .45; }
.is-skeleton .playlist-name { width: 60%; }
.is-skeleton .playlist-count { width: 32%; }

/* The card is a wrapper so the open button and the play button can be siblings;
   a button may not contain another button. */
.playlist-card { position: relative; display: flex; }
.playlist-open {
  flex: 1; min-width: 0;
  display: flex; flex-direction: column; gap: .7rem;
  padding: 0; text-align: left;
  background: none; border: none; color: inherit; cursor: pointer;
}
.cover-overlay {
  position: absolute;
  left: 0; right: 0; top: 0;
  aspect-ratio: 1;
  pointer-events: none;
}
.cover-play {
  position: absolute;
  right: .55rem; bottom: .55rem;
  pointer-events: auto;
  display: grid; place-items: center;
  width: 44px; height: 44px;
  border: none; border-radius: 50%;
  background: var(--accent); color: var(--bg);
  cursor: pointer;
  opacity: 0; transform: translateY(6px);
  transition: opacity .18s var(--ease), transform .18s var(--ease);
  box-shadow: 0 6px 18px rgb(0 0 0 / .35);
}
.cover-play svg { width: 17px; height: 17px; margin-left: 2px; }
.playlist-card:hover .cover-play,
.cover-play:focus-visible { opacity: 1; transform: translateY(0); }
/* Touch has no hover, so the affordance must simply be there. */
@media (hover: none) {
  .cover-play { opacity: 1; transform: none; }
}

/* The card whose tracks are being fetched keeps its play button visible and
   spins it, so the wait has a home on the thing that was clicked rather than
   only on the transport. It outranks the dimming above (which is why it is
   written body-first) and the glyph is hidden rather than removed, so the
   button does not resize under the ring. */
body.is-busy .playlist-card.is-loading { opacity: 1; }
.playlist-card.is-loading .cover-play { opacity: 1; transform: none; }
.playlist-card.is-loading .cover-play svg { visibility: hidden; }
.playlist-card.is-loading .cover-play::after {
  content: '';
  position: absolute;
  inset: 13px;
  border-radius: 50%;
  border: 2px solid transparent;
  border-top-color: currentColor;
  animation: lw-spin .9s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  .playlist-card.is-loading .cover-play::after { animation-duration: 2.4s; }
}

/* ------------------------------------------------- transport, redesigned */

/* One primary row, centred on the play button so the hierarchy is obvious at a
   glance, and one quiet utility row. The skip controls are deliberately smaller
   than prev/next: they are used less and should not compete for attention. */
.transport {
  margin-top: 1.25rem;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: clamp(.35rem, 2.5vw, .9rem);
  flex-wrap: nowrap;
}
.transport .tbtn { flex: 0 0 auto; }
.tbtn-sm { width: 40px; height: 40px; padding: 9px; color: var(--fg-faint); }
.tbtn-sm:hover { color: var(--fg); }

.utils {
  margin-top: .9rem;
  display: flex;
  align-items: center;
  gap: .4rem;
}
.utils-spacer { flex: 1 1 auto; }
.utils .icon-btn.is-on {
  color: var(--accent);
  background: color-mix(in oklab, var(--accent) 14%, transparent);
}

/* The volume slider only needs to be big enough to aim at; on a phone it gives
   its width back to the row instead. */
.volume input[type="range"] { width: clamp(64px, 16vw, 108px); }

@media (max-width: 34rem) {
  /* One-handed reach: the primary row centres so the play button sits under
     the thumb rather than against the left edge. */
  .transport { justify-content: center; gap: .5rem; margin-top: 1rem; }
  .utils { margin-top: .75rem; gap: .3rem; }
  .timer-btn { padding: 0 .5rem; }
  .volume input[type="range"] { width: 84px; }
}

/* The transport rows are tighter now, so the surrounding rhythm follows. */
.up-next { margin-top: .95rem; }
@media (max-width: 34rem) {
  .up-next { margin-top: .8rem; }
}

/* ------------------------------------------------------- vertical volume */

.volume { position: relative; display: flex; align-items: center; }

/* An explicit display value beats the browser's `[hidden] { display: none }`,
   so the popover has to opt back out or it is visible on load. */
.volume-menu[hidden] { display: none; }

.volume-menu {
  position: absolute;
  right: 50%;
  transform: translateX(50%);
  bottom: calc(100% + .5rem);
  z-index: 20;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .5rem;
  padding: .6rem .5rem .5rem;
  border-radius: 14px;
  border: 1px solid var(--line);
  background: var(--bg-raised);
  box-shadow: 0 18px 44px rgb(0 0 0 / .38);
}

.volume-pct {
  font-size: .7rem;
  font-variant-numeric: tabular-nums;
  color: var(--fg-dim);
  min-width: 2ch;
  text-align: center;
}

/* A drawn slider with the real input transparently on top. The native vertical
   range control does not centre its thumb on its track — every combination of
   track width, thumb width and margin tried left the thumb beside the rail
   rather than on it — so only the input's behaviour is used, not its painting. */
.vslider {
  position: relative;
  width: 26px;
  height: 104px;
  --pct: 70%;
}
.vslider-rail,
.vslider-fill {
  position: absolute;
  left: 50%;
  width: 5px;
  transform: translateX(-50%);
  border-radius: 999px;
  pointer-events: none;
}
.vslider-rail {
  top: 0; bottom: 0;
  background: color-mix(in oklab, var(--fg) 18%, transparent);
}
.vslider-fill {
  bottom: 0;
  height: var(--pct);
  background: var(--accent);
}
.vslider-thumb {
  position: absolute;
  left: 50%;
  bottom: var(--pct);
  width: 13px; height: 13px;
  margin-left: -6.5px;
  margin-bottom: -6.5px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 1px 4px rgb(0 0 0 / .45);
  pointer-events: none;
}
/* Transparent, but still the thing you actually drag and focus. */
.vslider input[type="range"] {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
  writing-mode: vertical-rl;
  direction: rtl;
  appearance: none;
  -webkit-appearance: none;
}
/* :focus-within would ring the thumb on every mouse click too. The thumb comes
   before the input in the DOM, so a sibling combinator cannot reach it — :has
   is what makes "keyboard focus only" expressible here. */
.vslider:has(input:focus-visible) .vslider-thumb {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

.volume-mute {
  display: grid; place-items: center;
  width: 30px; height: 30px;
  border-radius: 50%; border: none;
  background: none; color: var(--fg-dim); cursor: pointer;
}
.volume-mute:hover { color: var(--fg); background: color-mix(in oklab, var(--fg) 9%, transparent); }
.volume-mute.is-on { color: var(--accent); background: color-mix(in oklab, var(--accent) 15%, transparent); }
.volume-mute svg { width: 16px; height: 16px; }

.vol-btn { gap: .45rem; }
.vol-level {
  display: block; width: 34px; height: 4px;
  border-radius: 999px;
  background: color-mix(in oklab, var(--fg) 18%, transparent);
  overflow: hidden;
}
.vol-level-fill { display: block; height: 100%; width: 70%; background: currentColor; border-radius: 999px; }
.vol-btn.is-muted .vol-level-fill { background: var(--fg-faint); }

/* ---- transport weighting ------------------------------------------------ */

/* Prev/next were so low-contrast they read as disabled. They are primary
   navigation and should look it; the ±10 skips stay quieter and smaller. */
.transport .tbtn { color: var(--fg); }
.transport .tbtn:disabled { color: var(--fg-faint); opacity: .35; }
.tbtn-sm { color: var(--fg-dim); }
.tbtn-sm:hover { color: var(--fg); }

/* The play button no longer needs to shout now that the row around it is
   correctly weighted. */
.tbtn-play { width: 56px; height: 56px; padding: 15px; }

/* ---- progress ----------------------------------------------------------- */

.scrubber .bar { height: 6px; border-radius: 999px; background: color-mix(in oklab, var(--fg) 14%, transparent); }
.scrubber .bar-fill { height: 100%; border-radius: 999px; }
.bar::after { width: 13px; height: 13px; margin: -6.5px 0 0 -6.5px; box-shadow: 0 1px 4px rgb(0 0 0 / .4); }

@media (max-width: 40rem) {
  .utils { margin-top: 1.1rem; width: 100%; max-width: 20rem; }
  .tbtn-play { width: 60px; height: 60px; padding: 16px; }
}

/* Three equal segments rather than space-between: the buttons have unequal
   intrinsic widths, so distributing the free space between them left the row
   looking bunched to one side. */
.utils > * { flex: 1 1 0; min-width: 0; }
.utils .util-btn { width: 100%; justify-content: center; }
.utils .timer-wrap, .utils .volume { display: flex; }

/* The transport sits under a left-aligned title but should read as the centre
   of the player, not as another left-aligned row. Centre it, and the utility
   bar with it, inside the content column. */
.state.player > .transport { justify-content: center; }
.state.player > .utils { margin-inline: auto; }

/* The level bar is content, not slack: without this the equal-width segments
   squeeze it to zero and the volume control loses the thing that made it
   readable as a volume control. */
.vol-level { flex: 0 0 auto; }
.utils .util-btn { padding-inline: .45rem; gap: .35rem; }
/* Shrink-to-fit left the bar too narrow to hold its own contents. */
.state.player > .utils { width: min(22rem, 100%); }

/* ------------------------------- utility row (restored; see note below) ----
   These rules were deleted by an index-based edit that sliced from
   `.volume-menu` to `.vol-btn` and took everything in between with it. Without
   the base button rule the controls fell back to the browser's default button
   chrome, and without .util-ico the SVGs — which are width:100% by design —
   expanded to fill them. Kept at the end of the file so the cascade is
   obvious. */
.utils {
  margin-top: 1rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .35rem;
  padding: .3rem;
  border-radius: 999px;
  background: color-mix(in oklab, var(--fg) 5%, transparent);
  border: 1px solid var(--line-soft);
}
.util-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .4rem;
  height: 36px;
  padding: 0 .7rem;
  font: inherit;
  font-size: .76rem;
  border: none;
  border-radius: 999px;
  background: none;
  color: var(--fg-dim);
  cursor: pointer;
  font-variant-numeric: tabular-nums;
}
.util-btn:hover { color: var(--fg); background: color-mix(in oklab, var(--fg) 8%, transparent); }
.util-btn.is-on { color: var(--accent); background: color-mix(in oklab, var(--accent) 15%, transparent); }
.util-ico { width: 17px; height: 17px; display: block; flex: none; }
.util-ico svg { display: block; width: 100%; height: 100%; }
.timer-btn .timer-text:empty { display: none; }

/* ------------------------- player layout (restored; same deletion as above) --
   The index-based slice that removed the utility styles took this block too:
   the grid areas, and the width that keeps the artwork to 168px beside the
   controls. Without a width the cover filled the column and dominated the
   page. */
.state.player {
  display: grid;
  grid-template-columns: 168px 1fr;
  grid-template-areas:
    'cover info'
    'cover scrub'
    'cover transport'
    'cover utils'
    'cover next';
  column-gap: 1.6rem;
  align-content: start;
}
.state.player > .now-cover { grid-area: cover; align-self: start; }
.state.player > .now-info { grid-area: info; }
.state.player > .scrubber { grid-area: scrub; }
.state.player > .transport { grid-area: transport; }
.state.player > .utils { grid-area: utils; }
.state.player > .up-next { grid-area: next; }

.now-cover {
  width: 168px;
  aspect-ratio: 1;
  border-radius: 14px;
  overflow: hidden;
  background: color-mix(in oklab, hsl(var(--hue) 40% 40%) 20%, var(--bg));
  box-shadow: 0 12px 30px rgb(0 0 0 / .34);
}
.now-cover svg { display: block; width: 100%; height: 100%; }
.state.player .eyebrow { margin-top: 0; }

@media (max-width: 40rem) {
  /* One column stacks the artwork above everything else, so the player is
     roughly 590px here against 350px on a wide screen — the desktop
     reservation is nowhere near enough and the page jumped ~0.14 without
     this. Sized to the tallest case, the 260px artwork cap, which is reached
     at any width from about 420px up, plus the padding. */
  .stage { min-height: 42rem; }
  .state.player {
    grid-template-columns: 1fr;
    grid-template-areas: 'cover' 'info' 'scrub' 'transport' 'utils' 'next';
    justify-items: center;
    text-align: center;
    row-gap: 0;
  }
  .now-cover { width: min(62vw, 260px); margin-bottom: 1.25rem; }
  .state.player > .now-info,
  .state.player > .scrubber,
  .state.player > .up-next { width: 100%; }
  .state.player .track-title { font-size: clamp(1.5rem, 7vw, 2rem); }
  .state.player .up-next { justify-content: center; }
}

/* Confirmation that a copy actually happened; a silent copy reads as a dead button. */
.share-flash {
  font-size: .72rem;
  white-space: nowrap;
  margin-left: .1rem;
}

/* ------------------------------------------------------------------ intro -- */
/* The static, crawlable copy under the channel grid (index.html and the
   generated landing pages). Full shell width, so its rule and its text line up
   with the playlist grid above rather than sitting in a narrower column of
   their own. The lists go multi-column at that width: a single column of
   eleven short items would leave most of the row empty. */
.intro {
  width: 100%;
  border-top: 1px solid var(--line-soft);
  padding-top: 1.5rem;
}
.intro-lead { color: var(--fg); font-size: 1rem; margin-top: .35rem; }
.intro-channels, .intro-tracks, .intro-faq { list-style: none; padding: 0; margin: 0; display: grid; gap: .45rem; }
@media (min-width: 48rem) {
  /* Multi-column rather than a two-track grid: a grid aligns rows, so one
     two-line item leaves a gap beside every short one. Columns flow. */
  .intro-channels, .intro-tracks { display: block; columns: 2; column-gap: 2.5rem; }
  .intro-channels li, .intro-tracks li { break-inside: avoid; margin-bottom: .45rem; }
  .intro-faq { grid-template-columns: 1fr 1fr; column-gap: 2.5rem; align-items: start; }
}
.intro-channels li, .intro-tracks li, .intro-faq li { color: var(--fg-dim); font-size: .9rem; text-wrap: pretty; }
.intro-channels a, .intro-related a { color: var(--fg); text-decoration: none; border-bottom: 1px solid var(--line); }
.intro-channels a:hover, .intro-related a:hover { border-bottom-color: var(--accent); }
.intro-faq { row-gap: 1rem; }
.intro-tracks + p, .intro-channels + p { margin-top: .9rem; }
.intro-faq h3 { font-size: .92rem; font-weight: 600; color: var(--fg); margin: 0 0 .2rem; }
.intro-related { display: flex; flex-wrap: wrap; gap: .4rem .9rem; font-size: .9rem; }
