/*
  Homeward — site.css
  The single stylesheet. Design tokens live as custom properties on :root so
  every component reads from one source of truth. Mobile-first; the only
  breakpoints are 600px and 900px. See CLAUDE.md "Design system".
*/

/* ====================================================================== */
/* Tokens                                                                 */
/* ====================================================================== */

/*
  These tokens must stay identical to the ones inlined from critical.css.
  This stylesheet loads asynchronously and lands *after* first paint, so a
  different :root here does not merely disagree - it repaints the whole page
  once it promotes. That is what a dark-default block here would do to a
  light-default page: paint light, then jump.

  LIGHT IS THE DEFAULT. Dark applies on an explicit choice, or on a system
  preference while no choice has been made.
*/
:root {
  /*
    The page is tinted, the cards are white. It used to be #FFFFFF for both,
    which meant every card, panel and player surface was the same colour as
    the page behind it and the whole light theme read as one flat sheet with
    hairlines drawn on it. A card has to sit *on* something.

    Revision 3 specified #FFFFFF/#F7F7F9 here; this supersedes it at the
    user's request. Contrast was measured on the new ground, not assumed:
    --text #121826 is 15.8:1 on the page and 17.9:1 on a card. --muted moved
    from #6B6B78 to #5C6270 because the tinted page cost it contrast - it was
    5.25:1 on white and only 4.65:1 on #FFF6EC, which passes AA but with
    nothing to spare. At #5C6270 it is 5.25:1 on the page again.
  */
  --bg: #FFF6EC;
  --surface: #FFFFFF;
  --surface-2: #FFFAF4;
  --surface-3: #F5E8D8;
  --text: #121826;
  --muted: #5C6270;
  --border: #EBDCCB;

  /* --accent is a fill: near-black on orange is 6.8:1. Orange text on white is
     2.8:1 and fails AA, so accent-coloured *text* uses --accent-ink at 5.9:1. */
  --accent: #F26B1D;
  --accent-hover: #D95A12;
  --accent-contrast: #121826;
  --accent-ink: #B3480A;
  --focus: #B3480A;
  --shadow: 0 8px 32px rgba(16, 16, 28, 0.10);
  --overlay: rgba(255, 246, 236, 0.78);
  --flag-edge: rgba(16, 16, 28, 0.16);
  --skeleton: #F5E8D8;

  /* The hero band and its artwork. One pair per theme — see the note on
     .hero. The band colour is sampled from the image so the two meet
     without a seam. Mirrored in critical.css. */
  --hero-band: #FFFFFF;
  --hero-art: url("/static/hero/hero-light.webp");

  --font: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial,
    "Nirmala UI", "Noto Sans Devanagari", "Noto Sans Gurmukhi", "Noto Sans Bengali",
    "Noto Sans Tamil", "Noto Sans Telugu", "Noto Sans Gujarati",
    "Noto Sans Arabic", "Noto Naskh Arabic", "Vazirmatn", "Tahoma",
    "Noto Sans Hebrew", "Arial Hebrew", "David", "Segoe UI Historic",
    "PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC",
    "Hiragino Sans", "Yu Gothic UI", "Meiryo",
    "Malgun Gothic", "Apple SD Gothic Neo", sans-serif;
  --fs-xxl: 32px;
  --fs-xl: 24px;
  --fs-lg: 18px;
  --fs-md: 16px;
  --fs-sm: 14px;
  --lh: 1.5;
  --lh-tight: 1.25;
  --s1: 8px;
  --s2: 16px;
  --s3: 24px;
  --s4: 32px;
  --s5: 40px;
  --s6: 48px;
  --wrap: 1200px;
  --r-sm: 8px;
  --r-md: 12px;
  --r-lg: 16px;
  --r-full: 999px;
  --tap: 44px;
  --t-fast: 150ms ease;
  --t-base: 200ms ease;
  --player-h: 72px;

  color-scheme: light;
}

/* A system preference is a hint, not a decision. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
  --bg: #121826;
  --surface: #1A2132;
  --surface-2: #222A3D;
  --surface-3: #2C3549;
  --text: #F3F4F8;
  --muted: #A3ABBC;
  --border: #2E374C;
  --accent: #F26B1D;
  --accent-hover: #F5843F;
  --accent-contrast: #121826;
  --accent-ink: #F5843F;
  --focus: #F26B1D;
  --shadow: 0 8px 32px rgba(0, 0, 0, 0.45);
  --overlay: rgba(18, 24, 38, 0.6);
  --flag-edge: rgba(255, 255, 255, 0.18);
  --skeleton: #222A3D;
  --hero-band: #000103;
  --hero-art: url("/static/hero/hero-dark.webp");
  color-scheme: dark;
  }
}

/* An explicit choice wins, in both directions. */
:root[data-theme="dark"] {
  --bg: #121826;
  --surface: #1A2132;
  --surface-2: #222A3D;
  --surface-3: #2C3549;
  --text: #F3F4F8;
  --muted: #A3ABBC;
  --border: #2E374C;
  --accent: #F26B1D;
  --accent-hover: #F5843F;
  --accent-contrast: #121826;
  --accent-ink: #F5843F;
  --focus: #F26B1D;
  --shadow: 0 8px 32px rgba(0, 0, 0, 0.45);
  --overlay: rgba(18, 24, 38, 0.6);
  --flag-edge: rgba(255, 255, 255, 0.18);
  --skeleton: #222A3D;
  --hero-band: #000103;
  --hero-art: url("/static/hero/hero-dark.webp");
  color-scheme: dark;
}

:root[data-theme="light"] {
  color-scheme: light;
}

@media (min-width: 900px) {
  :root {
    --fs-xxl: 44px;
    --player-h: 80px;
  }
}

/* ====================================================================== */
/* Reset                                                                  */
/* ====================================================================== */

*, *::before, *::after { box-sizing: border-box; }

/*
  The `hidden` attribute must always win. Author rules such as
  `.btn { display: inline-flex }` outrank the browser's own
  `[hidden] { display: none }`, which silently left "hidden" controls on screen.
*/
[hidden] { display: none !important; }

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: var(--fs-md);
  line-height: var(--lh);
  -webkit-font-smoothing: antialiased;
  /* Never let the fixed player cover the end of the page. */
  padding-bottom: calc(var(--player-h) + var(--s3));
}

img, svg { display: block; max-width: 100%; }

/* Sprite icons (<use href="#i-*">) inherit the current text colour. */
.icon { fill: currentColor; color: inherit; }

a { color: var(--text); text-decoration: none; }
a:hover { color: var(--accent-ink); }

button, input, select, textarea { font: inherit; color: inherit; }

button {
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  color: inherit;
}

ul, ol { margin: 0; padding: 0; list-style: none; }

h1, h2, h3 { margin: 0; line-height: var(--lh-tight); font-weight: 700; }
h1 { font-size: var(--fs-xxl); letter-spacing: -0.02em; }
h2 { font-size: var(--fs-xl); letter-spacing: -0.01em; }
h3 { font-size: var(--fs-md); }
p { margin: 0; }

:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: 4px;
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
}

/* ====================================================================== */
/* Utilities                                                              */
/* ====================================================================== */

.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--s2);
}

.wrap--narrow { max-width: 640px; }

.text-muted { color: var(--muted); }
.text-sm { font-size: var(--fs-sm); }

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/*
  inset-inline-start, not left. Parking this at left: -9999px on a right-to-left
  page puts it 10,000px past the *end* of the flow, which the browser counts as
  real width: a horizontal scrollbar on every Arabic and Urdu page. The logical
  property parks it before the start in both directions, where nothing counts it.
*/
.skip-link {
  position: absolute;
  inset-inline-start: -9999px;
  top: var(--s1);
  z-index: 100;
  padding: var(--s1) var(--s2);
  background: var(--accent);
  color: var(--accent-contrast);
  border-radius: var(--r-sm);
}
.skip-link:focus { inset-inline-start: var(--s2); }

.empty {
  color: var(--muted);
  padding: var(--s5) var(--s2);
  text-align: center;
}
.empty a { color: var(--accent-ink); text-decoration: underline; }

.link-btn {
  color: var(--accent-ink);
  text-decoration: underline;
  min-height: var(--tap);
}

/* ====================================================================== */
/* Header / footer                                                        */
/* ====================================================================== */

.site-header {
  position: sticky;
  top: 0;
  z-index: 20;
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--border);
}

.site-header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s1);
  min-height: 64px;
  flex-wrap: nowrap;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: var(--s1);
  min-width: 0;
  font-weight: 700;
  font-size: var(--fs-md);
  letter-spacing: -0.01em;
}
.brand__name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

@media (min-width: 600px) {
  .brand { font-size: var(--fs-lg); }
  .site-header__inner { gap: var(--s2); }
}
.brand:hover { color: var(--text); }
.brand__mark {
  display: grid;
  place-items: center;
  flex-shrink: 0;
  width: 32px; height: 32px;
  border-radius: var(--r-sm);
  background: var(--accent);
  color: var(--accent-contrast);
}

/* Homeward lockup: header and hero. Fixed sizes, declared identically in
   critical.css (CSS_PARITY), so the async sheet never moves them. */
.brand__lockup {
  display: block;
  flex-shrink: 0;
  width: 153px;
  height: 24px;
  color: var(--text);
}
@media (min-width: 600px) {
  .brand__lockup { width: 203px; height: 32px; }
}
.hero__brand {
  margin: 0 0 var(--s1);
  line-height: 0;
  color: var(--text);
}
.hero__brand svg {
  display: block;
  width: 229px;
  height: 36px;
}
@media (min-width: 600px) {
  .hero__brand svg { width: 305px; height: 48px; }
}

.site-nav {
  display: flex;
  align-items: center;
  gap: 2px;
  flex-shrink: 0;
}
.site-nav a {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  padding-inline: 6px;
  color: var(--muted);
  font-size: var(--fs-sm);
  font-weight: 500;
  white-space: nowrap;
  transition: color var(--t-fast);
}
@media (min-width: 600px) {
  .site-nav { gap: var(--s1); }
  .site-nav a { padding-inline: var(--s1); }
}
.site-nav a:hover { color: var(--text); }

.icon-btn {
  display: inline-grid;
  place-items: center;
  min-width: var(--tap);
  min-height: var(--tap);
  border-radius: var(--r-full);
  color: var(--muted);
  transition: color var(--t-fast), background var(--t-fast);
}
.icon-btn:hover { color: var(--text); background: var(--surface-2); }
.icon-btn[aria-pressed="true"] { color: var(--accent-ink); }
.icon-btn__glyph { font-size: var(--fs-md); line-height: 1; }

.site-footer {
  border-top: 1px solid var(--border);
  margin-top: var(--s6);
  padding-block: var(--s4);
  color: var(--muted);
  font-size: var(--fs-sm);
}
.site-footer p + p { margin-top: var(--s1); }
.site-footer a { color: var(--text); text-decoration: underline; }
.site-footer a:hover { color: var(--accent-ink); }

.site-main { min-height: 60vh; }

/* ====================================================================== */

/* ====================================================================== */
/* Search field and typeahead                                             */
/* ====================================================================== */

.search {
  position: relative;
  display: flex;
  align-items: center;
  max-width: 640px;
}

.search__icon {
  position: absolute;
  inset-inline-start: var(--s2);
  color: var(--muted);
  pointer-events: none;
}

.search__input {
  width: 100%;
  min-height: 56px;
  padding: var(--s2) var(--s5) var(--s2) 52px;
  font-size: var(--fs-lg);
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-full);
  transition: border-color var(--t-fast), box-shadow var(--t-fast);
}
.search__input::placeholder { color: var(--muted); }
.search__input:focus {
  outline: none;
  border-color: var(--accent-ink);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 25%, transparent);
}
.search__input::-webkit-search-cancel-button { display: none; }

.search__clear {
  position: absolute;
  inset-inline-end: var(--s1);
  width: var(--tap);
  height: var(--tap);
  border-radius: var(--r-full);
  color: var(--muted);
  font-size: 24px;
  line-height: 1;
}
.search__clear:hover { color: var(--text); background: var(--surface-2); }

/* The header field only appears where there is room for it without
   squeezing the brand; below that the hero field is the way in. */
.search--header { display: none; }
@media (min-width: 900px) {
  .search--header { display: flex; max-width: 240px; flex: 0 1 240px; }
  .search--header .search__input {
    min-height: 40px;
    font-size: var(--fs-sm);
    padding: 0 var(--s2) 0 40px;
  }
  .search--header .search__icon { inset-inline-start: var(--s2); }
}

.typeahead {
  position: absolute;
  top: calc(100% + var(--s1));
  left: 0;
  right: 0;
  z-index: 40;
  max-height: min(60vh, 460px);
  overflow-y: auto;
  padding: var(--s1);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  box-shadow: var(--shadow);
}
.typeahead[hidden] { display: none; }

.typeahead__group {
  padding: var(--s1) var(--s2) 4px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
}

.typeahead__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s2);
  min-height: var(--tap);
  padding: var(--s1) var(--s2);
  border-radius: var(--r-sm);
  color: var(--text);
  transition: background var(--t-fast);
}
.typeahead__row:hover,
.typeahead__row.is-active { background: var(--surface-2); color: var(--text); }
.typeahead__row.is-active { box-shadow: inset 2px 0 0 var(--accent); }

.typeahead__name {
  font-size: var(--fs-sm);
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.typeahead__meta { flex-shrink: 0; font-size: 12px; color: var(--muted); }
.typeahead__empty { padding: var(--s2); font-size: var(--fs-sm); color: var(--muted); }

/* ====================================================================== */
/* Hero                                                                   */
/* ====================================================================== */

/*
  Hero composition.

  Two banners, one per theme — a daylight Earth on near-white, an Earth at
  night on near-black. Each matches its own page, so unlike the single black
  banner this replaces, the hero is no longer forced dark in both themes and
  the copy goes back to the ordinary theme tokens. `--hero-band` is the flat
  colour the artwork's empty half fades into, sampled from the images so the
  seam between image and page is invisible.

  Everything here that sets a box is duplicated in critical.css and guarded by
  CSS_PARITY. The hero is the first thing painted on the busiest page on the
  site, and the copy moving when the async stylesheet lands would take the H1,
  the search field and every chip with it.

  The copy is capped at 52% so it never runs into the globe. The chip rails sit
  outside that cap and run the full width — they are a control, not decoration.

  Both artworks are free of text, so nothing here has to fight to hide baked-in
  English any more: the RTL veil and the mobile crop that the previous banner
  needed are gone, and the composition mirrors normally.
*/
.hero {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  padding-block: var(--s5) var(--s4);
  background: var(--hero-band);
}
.hero__inner { position: relative; z-index: 1; }
.hero__copy { position: relative; min-width: 0; }
@media (min-width: 860px) {
  .hero__copy { max-width: 52%; }
}

/* The banner. A pseudo-element so the gradients that blend its edges into the
   band do not disturb .hero's own background. Anchored top-right and held to
   82% of the hero's height so the planet sits above the rails rather than
   behind them.

   Capping the height makes the image narrower than the hero, so its leading
   edge meets the band as a hard vertical line. The horizontal gradient has to
   run *past* that edge to hide it: at 82% height the image starts around 39%
   across on a 1440 viewport, so the feather ends at 54%, not before it. A
   vertical gradient does the same for the cropped lower edge. */
.hero::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  /* A wash behind the copy, which sits on the photograph below 860px. */
  background: linear-gradient(180deg,
      color-mix(in srgb, var(--hero-band) 80%, transparent) 0%,
      color-mix(in srgb, var(--hero-band) 80%, transparent) 46%,
      transparent 64%),
    linear-gradient(180deg, transparent 58%, var(--hero-band) 96%),
    var(--hero-art) no-repeat 72% top;
  background-size: auto, auto, auto 132%;
}
@media (min-width: 860px) {
  .hero::before {
    background: linear-gradient(90deg, var(--hero-band) 28%, transparent 54%),
      linear-gradient(180deg, transparent 56%, var(--hero-band) 96%),
      var(--hero-art) no-repeat right top;
    background-size: auto, auto, auto 82%;
  }
}

.hero__title-accent { color: var(--accent); }

/*
  The live badge and the brand line.

  Both are real translated text in the copy column, not baked into the
  artwork and not floating over it. The photograph is busy and its brightness
  varies across the two themes, so nothing laid on top of it could be
  guaranteed legible in 36 languages and four scripts. In the column it always
  is, it mirrors in RTL for free, and the letter-spacing that makes uppercase
  read as premium is applied per-script: Latin and Cyrillic take it, the Indic,
  CJK, Arabic and Hebrew scripts do not, because spacing them out breaks
  ligatures and conjuncts rather than looking refined.

  Geometry is duplicated in critical.css — both add height to the hero.
*/
.hero__badge {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 14px;
  padding: 7px 15px;
  border-radius: var(--r-full);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--accent-ink);
  background: color-mix(in srgb, var(--accent) 10%, var(--hero-band));
  border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent);
  box-shadow: 0 4px 18px color-mix(in srgb, var(--accent) 16%, transparent);
}
.hero__badge-dot {
  width: 7px;
  height: 7px;
  border-radius: var(--r-full);
  flex: none;
  background: var(--accent);
  animation: hero-live 2.4s ease-in-out infinite;
}
@keyframes hero-live {
  0%, 100% { opacity: 1; box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 55%, transparent); }
  70%      { opacity: 0.65; box-shadow: 0 0 0 7px color-mix(in srgb, var(--accent) 0%, transparent); }
}
@media (prefers-reduced-motion: reduce) { .hero__badge-dot { animation: none; } }

.hero__kicker {
  margin: var(--s2) 0 0;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--muted);
}

/*
  The hero's closing strip.

  The reference had these two word lists as vertical columns in the artwork's
  right margin. There is no such margin here: the globe reaches the viewport
  edge, and its limb is bright in the light photograph and dark in the night
  one, so a single grey treatment laid over it would be unreadable in one
  theme or the other. Below the rails they sit on the flat band colour, which
  is known in both themes and in every language.

  A hairline above turns them from stray words into a deliberate closing rule,
  and the separator is drawn between words rather than typed into the string,
  so no translator has to carry punctuation.
*/
.hero__meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2) var(--s4);
  justify-content: space-between;
  margin: var(--s3) 0 0;
  padding-top: var(--s2);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--muted);
  border-top: 1px solid color-mix(in srgb, var(--muted) 24%, transparent);
}
.hero__meta-set { display: flex; flex-wrap: wrap; gap: var(--s2); }
/*
  Uppercase and wide tracking are a Latin typographic device. Devanagari,
  Bengali, Tamil, Telugu, Malayalam, Gujarati and Gurmukhi build conjuncts that
  letter-spacing pulls apart; Arabic, Urdu and Persian are cursive and spacing
  breaks the joins outright; CJK has no case and tracking just looks loose.
  Those scripts keep the weight and colour and drop the rest.

  This is text metrics rather than decoration — it decides where the closing
  strip wraps — so it is duplicated in critical.css and guarded by CSS_PARITY.
*/
:is([lang="hi"],[lang="mr"],[lang="bn"],[lang="ta"],[lang="te"],[lang="ml"], [lang="gu"],[lang="pa"],[lang="ar"],[lang="ur"],[lang="fa"],[lang="he"], [lang="ja"],[lang="ko"],[lang="zh-CN"],[lang="th"])
:is(.hero__badge, .hero__kicker, .hero__meta) {
  text-transform: none;
  letter-spacing: normal;
}

/* Box duplicated in critical.css and guarded by CSS_PARITY — the dot has
   width and a gap, so it decides where the strip wraps. Only its colour is
   this file's business. Narrow screens keep the first list only: two of them
   wrap into four lines of stray words, which reads as clutter. */
.hero__meta-set > span + span::before {
  content: "";
  display: inline-block;
  width: 3px;
  height: 3px;
  margin-inline-end: var(--s2);
  border-radius: 50%;
  background: var(--accent);
  vertical-align: middle;
  opacity: 0.7;
}
@media (max-width: 599px) {
  .hero__meta-set:last-child { display: none; }
}





@media (min-width: 1000px) {
  .hero__glow { opacity: 0.45; }
}

/*
  The Earth no longer animates.

  Its motion lived on the SVG's own nodes — pulsing city lights, fading arcs —
  and there are no nodes any more now that the artwork is a background image.
  Animating a background at this size would mean repainting half the hero every
  frame, which is exactly the cost the raster was chosen to avoid, so the
  planet is deliberately still. The live badge's pulse carries the sense of
  something happening, at the cost of one 8px dot.
*/




/*
  The glow is what stops a dark page reading as an empty page, and it still
  does that job on the narrow layouts where the globe is a faint background
  wash. Once the globe is a real column it is pulled back hard: two soft
  radial fields plus the globe's own atmosphere is exactly the muddiness the
  brief warned about, and the globe should be the thing carrying the light.
*/
.hero__glow {
  position: absolute;
  inset: -40% 0 auto 0;
  height: 520px;
  z-index: -1;
  background:
    radial-gradient(60% 60% at 50% 40%, color-mix(in srgb, var(--accent) 22%, transparent), transparent 70%),
    radial-gradient(40% 50% at 15% 30%, color-mix(in srgb, #5B4B8A 26%, transparent), transparent 70%);
  filter: blur(10px);
  pointer-events: none;
}
@media (min-width: 1000px) {
  .hero__glow { opacity: 0.45; }
}

.hero__inner { position: relative; }

.hero__title {
  max-width: 15ch;
  letter-spacing: -0.03em;
}

.hero__proof {
  margin-top: var(--s2);
  max-width: 46ch;
  color: var(--muted);
  font-size: var(--fs-lg);
}

/* Second hero line: what plays in the browser, and a link to the app. */
.hero__playable {
  margin-top: var(--s1);
  max-width: 46ch;
  color: var(--muted);
  font-size: var(--fs-sm);
}
.hero__playable a {
  margin-inline-start: 0.25em;
  color: var(--accent-ink);
  font-weight: 600;
  text-decoration: underline;
  text-underline-offset: 2px;
}

.hero__search { display: block; margin-top: var(--s4); }

/* Quick-play chips: one tap from landing to audio. */
.quick {
  display: flex;
  flex-wrap: nowrap;
  gap: var(--s1);
  margin-top: var(--s3);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  margin-inline: calc(-1 * var(--s2));
  padding-inline: var(--s2);
  padding-bottom: var(--s1);
  scrollbar-width: none;
}
.quick::-webkit-scrollbar { display: none; }

@media (min-width: 700px) {
  .quick { flex-wrap: wrap; overflow: visible; margin-inline: 0; padding-inline: 0; }
}

.quick__chip {
  display: inline-flex;
  align-items: center;
  gap: var(--s1);
  min-height: var(--tap);
  padding-inline: var(--s2);
  border-radius: var(--r-full);
  background: var(--surface);
  border: 1px solid var(--border);
  color: var(--text);
  font-size: var(--fs-sm);
  font-weight: 600;
  white-space: nowrap;
  transition: border-color var(--t-fast), background var(--t-fast), transform var(--t-fast);
}
.quick__chip:hover {
  border-color: var(--accent-ink);
  background: var(--surface-2);
}
.quick__chip:active { transform: scale(0.97); }
.quick__icon { display: grid; place-items: center; color: var(--accent-ink); }

/* ====================================================================== */
/* Country tiles                                                          */
/* ====================================================================== */

.tile-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: var(--s2);
}
@media (min-width: 700px) {
  .tile-grid { grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); }
}

/* The tiles themselves are defined in the premium block further down. This
   half used to redefine them here as well, and its .tile__name set
   white-space:nowrap with overflow:hidden;text-overflow:ellipsis. Because
   white-space inherits and the ellipsis is drawn by the clipping ancestor,
   that made the child .tile__label print "The United St…" no matter what
   line-clamp it declared. Country names are long on purpose; the tile has
   room for two lines. Do not reintroduce a nowrap ancestor here. */

/* ====================================================================== */
/* Genre rail                                                             */
/* ====================================================================== */

/* Two rows that scroll sideways, so 45 genres stay browsable without a wall. */
.genre-rail {
  display: grid;
  grid-auto-flow: column;
  grid-template-rows: repeat(2, auto);
  grid-auto-columns: max-content;
  gap: var(--s1);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  margin-inline: calc(-1 * var(--s2));
  padding: 0 var(--s2) var(--s1);
  scrollbar-width: thin;
}

.genre-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--s1);
  min-height: var(--tap);
  padding-inline: var(--s2);
  border-radius: var(--r-full);
  background: var(--surface);
  border: 1px solid var(--border);
  color: var(--text);
  font-size: var(--fs-sm);
  font-weight: 500;
  white-space: nowrap;
  transition: border-color var(--t-fast), background var(--t-fast);
}
.genre-pill:hover { border-color: var(--accent-ink); background: var(--surface-2); color: var(--text); }
.genre-pill__icon { display: grid; place-items: center; color: var(--muted); }
.genre-pill:hover .genre-pill__icon { color: var(--accent-ink); }
.genre-pill__count { font-size: 11px; color: var(--muted); }

/* ====================================================================== */
/* Station page                                                           */
/* ====================================================================== */

.station-hero {
  position: absolute;
  inset: 0 0 auto 0;
  height: 340px;
  overflow: hidden;
  z-index: -1;
  pointer-events: none;
  mask-image: linear-gradient(to bottom, #000 0%, transparent 100%);
}
.station-hero__blur {
  width: 100%;
  height: 100%;
  background-size: cover;
  background-position: center;
  filter: blur(60px) saturate(1.3) brightness(0.55);
  transform: scale(1.3);
  opacity: 0.7;
}
.station-hero__blur--mono {
  background-image: linear-gradient(145deg, var(--g1), var(--g2));
}

.station-page { position: relative; }

.station-layout { display: grid; gap: var(--s4); padding-bottom: var(--s4); }

@media (min-width: 900px) {
  .station-layout {
    grid-template-columns: minmax(0, 1fr) 340px;
    gap: var(--s6);
    align-items: start;
  }
}

/* Grid items default to min-width:auto, which refuses to shrink below the
   content's min-content width. .station-main contains a .rail, and a rail is
   twelve 144px columns laid out with grid-auto-flow:column — 944px of
   min-content that a 375px phone cannot shrink. The rail scrolls sideways by
   itself; the column must simply be allowed to be narrower than it. Without
   this the whole station page scrolled horizontally on every phone, 976px of
   document in a 375px viewport. Same fix, same reason, as .home__main. */
.station-main { min-width: 0; }
.station-side { min-width: 0; }

.station-id { display: grid; gap: var(--s3); }
@media (min-width: 600px) {
  .station-id { grid-template-columns: 180px minmax(0, 1fr); gap: var(--s4); align-items: center; }
}

/* Box geometry is duplicated in critical.css and guarded by CSS_PARITY — see
   the note there. Only the surface and the drop shadow are unique to this
   file. The inset hairline that used to be declared here was dead: the
   drop-shadow declaration two lines below replaced it outright. */
.station-id__art {
  container-type: inline-size;
  display: grid;
  place-items: center;
  aspect-ratio: 1 / 1;
  width: 150px;
  padding: 10px;
  background: var(--surface-2);
  border-radius: var(--r-lg);
  overflow: hidden;
  box-shadow: 0 18px 44px rgba(0, 0, 0, 0.5);
}
@media (min-width: 600px) { .station-id__art { width: 180px; } }
.station-id__art .art__img,
.station-id__art .mono { border-radius: var(--r-lg); }

.station-id__name { overflow-wrap: anywhere; letter-spacing: -0.02em; }
.station-id__location { margin-top: var(--s1); color: var(--muted); font-size: var(--fs-md); }

.station-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s1);
  margin-top: var(--s4);
}
.btn--lg { min-height: 52px; padding-inline: var(--s4); font-size: var(--fs-lg); }

.station-links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s1);
  align-items: center;
  margin-top: var(--s4);
  color: var(--muted);
}
.station-links a { color: var(--muted); text-decoration: underline; }
.station-links a:hover { color: var(--accent-ink); }

.station-side { display: grid; gap: var(--s3); }

.panel {
  padding: var(--s3);
  border-radius: var(--r-md);
  background: var(--surface);
  border: 1px solid var(--border);
}
.panel__title {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: var(--s2);
}

.detail-list { display: grid; gap: var(--s2); }
.detail-list > div {
  display: grid;
  grid-template-columns: 100px minmax(0, 1fr);
  gap: var(--s2);
  align-items: baseline;
}
.detail-list dt { font-size: var(--fs-sm); color: var(--muted); }
.detail-list dd { margin: 0; font-size: var(--fs-sm); font-weight: 500; overflow-wrap: anywhere; }
.detail-list a { color: var(--accent-ink); text-decoration: underline; }

/* Compact station row, used in the sidebar and the player's Up next menu. */
.mini-list { display: grid; gap: var(--s1); }
.mini {
  display: grid;
  grid-template-columns: 44px minmax(0, 1fr) var(--tap);
  gap: var(--s2);
  align-items: center;
  padding: var(--s1);
  border-radius: var(--r-sm);
  transition: background var(--t-fast);
}
.mini:hover { background: var(--surface-2); }
.mini__art { container-type: inline-size; display: grid; place-items: center;
  width: 44px; height: 44px; background: var(--surface-2); border-radius: var(--r-sm); }
.mini__art .art__img, .mini__art .mono { border-radius: var(--r-sm); }
.mini__body { display: grid; gap: 1px; min-width: 0; }
.mini__name {
  font-size: var(--fs-sm);
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.mini__meta {
  font-size: 12px;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.mini__play {
  display: grid;
  place-items: center;
  width: var(--tap);
  height: var(--tap);
  border-radius: var(--r-full);
  background: var(--surface-2);
  color: var(--text);
  transition: background var(--t-fast), color var(--t-fast);
}
.mini__play:hover { background: var(--accent); color: var(--accent-contrast); }
.mini.is-playing .mini__name { color: var(--accent-ink); }

/* ====================================================================== */
/* App strip                                                              */
/* ====================================================================== */

.app-strip {
  position: fixed;
  left: 0;
  right: 0;
  bottom: calc(var(--player-h) + env(safe-area-inset-bottom, 0px));
  z-index: 24;
  background: color-mix(in srgb, var(--accent) 16%, var(--surface-2));
  border-top: 1px solid color-mix(in srgb, var(--accent) 30%, var(--border));
}
.app-strip__inner {
  display: flex;
  align-items: center;
  gap: var(--s2);
  min-height: 46px;
  font-size: var(--fs-sm);
}
.app-strip__text {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.app-strip__cta {
  flex-shrink: 0;
  font-weight: 700;
  color: var(--accent-ink);
  text-decoration: underline;
}
/* Sections                                                               */
/* ====================================================================== */

.section { margin-top: var(--s5); }

.section__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s2);
  margin-bottom: var(--s2);
}
.section__title { font-size: var(--fs-xl); }
.section__link {
  color: var(--accent-ink);
  font-size: var(--fs-sm);
  font-weight: 500;
}

.page-head { padding-block: var(--s5) var(--s2); }
.page-head p { margin-top: var(--s1); }
.page-head--center { text-align: center; }
.page-head--center .btn { margin-top: var(--s3); }

.breadcrumb {
  display: flex;
  gap: var(--s1);
  align-items: center;
  padding-block: var(--s3) var(--s2);
  font-size: var(--fs-sm);
  color: var(--muted);
}
.breadcrumb a { color: var(--muted); }
.breadcrumb a:hover { color: var(--accent-ink); }
.breadcrumb span[aria-current] {
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 50vw;
}

/* ====================================================================== */
/* Artwork: cached logo or designed monogram                              */
/* ====================================================================== */

/*
  Roughly half of all stations have no usable logo, so the monogram is not a
  fallback in the apologetic sense — it is artwork. Twelve muted gradients,
  chosen deterministically from the station name (see monogram.py), with the
  genre as a caption. Nothing ever renders as an empty box.
*/

/*
  Sized by the file's own pixels, capped by the slot.

  This used to be width:100% with object-fit:cover, which was fine while every
  cached logo was padded to the same 256px square. Now the encoder refuses to
  upscale — a 96px favicon is stored as a 96px file — and stretching it to fill
  a 256px card is exactly the blur that rule exists to avoid. The container
  supplies the square, the tint and the hairline; the image sits centred in it
  at whatever size it really is.
*/
.art__img {
  display: block;
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  image-rendering: auto;
  border-radius: var(--r-sm);
}

.mono {
  display: block;
  width: 100%;
  /* height:auto is load-bearing. The width/height attributes on <img> are
     presentational hints that set a used height, and a used height wins over
     aspect-ratio — which rendered every 256px logo 175x256 inside a 175px
     square and cropped it. */
  height: auto;
  aspect-ratio: 1 / 1;
  border-radius: var(--r-md);
  object-fit: cover;
  background: var(--surface-2);
}

.mono {
  position: relative;
  display: grid;
  place-content: center;
  gap: 4px;
  background: linear-gradient(145deg, var(--g1, #2A2A45), var(--g2, #4E3E7A));
  color: #fff;
  text-align: center;
  overflow: hidden;
  user-select: none;
}

/* A soft highlight stops the flat gradient reading as a placeholder. */
.mono::after {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(120% 80% at 30% 0%, rgba(255, 255, 255, 0.14), transparent 60%);
  pointer-events: none;
}

.mono__letters {
  font-size: clamp(24px, 34cqw, 64px);
  font-weight: 700;
  letter-spacing: -0.03em;
  line-height: 1;
  text-transform: uppercase;
}

.mono__genre {
  font-size: clamp(9px, 5cqw, 12px);
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  opacity: 0.72;
}

.mono--hero .mono__letters { font-size: 64px; }
.mono--hero .mono__genre { font-size: 12px; }
.mono--mini .mono__letters { font-size: 18px; }
.mono--player .mono__letters { font-size: 15px; }

/* ====================================================================== */
/* Equaliser                                                              */
/* ====================================================================== */

.eq {
  display: none;
  align-items: flex-end;
  gap: 2px;
  height: 11px;
  flex-shrink: 0;
}
.eq i,
.card__eq i,
.row__eq i,
.np__eq i {
  display: block;
  width: 3px;
  height: 100%;
  border-radius: 1px;
  background: var(--accent);
  transform-origin: bottom;
  will-change: transform;
}

/*
  Five bars that never move in unison.

  We cannot read real levels: station streams are cross-origin, and adding
  `crossorigin` to the <audio> element makes most of them fail CORS and stop
  playing outright, so a Web Audio AnalyserNode is not available at any price.
  Do not add it.

  What sells the illusion is that each bar has its own *duration* as well as its
  own delay. Shared-duration bars are phase-shifted copies of one wave: they
  drift apart for a moment and then visibly snap back into step, which is what
  made the old three-bar version look broken. These five durations share no
  small common multiple, so the pattern does not repeat for minutes. Each bar
  also gets its own height range, so the group never flattens to one level.

  Negative delays start each bar part-way through its cycle, so nothing lines
  up even on the first frame.
*/
@keyframes eq-a { 0%, 100% { transform: scaleY(0.26); } 50% { transform: scaleY(0.92); } }
@keyframes eq-b { 0%, 100% { transform: scaleY(0.48); } 50% { transform: scaleY(1); } }
@keyframes eq-c { 0%, 100% { transform: scaleY(0.18); } 50% { transform: scaleY(0.68); } }
@keyframes eq-d { 0%, 100% { transform: scaleY(0.58); } 50% { transform: scaleY(0.86); } }
@keyframes eq-e { 0%, 100% { transform: scaleY(0.32); } 50% { transform: scaleY(0.99); } }

.eq i:nth-child(1), .card__eq i:nth-child(1),
.row__eq i:nth-child(1), .np__eq i:nth-child(1) {
  animation: eq-a 620ms ease-in-out infinite; animation-delay: -90ms;
}
.eq i:nth-child(2), .card__eq i:nth-child(2),
.row__eq i:nth-child(2), .np__eq i:nth-child(2) {
  animation: eq-b 1150ms ease-in-out infinite; animation-delay: -430ms;
}
.eq i:nth-child(3), .card__eq i:nth-child(3),
.row__eq i:nth-child(3), .np__eq i:nth-child(3) {
  animation: eq-c 830ms ease-in-out infinite; animation-delay: -260ms;
}
.eq i:nth-child(4), .card__eq i:nth-child(4),
.row__eq i:nth-child(4), .np__eq i:nth-child(4) {
  animation: eq-d 1370ms ease-in-out infinite; animation-delay: -700ms;
}
.eq i:nth-child(5), .card__eq i:nth-child(5),
.row__eq i:nth-child(5), .np__eq i:nth-child(5) {
  animation: eq-e 970ms ease-in-out infinite; animation-delay: -150ms;
}

/*
  Paused and buffering are different states and must not look the same.
  Paused freezes the bars where they stand - the shape stays, the motion stops.
  Buffering keeps them alive but drains the colour and moves them together on
  purpose, so "waiting" reads as clearly different from "playing".
*/
[data-state="paused"] .eq i, [data-state="paused"] .np__eq i,
[data-player-state="paused"] .card__eq i,
[data-player-state="paused"] .row__eq i,
.eq--paused i { animation-play-state: paused; }

[data-state="buffering"] .eq i, [data-state="buffering"] .np__eq i,
[data-player-state="buffering"] .card__eq i,
[data-player-state="buffering"] .row__eq i,
.eq--buffering i {
  animation: eq-wait 1100ms ease-in-out infinite;
  animation-delay: calc(var(--i, 0) * 120ms);
  background: var(--muted);
}
@keyframes eq-wait { 0%, 100% { transform: scaleY(0.3); opacity: 0.55; }
                     50%      { transform: scaleY(0.5); opacity: 1; } }

@media (prefers-reduced-motion: reduce) {
  .eq i, .card__eq i, .row__eq i, .np__eq i {
    animation: none !important;
    transform: scaleY(0.62);
  }
  /* Motion is off, so buffering has to say so without moving. */
  [data-state="buffering"] .eq i, .eq--buffering i {
    background: var(--muted);
    opacity: 0.7;
  }
}

/* ====================================================================== */
/* Cards                                                                  */
/* ====================================================================== */

.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: var(--s3) var(--s2);
}

@media (min-width: 600px) {
  .card-grid {
    grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
    gap: var(--s4) var(--s3);
  }
}

/*
  Rail: a horizontal scroller on mobile, six across on desktop. Scroll snapping
  is what makes the mobile version feel like an app rather than a web page.
*/
.rail {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(144px, 45%);
  gap: var(--s2);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  padding-bottom: var(--s1);
  margin-inline: calc(-1 * var(--s2));
  padding-inline: var(--s2);
  scrollbar-width: thin;
}
.rail > * { scroll-snap-align: start; }

@media (min-width: 700px) {
  .rail {
    grid-auto-flow: row;
    /* Fit the container rather than forcing six columns. The rail now also
       renders inside the station page's main column, which is ~810px wide, and
       six fixed columns there gave 120px cards - every name truncated to
       "Non-Stop O". auto-fill keeps cards at a readable width and simply wraps
       to a second row when there is not enough space for one. */
    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
    grid-auto-columns: auto;
    overflow: visible;
    margin-inline: 0;
    padding-inline: 0;
    gap: var(--s3);
  }
  .rail > *:nth-child(n + 13) { display: none; }
}

.card {
  position: relative;
  container-type: inline-size;
}

.card__art {
  position: relative;
  display: grid;
  place-items: center;
  aspect-ratio: 1 / 1;
  /* Breathing room and a hairline, so a logo with a transparent background
     does not bleed into the tile and look torn off at the edges. */
  padding: 6%;
  border-radius: var(--r-md);
  overflow: hidden;
  background: var(--surface-2);
  /* One declaration: the inner hairline and the drop shadow together. Split
     across two box-shadow lines the second silently wins and the hairline that
     keeps transparent logos from looking torn simply never renders. */
  box-shadow: inset 0 0 0 1px var(--border), 0 2px 10px rgba(0, 0, 0, 0.28);
  transition: transform var(--t-base), box-shadow var(--t-base);
}
.card:hover .card__art,
.card:focus-within .card__art {
  transform: translateY(-3px);
  /* Keeps the inner hairline through the hover transition. */
  box-shadow: inset 0 0 0 1px var(--border), 0 12px 28px rgba(0, 0, 0, 0.45);
}

.card__play {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  background: linear-gradient(to top, rgba(8, 8, 12, 0.72), rgba(8, 8, 12, 0.12));
  opacity: 0;
  transition: opacity var(--t-fast);
}
.card:hover .card__play,
.card:focus-within .card__play { opacity: 1; }

.card__play-btn {
  display: grid;
  place-items: center;
  width: 46px;
  height: 46px;
  padding-left: 3px;
  border-radius: var(--r-full);
  background: var(--accent);
  color: var(--accent-contrast);
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.4);
  transform: translateY(6px) scale(0.92);
  transition: transform var(--t-base);
}
.card:hover .card__play-btn,
.card:focus-within .card__play-btn { transform: translateY(0) scale(1); }

/* Now playing: a persistent accent ring plus a live equaliser badge. */
.card__eq {
  position: absolute;
  inset-inline-start: var(--s1);
  bottom: var(--s1);
  display: none;
  align-items: flex-end;
  gap: 2px;
  height: 14px;
  padding: 4px 6px;
  border-radius: var(--r-full);
  background: rgba(8, 8, 12, 0.7);
}
.card__eq i {
  display: block;
  width: 3px;
  height: 100%;
  border-radius: 1px;
  background: var(--accent);
  transform-origin: bottom;
  animation: eq-bounce 900ms ease-in-out infinite;
}
.card__eq i:nth-child(2) { animation-delay: 150ms; }
.card__eq i:nth-child(3) { animation-delay: 300ms; }

.card.is-playing .card__art {
  box-shadow: 0 0 0 2px var(--accent), 0 10px 26px rgba(0, 0, 0, 0.45);
}
.card.is-playing .card__eq { display: flex; }
.card.is-playing .card__play { opacity: 0; }

.card__body { padding-top: var(--s2); padding-inline-end: 34px; }

.card__name {
  font-size: var(--fs-sm);
  font-weight: 600;
  line-height: 1.35;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* 24px minimum target for the primary link on a card (WCAG 2.5.8). The type
   is unchanged; only the hit box grows. */
.card__name a { display: inline-block; padding-block: 3px; }

.card__meta {
  margin-top: 2px;
  font-size: var(--fs-sm);
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.chip {
  display: inline-flex;
  align-items: center;
  margin-top: var(--s1);
  padding: 3px 10px;
  border-radius: var(--r-full);
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--muted);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  white-space: nowrap;
}
.chip--link {
  color: var(--text);
  text-transform: none;
  letter-spacing: 0;
  font-size: var(--fs-sm);
  padding: 6px 12px;
  min-height: 34px;
}
.chip--link:hover { border-color: var(--accent-ink); color: var(--accent-ink); }

.chip-row { display: flex; flex-wrap: wrap; gap: var(--s1); margin-top: var(--s2); }
.chip-row--spaced { margin-top: var(--s3); }

.card__fav {
  position: absolute;
  inset-inline-end: -4px;
  bottom: 4px;
  display: grid;
  place-items: center;
  width: var(--tap);
  height: var(--tap);
  border-radius: var(--r-full);
  color: var(--muted);
  transition: color var(--t-fast), transform var(--t-fast);
}
.card__fav:hover { color: var(--text); transform: scale(1.08); }
.card__fav[aria-pressed="true"] { color: var(--accent-ink); }

/* ====================================================================== */
/* Skeletons                                                              */
/* ====================================================================== */

.skeleton {
  background: linear-gradient(
    90deg,
    var(--surface) 25%,
    var(--surface-3) 37%,
    var(--surface) 63%
  );
  background-size: 400% 100%;
  animation: shimmer 1.4s ease infinite;
  border-radius: var(--r-md);
}

.skeleton--line {
  height: 12px;
  margin-top: var(--s1);
  border-radius: var(--r-sm);
}
.skeleton--short { width: 60%; }

.card--skeleton .card__art { border-radius: var(--r-md); }

@keyframes shimmer {
  0% { background-position: 100% 50%; }
  100% { background-position: 0 50%; }
}

/* ====================================================================== */
/* Pills + country grid                                                   */
/* ====================================================================== */

.pill-grid, .pill-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s1);
}
.pill-row { margin-top: var(--s2); }

.pill {
  display: inline-flex;
  align-items: center;
  gap: var(--s1);
  min-height: var(--tap);
  padding-inline: var(--s2);
  border-radius: var(--r-full);
  background: var(--surface);
  border: 1px solid var(--border);
  color: var(--text);
  font-size: var(--fs-sm);
  font-weight: 500;
  transition: background var(--t-fast), border-color var(--t-fast), color var(--t-fast);
}
.pill:hover {
  color: var(--text);
  background: var(--surface-2);
  border-color: var(--surface-3);
}
.pill--sm { min-height: 32px; font-size: var(--fs-sm); }
.pill__count { color: var(--muted); font-size: 12px; }
.pill[aria-pressed="true"], .pill.is-active {
  background: var(--accent);
  border-color: var(--accent-ink);
  color: var(--accent-contrast);
}
.pill[aria-pressed="true"] .pill__count { color: inherit; opacity: 0.8; }

.country-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: var(--s1);
}

/*
  Geometry here is duplicated verbatim in critical.css and guarded by
  CSS_PARITY in build.py. The directory pages put well over a hundred of these
  above the fold, so anything that changes the box has to be right at first
  paint: when the height, the padding, the grid gap and the icon chip lived
  only in site.css, every row moved the moment the async stylesheet promoted
  and /genres/, /languages/ and /countries/ each measured CLS 0.119.

  Each property is therefore declared exactly once for this selector. A second
  declaration further down the file would not show up as a visual bug — it
  would just silently break the parity check's list comparison.
*/
.country {
  display: flex;
  align-items: center;
  gap: var(--s1);
  min-height: 56px;
  padding: 10px var(--s2);
  border-radius: var(--r-md);
  background: var(--surface);
  border: 1px solid var(--border);
  transition: background var(--t-fast), border-color var(--t-fast);
}
.country:hover { background: var(--surface-2); color: var(--text); }
.country__flag { font-size: var(--fs-lg); line-height: 1; }
.country__name {
  flex: 1;
  font-size: var(--fs-sm);
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.country__count { font-size: 12px; }

/* ====================================================================== */
/* Collection pages                                                       */
/* ====================================================================== */

.collection__intro {
  max-width: 70ch;
  margin-top: var(--s2);
  color: var(--text);
  line-height: 1.65;
}
.collection__count { margin-top: var(--s1); }

.filter-note { margin-top: calc(-1 * var(--s1)); }

.load-more {
  display: flex;
  justify-content: center;
  margin-top: var(--s3);
}

.pagination {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s2);
  margin-top: var(--s4);
  padding-top: var(--s3);
  border-top: 1px solid var(--border);
}
.pagination__status { text-align: center; }
.btn.is-disabled {
  opacity: 0.4;
  pointer-events: none;
}

.directory-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: var(--s2);
}

.station__explore { margin-top: var(--s3); }
.station__explore-title {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}
.station__explore .pill-row { margin-top: var(--s1); }

/* Needs to out-specify `.site-nav a`, which sets display: inline-flex. */
.site-nav a.site-nav__wide { display: none; }
.site-nav a.site-nav__mid { display: none; }
@media (min-width: 520px) { .site-nav a.site-nav__mid { display: inline-flex; } }
@media (min-width: 700px) {
  .site-nav a.site-nav__wide { display: inline-flex; }
}

/* ====================================================================== */
/* Forms                                                                  */
/* ====================================================================== */

.form { display: grid; gap: var(--s3); padding-block: var(--s2) var(--s5); }
.form__row { display: grid; gap: var(--s1); }
.form__row label { font-size: var(--fs-sm); font-weight: 600; }
.form input, .form select, .form textarea {
  width: 100%;
  min-height: var(--tap);
  padding: var(--s1) var(--s2);
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  transition: border-color var(--t-fast);
}
.form textarea { min-height: 140px; resize: vertical; }
.form input:focus, .form select:focus, .form textarea:focus {
  outline: none;
  border-color: var(--accent-ink);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
}
.form .btn { justify-self: start; }

/* Space held for content that arrives from localStorage; see critical.css. */
[data-favorites-section], [data-recent-page] { min-height: 320px; }

/* ====================================================================== */
/* Prose (legal, trust and editorial pages)                               */
/* ====================================================================== */

.prose {
  max-width: 68ch;
  padding-bottom: var(--s5);
  line-height: 1.7;
}
.prose > * + * { margin-top: var(--s2); }
.prose h2 {
  margin-top: var(--s5);
  font-size: var(--fs-xl);
}
.prose h3 {
  margin-top: var(--s4);
  font-size: var(--fs-lg);
}
.prose a {
  color: var(--accent-ink);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.prose ul, .prose ol {
  padding-inline-start: var(--s3);
  list-style: disc;
}
.prose ol { list-style: decimal; }
.prose li + li { margin-top: var(--s1); }
.prose li::marker { color: var(--muted); }
.prose strong { color: var(--text); }
.prose code {
  padding: 2px 6px;
  border-radius: var(--r-sm);
  background: var(--surface-2);
  font-size: 0.9em;
}
.prose blockquote {
  margin-inline: 0;
  padding: var(--s2);
  border-inline-start: 3px solid var(--accent);
  background: var(--surface);
  border-start-end-radius: var(--r-md);
  border-end-end-radius: var(--r-md);
  color: var(--muted);
}
.prose blockquote > * + * { margin-top: var(--s1); }
.prose hr { border: 0; border-top: 1px solid var(--border); margin-block: var(--s4); }

.draft-badge {
  display: inline-block;
  padding: 4px var(--s1);
  border-radius: var(--r-full);
  background: color-mix(in srgb, var(--accent) 18%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--accent) 40%, var(--border));
  color: var(--accent-ink);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.draft-badge--inline { margin-inline-start: var(--s1); vertical-align: middle; }

.guide-list { display: grid; gap: var(--s3); padding-bottom: var(--s5); }
.guide-list__item {
  padding-bottom: var(--s3);
  border-bottom: 1px solid var(--border);
}
.guide-list__title { font-size: var(--fs-lg); margin-bottom: 4px; }
.guide__footer { margin-top: var(--s4); }
.guide__footer a { color: var(--accent-ink); text-decoration: underline; }

/* ====================================================================== */
/* Form status and honeypot                                               */
/* ====================================================================== */

.form-status {
  padding: var(--s2);
  border-radius: var(--r-md);
  font-size: var(--fs-sm);
  margin-bottom: var(--s2);
}
.form-status[hidden] { display: none; }
.form-status--ok {
  background: color-mix(in srgb, #2ecc71 16%, var(--surface));
  border: 1px solid color-mix(in srgb, #2ecc71 40%, var(--border));
}
.form-status--error {
  background: color-mix(in srgb, #ff3b30 14%, var(--surface));
  border: 1px solid color-mix(in srgb, #ff3b30 40%, var(--border));
}

/* Bait for bots: off-screen, untabbable, hidden from assistive tech.
   Logical offset so it does not widen a right-to-left page. */
.honeypot {
  position: absolute;
  inset-inline-start: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.form__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s1);
  align-items: center;
}

.site-footer__notice {
  color: var(--text);
  font-weight: 500;
}
.site-footer__links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s1) var(--s2);
  margin-top: var(--s2);
  margin-bottom: var(--s2);
}
.site-footer__links a { text-decoration: none; color: var(--muted); }
.site-footer__links a:hover { color: var(--accent-ink); }

/* ====================================================================== */
/* ====================================================================== */
/* Player bar                                                             */
/* ====================================================================== */

.player {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 30;
  background: color-mix(in srgb, var(--surface) 97%, transparent);
  backdrop-filter: blur(18px);
  border-top: 1px solid var(--border);
  box-shadow: 0 -6px 28px rgba(0, 0, 0, 0.35);
  padding-bottom: env(safe-area-inset-bottom, 0);
}

.player__inner {
  display: flex;
  align-items: center;
  gap: var(--s2);
  min-height: var(--player-h);
}

.player__station {
  display: flex;
  align-items: center;
  gap: var(--s2);
  min-width: 0;
  flex: 1;
}
.player__station:hover { color: var(--text); }
.player__station:hover .player__name { color: var(--accent-ink); }

.player__art {
  container-type: inline-size;
  display: grid;
  place-items: center;
  flex-shrink: 0;
  width: 48px;
  height: 48px;
  background: var(--surface-2);
  border-radius: var(--r-sm);
  overflow: hidden;
  background: var(--surface-2);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
}
.player__art .art__img,
.player__art .mono { border-radius: var(--r-sm); }

.player__meta { min-width: 0; display: grid; gap: 2px; }
.player__name {
  font-size: var(--fs-sm);
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  transition: color var(--t-fast);
}

.player__sub {
  display: flex;
  align-items: center;
  gap: var(--s1);
  min-width: 0;
}
.player__sub-text {
  font-size: 12px;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The equaliser only runs while audio is actually playing. */
.player[data-state="playing"] [data-player-eq] { display: flex; }

.player__controls {
  display: flex;
  align-items: center;
  gap: var(--s1);
  flex-shrink: 0;
}

.player__play {
  position: relative;
  display: grid;
  place-items: center;
  width: 50px;
  height: 50px;
  border-radius: var(--r-full);
  background: var(--accent);
  color: var(--accent-contrast);
  transition: background var(--t-fast), transform var(--t-fast);
}
.player__play:hover { background: var(--accent-hover); }
.player__play:active { transform: scale(0.96); }

.player__icon { display: none; }
.player__icon--play { display: grid; padding-left: 3px; }
.player[data-state="playing"] .player__icon--play { display: none; }
.player[data-state="playing"] .player__icon--pause { display: grid; }

/* Loading and buffering read differently from playing: a spinner, and the
   button dims so it is obvious nothing is coming out of the speakers yet. */
.player[data-state="loading"] .player__icon,
.player[data-state="buffering"] .player__icon { display: none; }
.player[data-state="loading"] .player__spinner,
.player[data-state="buffering"] .player__spinner { display: block; }
.player[data-state="loading"] .player__play { background: color-mix(in srgb, var(--accent) 65%, var(--surface-3)); }
.player[data-state="buffering"] .player__play { background: color-mix(in srgb, var(--accent) 78%, var(--surface-3)); }

/* Error is the loudest state: the button goes red and the note appears. */
.player[data-state="error"] .player__play {
  background: #C62B22;
  color: #fff;
}

.player__spinner {
  display: none;
  width: 20px; height: 20px;
  border: 2px solid color-mix(in srgb, var(--accent-contrast) 35%, transparent);
  border-top-color: var(--accent-contrast);
  border-radius: var(--r-full);
  animation: spin 700ms linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

.player__volume { display: none; }
@media (min-width: 700px) {
  .player__volume { display: flex; align-items: center; gap: var(--s1); color: var(--muted); }
  .player__volume input[type="range"] { width: 92px; accent-color: var(--accent-ink); cursor: pointer; }
}

.player__menu-wrap { position: relative; }
.player__shortcuts { display: none; }
@media (min-width: 900px) { .player__shortcuts { display: block; } }

.player__sleep-remaining {
  position: absolute;
  bottom: 2px; inset-inline-end: 2px;
  padding: 0 4px;
  border-radius: var(--r-full);
  background: var(--surface);
  color: var(--accent-ink);
  font-size: 10px;
  font-weight: 700;
}

.player__note {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s1);
  padding: var(--s1) var(--s2);
  background: color-mix(in srgb, #ff3b30 20%, var(--surface));
  border-bottom: 1px solid color-mix(in srgb, #ff3b30 38%, var(--border));
  font-size: var(--fs-sm);
  text-align: center;
}
.player__note[hidden] { display: none; }

/* ---------------------------------------------------------------- menus */

.menu {
  position: absolute;
  bottom: calc(100% + var(--s1));
  inset-inline-end: 0;
  min-width: 200px;
  padding: var(--s1);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  box-shadow: var(--shadow);
  display: grid;
  gap: 2px;
  z-index: 40;
}
.menu[hidden] { display: none; }
.menu__title {
  padding: var(--s1) var(--s2) 4px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
}
.menu button:not(.mini__play) {
  display: block;
  width: 100%;
  min-height: var(--tap);
  padding-inline: var(--s2);
  text-align: start;
  border-radius: var(--r-sm);
  font-size: var(--fs-sm);
  color: var(--text);
  transition: background var(--t-fast);
}
.menu button:not(.mini__play):hover { background: var(--surface-3); }
.menu button[aria-checked="true"] { color: var(--accent-ink); font-weight: 600; }

.menu--next { min-width: 280px; }
.menu--next .mini { grid-template-columns: 40px minmax(0, 1fr) var(--tap); }
.menu--next .mini__art { width: 40px; height: 40px; }

.menu--keys { min-width: 260px; }
.keys { display: grid; gap: var(--s1); padding: var(--s1) var(--s2) var(--s2); }
.keys > div { display: grid; grid-template-columns: 92px 1fr; gap: var(--s2); align-items: center; }
.keys dd { margin: 0; font-size: var(--fs-sm); color: var(--muted); }
kbd {
  display: inline-block;
  padding: 2px 6px;
  border-radius: 4px;
  background: var(--surface-3);
  border: 1px solid var(--border);
  font-family: inherit;
  font-size: 11px;
  font-weight: 600;
}
/* ====================================================================== */
/* Language switcher, prompt, and right-to-left                           */
/* ====================================================================== */

.lang { position: relative; flex-shrink: 0; }
.lang [data-lang-toggle] {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  width: auto;
  padding-inline: 6px;
}
.lang__current {
  display: none;
  max-width: 12ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--fs-sm);
  font-weight: 500;
}
@media (min-width: 1100px) { .lang__current { display: inline; } }

/* 24 languages is a long list: it scrolls rather than running off screen. */
.menu--lang {
  top: calc(100% + var(--s1));
  bottom: auto;
  inset-inline-end: 0;
  min-width: 200px;
  max-height: min(60vh, 420px);
  overflow-y: auto;
  overscroll-behavior: contain;
}
.menu--lang button[aria-current="true"] { color: var(--accent-ink); font-weight: 600; }
/* In the footer there is nothing below, so it opens upward. */
.menu--up { top: auto; bottom: calc(100% + var(--s1)); }

.lang__footer {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: var(--tap);
  color: var(--muted);
  font-size: var(--fs-sm);
  transition: color var(--t-fast);
}
.lang__footer:hover { color: var(--text); }

/*
  The one-time prompt. It sits above the player, matching the install banner,
  and is written in the language being offered — never switching on its own.
*/
/*
  Stacked above the install banner rather than over it: both are fixed to the
  same slot, and asking two questions at once is worse than asking one. pwa.js
  holds the install prompt back until a second visit, by which time this one
  has been answered and removed.
*/
.lang-banner {
  position: fixed;
  inset-inline: 0;
  bottom: calc(var(--player-h) + env(safe-area-inset-bottom, 0px));
  z-index: 26;
  background: var(--surface-2);
  border-top: 1px solid var(--border);
}
.lang-banner__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--s2);
  padding-block: var(--s2);
  font-size: var(--fs-md);
}
.lang-banner__actions { display: flex; gap: var(--s1); flex-shrink: 0; }
/* 44px like every other control — the design system has no exception for
   a banner the visitor is meant to answer with a thumb. */
.btn--sm { min-height: var(--tap); padding-inline: var(--s2); font-size: var(--fs-sm); }

/* Editorial prose is still English; the note says so in the reader's own
   language rather than leaving them guessing. */
.english-note {
  margin-top: var(--s2);
  padding: var(--s1) var(--s2);
  border-inline-start: 3px solid var(--border);
  color: var(--muted);
  font-size: var(--fs-sm);
}

/* ------------------------------------------------------------ right to left
   Arabic and Urdu. Most of the layout is already logical (padding-inline,
   flex, grid), so what remains is the handful of physical offsets and the
   two arrows that have to point the other way. Media transport controls are
   deliberately NOT mirrored: play always points the way audio runs.
*/
/*
  Almost nothing is needed here. The layout is written with logical properties
  — inset-inline-start, padding-inline-end, text-align: start — so it mirrors
  itself, which also means the RTL layout is correct at first paint instead of
  waiting for this stylesheet to load. What is left is the arrows, which are
  glyphs rather than layout: "previous" points right in a right-to-left page.
*/
[dir="rtl"] .pagination__arrow { display: inline-block; transform: scaleX(-1); }

/* View transition on in-site navigation                                  */
/* ====================================================================== */

.site-main { transition: opacity var(--t-fast); }
.site-main.is-loading { opacity: 0.55; }

/* ====================================================================== */
/* Homepage: chip filter, now playing, side panels                        */
/* ====================================================================== */

/*
  The chip row and the two-column shell also live in critical.css. Both files
  must carry any rule that affects above-the-fold layout or text metrics, or
  the async stylesheet reflows the page when it promotes.
*/
.chips { display: flex; flex-wrap: wrap; gap: var(--s1); margin-top: var(--s3); }

/* ====================================================================== */
/* Chip rails - the three self-scrolling lines under the search field     */
/* ====================================================================== */

/*
  Each rail is one line that never wraps and drifts slowly sideways, stopping
  whenever the pointer is over it or anything inside it has focus. The chips
  used to wrap onto three ragged rows, which read as a wall of buttons.

  The loop is seamless because ui.js appends one clone of the chips to the
  track and the animation travels exactly half the track's width. The clones
  are aria-hidden and untabbable, so assistive tech and the keyboard see each
  chip once, while a pointer can click either copy - home.js matches chips by
  their data-chip key, not by element identity, so both copies stay in sync.

  With JavaScript off there are no clones and no animation: the track simply
  scrolls, which is what the overflow-x below is for.
*/
.chiprail{
  position:relative;
  margin-top:var(--s3);
  overflow-x:auto;
  overflow-y:hidden;
  scrollbar-width:none;
}
/* The three rails are one block, not three separate rows: only the first is
   spaced off the search field, and the rest sit close under it. */
.chiprail + .chiprail{margin-top:4px}

/*
  The edge fade belongs only to a rail that is actually moving.

  It used to be on every .chiprail unconditionally, and the first and last
  chip of a *static* row came out half-transparent for no reason — a row that
  is not going anywhere has no reason to suggest that it is. A rail that
  stopped because the visitor asked for reduced motion, or because its chips
  already fit, now simply ends.

  20px, not 40: the fade is there so a clipped chip does not look chopped off,
  and anything wider than the pill's own corner starts bleaching whole words.
*/
.chiprail[data-rail-active]{
  -webkit-mask-image:linear-gradient(to right,transparent,#000 20px,
    #000 calc(100% - 20px),transparent);
  mask-image:linear-gradient(to right,transparent,#000 20px,
    #000 calc(100% - 20px),transparent);
}
.chiprail::-webkit-scrollbar{display:none}
.chiprail__track{
  display:flex;
  flex-wrap:nowrap;
  gap:var(--s1);
  width:max-content;
  padding-inline:2px;
  /* Just enough for the chips' drop shadows to breathe. This used to be --s1
     on top of the rail's own margin, which put 44px between three rows that
     read as one control. */
  padding-block:5px;
}
/*
  Only a rail ui.js has cloned animates. ui.js builds the track as exactly two
  identical halves, so travelling 50% of it lands the second half precisely
  where the first began and the loop is seamless.

  The distance is -50% and not a custom property, and that matters: a
  transform whose value contains var() cannot be handed to the compositor,
  because the variable has to be resolved during style recalculation on every
  frame. Three rails animating that way spent 1,677ms in Style & Layout across
  a Lighthouse trace and cost the homepage 24 performance points. A literal
  percentage is a plain compositor animation and costs nothing per frame.

  Duration still comes from a variable — that is read once when the animation
  starts, not per frame.
*/
.chiprail__track[data-rail-ready]{
  animation:chiprail var(--rail-time,60s) linear infinite;
  will-change:transform;
}
/* The rail's own box never changes size, so the browser need not consider its
   contents when laying out anything else. */
.chiprail{contain:layout paint}
@keyframes chiprail{
  from{transform:translate3d(0,0,0)}
  to{transform:translate3d(-50%,0,0)}
}
/* Stop for anyone looking at it. :focus-within covers the keyboard. */
.chiprail:hover .chiprail__track,
.chiprail:focus-within .chiprail__track{animation-play-state:paused}

/* A rail that fits on screen has nothing to scroll; ui.js leaves it alone. */
@media (prefers-reduced-motion:reduce){
  .chiprail__track[data-rail-ready]{animation:none}
}


.chip-btn {
  display: inline-flex;
  align-items: center;
  min-height: 36px;
  padding-inline: 14px;
  font: inherit;
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--muted);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-full);
  cursor: pointer;
  white-space: nowrap;
  transition: color var(--t-fast), background var(--t-fast), border-color var(--t-fast);
}
/*
  Chips, dressed.

  A flat white pill with a grey hairline on a grey page has no depth and no
  identity — thirty of them read as one undifferentiated mass. Three things
  fix that, and all three are paint, so none of it can move anything when the
  async stylesheet lands:

  1. A surface with a top-down gradient rather than a flat fill, so the pill
     catches light the way a physical control would.
  2. A two-part shadow: a tight, nearly-black hairline that defines the edge,
     and a wide soft one that lifts it off the page. One shadow alone gives
     you either a hard outline or a vague smudge.
  3. The chip's own hue, from --h — the same number the country tiles, genre
     cards and language tiles already use, so a chip and the tile it leads to
     are recognisably the same thing. Held at a very low saturation: this is a
     tint, not a paint job, and the text has to stay the most legible thing
     on it.
*/
.chip-btn {
  position: relative;
  font-weight: 600;
  letter-spacing: -0.005em;
  color: var(--text);
  background: linear-gradient(180deg, #FFFFFF, #F7F8FC);
  border-color: #E2E5EF;
  box-shadow:
    0 1px 1px rgba(16, 16, 28, 0.04),
    0 2px 6px rgba(16, 16, 28, 0.05);
  transition:
    transform var(--t-fast), box-shadow var(--t-base),
    border-color var(--t-fast), background var(--t-fast);
}
.chip-btn--hued {
  background: linear-gradient(180deg,
    hsl(var(--h, 240) 60% 99%),
    hsl(var(--h, 240) 54% 94%));
  border-color: hsl(var(--h, 240) 40% 84%);
}
.chip-btn:hover {
  color: var(--text);
  transform: translateY(-1px);
  border-color: hsl(var(--h, 240) 48% 76%);
  box-shadow:
    0 1px 1px rgba(16, 16, 28, 0.05),
    0 6px 16px hsl(var(--h, 240) 40% 60% / 0.22);
}
.chip-btn:active { transform: translateY(0); }
.chip-btn:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/*
  The selected chip. An inset highlight along the top edge is what stops a
  saturated fill from looking like a flat sticker — it reads as a lit surface
  instead. The glow underneath is the accent itself at low alpha, so the chip
  appears to sit above the page rather than be printed on it.
*/
.chip-btn[aria-pressed="true"] {
  color: var(--accent-contrast);
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--accent) 88%, #fff),
    var(--accent) 55%,
    color-mix(in srgb, var(--accent) 86%, #000));
  border-color: color-mix(in srgb, var(--accent) 72%, #000);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.42),
    0 1px 2px rgba(16, 16, 28, 0.14),
    0 6px 18px color-mix(in srgb, var(--accent) 42%, transparent);
}
.chip-btn[aria-pressed="true"]:hover {
  transform: translateY(-1px);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.42),
    0 2px 4px rgba(16, 16, 28, 0.16),
    0 10px 24px color-mix(in srgb, var(--accent) 50%, transparent);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .chip-btn {
    color: var(--text);
    background: linear-gradient(180deg, #1E1E28, #16161E);
    border-color: #30303E;
    box-shadow:
      inset 0 1px 0 rgba(255, 255, 255, 0.05),
      0 2px 6px rgba(0, 0, 0, 0.4);
  }
  :root:not([data-theme="light"]) .chip-btn--hued {
    background: linear-gradient(180deg,
      hsl(var(--h, 240) 22% 16%), hsl(var(--h, 240) 24% 11%));
    border-color: hsl(var(--h, 240) 24% 28%);
  }
  :root:not([data-theme="light"]) .chip-btn:hover {
    border-color: hsl(var(--h, 240) 42% 46%);
    box-shadow:
      inset 0 1px 0 rgba(255, 255, 255, 0.07),
      0 8px 20px rgba(0, 0, 0, 0.55);
  }
}
:root[data-theme="dark"] .chip-btn {
  color: var(--text);
  background: linear-gradient(180deg, #1E1E28, #16161E);
  border-color: #30303E;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.05),
    0 2px 6px rgba(0, 0, 0, 0.4);
}
:root[data-theme="dark"] .chip-btn--hued {
  background: linear-gradient(180deg,
    hsl(var(--h, 240) 22% 16%), hsl(var(--h, 240) 24% 11%));
  border-color: hsl(var(--h, 240) 24% 28%);
}
:root[data-theme="dark"] .chip-btn:hover {
  border-color: hsl(var(--h, 240) 42% 46%);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.07),
    0 8px 20px rgba(0, 0, 0, 0.55);
}

/*
  The selected chip, restated at the themed selectors' weight.

  `:root[data-theme="dark"] .chip-btn` is (0,3,0) and
  `.chip-btn[aria-pressed="true"]` is only (0,2,0), so the dark theme's plain
  chip surface was painting straight over the selection — Top Global came out
  the same grey as every other chip in dark mode while being orange in light.
  Selection has to outrank the theme, not the other way round.
*/
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .chip-btn[aria-pressed="true"] {
  color: var(--accent-contrast);
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--accent) 88%, #fff),
    var(--accent) 55%,
    color-mix(in srgb, var(--accent) 86%, #000));
  border-color: color-mix(in srgb, var(--accent) 72%, #000);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.42),
    0 1px 2px rgba(16, 16, 28, 0.14),
    0 6px 18px color-mix(in srgb, var(--accent) 42%, transparent);
  }
}
:root[data-theme="dark"] .chip-btn[aria-pressed="true"] {
  color: var(--accent-contrast);
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--accent) 88%, #fff),
    var(--accent) 55%,
    color-mix(in srgb, var(--accent) 86%, #000));
  border-color: color-mix(in srgb, var(--accent) 72%, #000);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.42),
    0 1px 2px rgba(16, 16, 28, 0.14),
    0 6px 18px color-mix(in srgb, var(--accent) 42%, transparent);
}

@media (prefers-reduced-motion: reduce) {
  .chip-btn:hover, .chip-btn[aria-pressed="true"]:hover { transform: none; }
}

/*
  The rails need room for the shadows. Without it the soft one is clipped flat
  along the top and bottom of the track and the chips look pasted on.
*/
.chiprail { padding-block: 1px; }

.home { display: grid; gap: var(--s4); align-items: start; padding-block: var(--s4); }
@media (min-width: 1000px) { .home { grid-template-columns: minmax(0, 1fr) 320px; } }
.home__main { min-width: 0; }
.home__side { min-width: 0; }
@media (min-width: 1000px) {
  .home__sticky { position: sticky; top: var(--s3); display: grid; gap: var(--s2); }
}

/* Mobile: the side panels become tabs above the list. */
.home__tabs { display: flex; gap: var(--s1); overflow-x: auto; margin-bottom: var(--s3); }
@media (min-width: 1000px) { .home__tabs { display: none; } }
.home__tab {
  min-height: var(--tap);
  padding-inline: var(--s2);
  font: inherit;
  font-size: var(--fs-sm);
  color: var(--muted);
  background: none;
  border: 0;
  border-bottom: 2px solid transparent;
  cursor: pointer;
  white-space: nowrap;
}
.home__tab.is-active { color: var(--text); border-bottom-color: var(--accent-ink); font-weight: 600; }

@media (max-width: 999px) {
  .home__side .panel { display: none; }
  .home__side .panel.is-shown-mobile { display: block; }
  .is-hidden-mobile { display: none; }
}
@media (min-width: 1000px) { .is-hidden-mobile { display: block; } }

/* ---------------------------------------------------------- now playing */

.np {
  position: relative;
  overflow: hidden;
  padding: var(--s3);
  margin-bottom: var(--s4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
}
/*
  A soft accent wash, not an accent fill. The accent is used sparingly by
  design, so it arrives here as a low-alpha radial gradient behind the content
  rather than as a background colour.
*/
.np__glow {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(60% 90% at 85% 10%, color-mix(in srgb, var(--accent) 18%, transparent), transparent 70%),
    radial-gradient(50% 80% at 5% 95%, color-mix(in srgb, var(--accent) 10%, transparent), transparent 70%);
  pointer-events: none;
}
.np[data-state="playing"] { border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); }

.np__inner {
  position: relative;
  display: grid;
  grid-template-columns: 88px minmax(0, 1fr);
  gap: var(--s2);
  align-items: center;
}
@media (min-width: 700px) { .np__inner { grid-template-columns: 88px minmax(0, 1fr) auto; } }

.np__art {
  display: grid;
  place-items: center;
  width: 88px;
  height: 88px;
  overflow: hidden;
  color: var(--muted);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
}
.np__art img, .np__art .mono { width: 100%; height: 100%; }

.np__meta { min-width: 0; }
.np__name {
  margin: 0;
  font-size: var(--fs-xl);
  font-weight: 600;
  line-height: var(--lh-tight);
  overflow-wrap: anywhere;
}
.np__sub { margin: 4px 0 0; color: var(--muted); font-size: var(--fs-sm); }
.np__tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: var(--s1); }

/*
  The now-playing visualiser.

  This used to be fourteen bars all running one 1.1s animation. The per-bar
  `animation-delay` rules below it never applied: `animation:` is a shorthand
  and resets delay to 0, and the rule carrying it,
  `.np[data-state="playing"] .np__eq i`, outranks `.np__eq i:nth-child(2n)` —
  (0,3,1) against (0,2,1). So every bar rose and fell in perfect unison and the
  whole thing read as one solid block pulsing. It looked like nothing was
  playing, which is exactly what it was reported as.

  The fix is to keep the per-bar numbers in custom properties. A shorthand does
  not reset those, so low-specificity nth-child rules can set them freely and
  the one animation rule reads them. Delay is also its own declaration inside
  that rule rather than part of the shorthand.

  Every bar gets its own duration, its own phase and its own height range, so
  no two are ever in step — the brief's "never in unison". The centre bars
  reach higher than the edges, which is the envelope a real spectrum has and
  is what makes it read as sound rather than as a row of blinking lights.
*/
.np__eq {
  display: none;
  gap: 3px;
  align-items: flex-end;
  height: 34px;
  padding-inline: 2px;
}
@media (min-width: 700px) { .np__eq { display: flex; } }
.np__eq i {
  display: block;
  width: 3px;
  height: var(--lo, 6px);
  border-radius: 2px;
  /* A gradient, so a tall bar is brighter at the tip than at the base. The
     bar is only 3px wide; the glow is what gives it presence. */
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--accent) 55%, #fff),
    var(--accent) 55%,
    color-mix(in srgb, var(--accent) 80%, #000));
  box-shadow: 0 0 6px color-mix(in srgb, var(--accent) 45%, transparent);
}
/* Per-bar character. Durations are deliberately not multiples of one another
   and the phases are negative, so the visualiser is already mid-motion on the
   first frame instead of every bar starting from the floor together. */
.np__eq i:nth-child(1)  { --d: 1.02s; --t: -0.31s; --lo: 5px;  --hi: 17px; }
.np__eq i:nth-child(2)  { --d: 0.74s; --t: -0.08s; --lo: 6px;  --hi: 23px; }
.np__eq i:nth-child(3)  { --d: 1.27s; --t: -0.55s; --lo: 4px;  --hi: 28px; }
.np__eq i:nth-child(4)  { --d: 0.63s; --t: -0.22s; --lo: 7px;  --hi: 31px; }
.np__eq i:nth-child(5)  { --d: 1.11s; --t: -0.67s; --lo: 5px;  --hi: 26px; }
.np__eq i:nth-child(6)  { --d: 0.86s; --t: -0.14s; --lo: 6px;  --hi: 33px; }
.np__eq i:nth-child(7)  { --d: 1.34s; --t: -0.42s; --lo: 8px;  --hi: 30px; }
.np__eq i:nth-child(8)  { --d: 0.69s; --t: -0.59s; --lo: 5px;  --hi: 34px; }
.np__eq i:nth-child(9)  { --d: 1.18s; --t: -0.05s; --lo: 7px;  --hi: 29px; }
.np__eq i:nth-child(10) { --d: 0.79s; --t: -0.37s; --lo: 4px;  --hi: 32px; }
.np__eq i:nth-child(11) { --d: 1.41s; --t: -0.71s; --lo: 6px;  --hi: 25px; }
.np__eq i:nth-child(12) { --d: 0.92s; --t: -0.18s; --lo: 5px;  --hi: 27px; }
.np__eq i:nth-child(13) { --d: 1.07s; --t: -0.48s; --lo: 6px;  --hi: 20px; }
.np__eq i:nth-child(14) { --d: 0.71s; --t: -0.26s; --lo: 4px;  --hi: 15px; }

.np[data-state="playing"] .np__eq i {
  animation-name: npEq;
  animation-duration: var(--d, 1s);
  animation-timing-function: cubic-bezier(.4, 0, .3, 1);
  animation-iteration-count: infinite;
  animation-direction: alternate;
  animation-delay: var(--t, 0s);
}
/* `alternate` above means this only needs the two ends, and the bar spends
   its time easing between them rather than snapping back at 100%. */
@keyframes npEq {
  from { height: var(--lo, 6px); }
  to   { height: var(--hi, 30px); }
}

@media (prefers-reduced-motion: reduce) {
  .np[data-state="playing"] .np__eq i {
    animation: none;
    height: calc((var(--lo, 6px) + var(--hi, 30px)) / 2);
  }
}

.np__controls {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s1);
  margin-top: var(--s3);
}
.np__play {
  display: grid;
  place-items: center;
  width: 56px;
  height: 56px;
  color: var(--accent-contrast);
  background: var(--accent);
  border: 0;
  border-radius: 50%;
  cursor: pointer;
}
.np__play:disabled { opacity: .45; cursor: not-allowed; }
.np__play:hover:not(:disabled) { background: var(--accent-hover); }
.np__icon { grid-area: 1 / 1; }
.np__icon--pause, .np__spinner { display: none; }
.np[data-state="playing"] .np__icon--play { display: none; }
.np[data-state="playing"] .np__icon--pause { display: block; }
.np[data-state="loading"] .np__icon--play,
.np[data-state="buffering"] .np__icon--play { visibility: hidden; }
.np[data-state="loading"] .np__spinner,
.np[data-state="buffering"] .np__spinner {
  display: block;
  grid-area: 1 / 1;
  width: 22px;
  height: 22px;
  border: 2px solid color-mix(in srgb, var(--accent-contrast) 35%, transparent);
  border-top-color: var(--accent-contrast);
  border-radius: 50%;
  animation: npSpin .8s linear infinite;
}
@keyframes npSpin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .np__spinner { animation-duration: 2.4s; } }

.np__volume {
  display: flex;
  align-items: center;
  gap: var(--s1);
  min-width: 140px;
  padding-inline: var(--s2);
  color: var(--muted);
}
.np__volume input { flex: 1; min-width: 80px; accent-color: var(--accent-ink); }

/* --------------------------------------------------------- list heading */

.section__head--list { align-items: flex-end; gap: var(--s2); flex-wrap: wrap; }
.section__count { margin: 4px 0 0; color: var(--muted); font-size: var(--fs-sm); }
.section__more { display: flex; justify-content: center; margin-top: var(--s3); }
.section__empty { padding: var(--s4) 0; color: var(--muted); text-align: center; }

.sort__select {
  min-height: var(--tap);
  padding-inline: var(--s2);
  font: inherit;
  font-size: var(--fs-sm);
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  cursor: pointer;
}

/* ------------------------------------------------------------- panels */

.panel {
  padding: var(--s2);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
}
.panel + .panel { margin-top: var(--s2); }

/*
  The sidebar, given a palette.

  Four panels that were four identical grey boxes now each carry their own
  hue: the icon chip, the title, the leading edge and a wash at the top of the
  card all derive from one --h, exactly like the genre and language tiles and
  the country tiles. One number per panel, and the rest is arithmetic, so both
  themes come out of the same declaration.

  This is all paint, no layout — the chip's 28px box is in critical.css, where
  it has to be. Nothing here can move anything when the async sheet lands.
*/
.home__side .panel,
.station-side .panel { --h: 240; position: relative; overflow: hidden; }
.home__side [data-panel="favorites"] { --h: 42; }   /* the star: warm gold */
.home__side [data-panel="recent"]    { --h: 268; }  /* the disc: violet */
.home__side [data-panel="sleep"]     { --h: 212; }  /* the moon: night blue */
.home__side [data-panel="browse"]    { --h: 168; }  /* the globe: teal */

.home__side .panel::before {
  content: "";
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  height: 3px;
  background: linear-gradient(90deg,
    hsl(var(--h) 70% 55%), hsl(calc(var(--h) + 26) 70% 62%));
}
.home__side .panel::after {
  content: "";
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  height: 96px;
  pointer-events: none;
  background: linear-gradient(180deg,
    hsl(var(--h) 62% 96%), transparent);
}
.home__side .panel > * { position: relative; }
.panel__icon {
  background: hsl(var(--h) 62% 90%);
  color: hsl(var(--h) 68% 30%);
}
.home__side .panel__title { color: hsl(var(--h) 38% 32%); }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .home__side .panel::after {
    background: linear-gradient(180deg,
      hsl(var(--h) 38% 18%), transparent);
  }
  :root:not([data-theme="light"]) .panel__icon {
    background: hsl(var(--h) 34% 24%);
    color: hsl(var(--h) 78% 74%);
  }
  :root:not([data-theme="light"]) .home__side .panel__title {
    color: hsl(var(--h) 32% 72%);
  }
}
:root[data-theme="dark"] .home__side .panel::after {
  background: linear-gradient(180deg, hsl(var(--h) 38% 18%), transparent);
}
:root[data-theme="dark"] .panel__icon {
  background: hsl(var(--h) 34% 24%);
  color: hsl(var(--h) 78% 74%);
}
:root[data-theme="dark"] .home__side .panel__title {
  color: hsl(var(--h) 32% 72%);
}
/* The icon chip and the title are one group on the leading side; the "All"
   button, when there is one, is pushed to the far end. It used to be
   justify-content:space-between, which was right while the head held exactly
   a title and a button — adding the icon made that three children spread
   across the row, and the panels with no button had their title shoved to the
   trailing edge. */
.panel__head { display: flex; align-items: center; justify-content: flex-start; gap: var(--s1); }
.panel__head > .link-btn { margin-inline-start: auto; }
/* Geometry mirrored from critical.css and guarded by CSS_PARITY. */
.panel__icon{display:grid;place-items:center;flex:none;width:28px;height:28px;
  border-radius:var(--r-sm);margin-bottom:var(--s2)}
.panel__title { margin: 0; font-size: var(--fs-md); font-weight: 600; }
.panel__body { margin-top: var(--s1); }
.panel__empty { margin: var(--s1) 0 0; color: var(--muted); font-size: var(--fs-sm); }
.panel__note { margin: var(--s1) 0 0; color: var(--muted); font-size: var(--fs-sm); text-align: center; }
.panel__links { margin: var(--s1) 0 0; padding: 0; list-style: none; display: grid; gap: 2px; }
.panel__links a {
  display: block;
  padding: var(--s1) 0;
  min-height: var(--tap);
  color: var(--muted);
  font-size: var(--fs-sm);
  text-decoration: none;
}
.panel__links a:hover { color: var(--text); }

.panel__sleep { display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px; margin-top: var(--s1); }
.sleep-btn {
  min-height: var(--tap);
  font: inherit;
  font-size: var(--fs-sm);
  color: var(--muted);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  cursor: pointer;
}
.sleep-btn:hover { color: var(--text); }
.sleep-btn.is-active { color: var(--accent-contrast); background: var(--accent); border-color: var(--accent-ink); }

/* Bitrate sits after the place, muted and secondary — it is a quality hint,
   not a headline. Hidden on the narrowest cards where the place already
   fills the line. */
.card__rate {
  margin-inline-start: var(--s1);
  padding-inline-start: var(--s1);
  border-inline-start: 1px solid var(--border);
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* Country flags - see critical.css, which carries the same box so the async
   stylesheet cannot reflow rows that already reserved space for one. */
.flag{display:inline-block;flex:none;width:20px;height:15px;border-radius:2px;
  object-fit:cover;vertical-align:-2px;box-shadow:inset 0 0 0 1px var(--flag-edge);}
.flag--code{width:auto;height:auto;padding:0 4px;font-size:10px;font-weight:700;
  letter-spacing:.04em;line-height:15px;background:var(--surface-2);
  color:var(--muted);border-radius:3px;box-shadow:none;}

/* Dense station rows - the box is duplicated in critical.css; see the note
   there. Everything below is presentation the async sheet may safely add. */

/* The box, restated so the two files provably agree. CSS_PARITY compares
   these property-by-property and fails the build on any drift. */
.rows{display:grid;gap:10px;grid-template-columns:1fr}
@media (min-width:640px){.rows{grid-template-columns:repeat(2,1fr);gap:10px 16px}}
@media (min-width:1024px){.rows{grid-template-columns:repeat(3,1fr)}}
.row{padding:10px;border-radius:var(--r-md);min-height:84px;
  background:var(--surface);border:1px solid var(--border)}

/* Presentation only - nothing here changes the box, so the async sheet can
   add it without moving anything. The hover state borrows the station's own
   gradient for its edge, which is the same --g2 the artwork tile uses, so a
   row lights up in its own colour rather than a generic grey. */
.row{
  box-shadow:0 1px 2px rgba(16,16,28,.05);
  transition:border-color var(--t-fast), box-shadow var(--t-base),
             transform var(--t-base), background var(--t-fast);
}
.row:hover{
  background:var(--surface);
  border-color:color-mix(in srgb, var(--g2,#4E3E7A) 38%, var(--border));
  box-shadow:0 6px 18px rgba(16,16,28,.10);
  transform:translateY(-1px);
}
.row:focus-within{
  border-color:color-mix(in srgb, var(--accent) 45%, var(--border));
}
/* The row currently playing is the one thing on the page that should be
   unmistakable, so it keeps the accent edge and a tinted ground. */
.row[data-playing]{
  border-color:color-mix(in srgb, var(--accent) 55%, var(--border));
  background:color-mix(in srgb, var(--accent) 7%, var(--surface));
}
@media (prefers-color-scheme:dark){
  :root:not([data-theme="light"]) .row{
    box-shadow:0 1px 2px rgba(0,0,0,.35);
    /* A hairline of light along the top edge is what makes a dark card look
       lifted rather than painted on. */
    background:linear-gradient(var(--surface), var(--surface)) padding-box,
               linear-gradient(180deg,
                 rgba(255,255,255,.07), rgba(255,255,255,0) 40%) border-box;
  }
  :root:not([data-theme="light"]) .row:hover{
    box-shadow:0 8px 22px rgba(0,0,0,.5);
    border-color:color-mix(in srgb, var(--g2,#4E3E7A) 55%, var(--border));
  }
}
:root[data-theme="dark"] .row{
  box-shadow:0 1px 2px rgba(0,0,0,.35);
  background:linear-gradient(var(--surface), var(--surface)) padding-box,
             linear-gradient(180deg,
               rgba(255,255,255,.07), rgba(255,255,255,0) 40%) border-box;
}
:root[data-theme="dark"] .row:hover{
  box-shadow:0 8px 22px rgba(0,0,0,.5);
  border-color:color-mix(in srgb, var(--g2,#4E3E7A) 55%, var(--border));
}
@media (prefers-reduced-motion:reduce){.row:hover{transform:none}}
.row:hover .row__play,.row__play:focus-visible{opacity:1}
.row__name:hover{color:var(--accent-ink)}
.row__name:focus-visible{outline:2px solid var(--focus);outline-offset:2px;border-radius:4px}
.row__fav:hover{color:var(--accent-ink);background:var(--surface-3)}
.row__fav[aria-pressed="true"]{color:var(--accent-ink)}
.row__fav:focus-visible,.row__play:focus-visible{outline:2px solid var(--focus);
  outline-offset:2px}
.row.is-playing{background:var(--surface-2)}
.row.is-playing .row__name{color:var(--accent-ink)}

/* The equaliser replaces the artwork while this row is the one playing. */
.row.is-playing .row__eq{position:absolute;inset:0;display:flex;align-items:flex-end;
  justify-content:center;gap:2px;padding:10px 0;background:var(--overlay);
  border-radius:var(--r-sm);z-index:1}
.row.is-playing .row__eq i{width:3px;border-radius:2px;background:var(--accent);
  height:30%;transform-origin:bottom}

/* ====================================================================== */
/* Footer - four columns                                                  */
/* ====================================================================== */

.footer-cols{display:grid;gap:var(--s4) var(--s3);
  grid-template-columns:1fr;padding-block:var(--s4)}
@media (min-width:640px){.footer-cols{grid-template-columns:repeat(2,1fr)}}
@media (min-width:1024px){.footer-cols{grid-template-columns:repeat(4,1fr);gap:var(--s5)}}

.footer-col{display:flex;flex-direction:column;gap:10px;align-items:flex-start}
.footer-col__title{margin:0 0 2px;font-size:var(--fs-sm);font-weight:700;
  letter-spacing:.04em;text-transform:uppercase;color:var(--text)}
.footer-col a{color:var(--muted);text-decoration:none;font-size:var(--fs-sm);
  line-height:1.4}
.footer-col a:hover{color:var(--accent-ink);text-decoration:underline}
.footer-col a:focus-visible{outline:2px solid var(--focus);outline-offset:2px;
  border-radius:4px}
.footer-col__note{margin:0;color:var(--muted);font-size:var(--fs-sm);max-width:32ch}

.footer-badges{display:flex;flex-wrap:wrap;gap:var(--s1)}
.footer-badges:empty{display:none}

.footer-switches{display:flex;align-items:center;gap:var(--s1);margin-top:var(--s1)}

/* The trust lines sit above the columns and stay full width. */
.site-footer__notice{max-width:70ch}

/* ====================================================================== */
/* Language chips - station language, not interface language              */
/* ====================================================================== */

.langchips{display:flex;gap:var(--s1);overflow-x:auto;padding-block:var(--s1);
  scrollbar-width:none;-ms-overflow-style:none}
.langchips::-webkit-scrollbar{display:none}
/* Box only. The language chips carry .chip-btn as well now, so the surface,
   the edge, the shadow and the hue all come from there — repeating them here
   just painted flat white over the tint. */
.langchips__btn{flex:none;display:inline-flex;align-items:center;gap:6px;
  min-height:36px;padding:0 12px;border-radius:var(--r-full);
  font-size:var(--fs-sm);font-weight:600;cursor:pointer;white-space:nowrap}
.langchips__count{opacity:.7;font-variant-numeric:tabular-nums}

/* ====================================================================== */
/* Genre rail - two scrollable rows of gradient cards                      */
/* ====================================================================== */

.rail-wrap{position:relative}
.genre-rail{
  display:grid;
  grid-auto-flow:column;
  /* Two rows, filled column by column, so the rail is half as long and twice
     as scannable. grid-auto-columns keeps every card the same width whatever
     the label length. */
  grid-template-rows:repeat(2,auto);
  grid-auto-columns:minmax(150px,1fr);
  gap:var(--s1);
  overflow-x:auto;
  scroll-snap-type:x proximity;
  scroll-behavior:smooth;
  padding-block:2px;
  scrollbar-width:none;-ms-overflow-style:none;
}
.genre-rail::-webkit-scrollbar{display:none}
@media (min-width:900px){.genre-rail{grid-auto-columns:minmax(170px,1fr)}}

.genre-card{
  scroll-snap-align:start;
  display:grid;
  grid-template-columns:auto 1fr auto;
  align-items:center;
  gap:10px;
  min-height:56px;
  padding:10px 14px;
  border-radius:var(--r-md);
  text-decoration:none;
  color:var(--text);
  /* One hue per genre from build.py; the gradient is built per theme so a
     single number serves both instead of two hard-coded colour pairs. */
  background:linear-gradient(135deg,
    hsl(var(--h,240) 62% 96%),
    hsl(var(--h,240) 54% 91%));
  box-shadow:inset 0 0 0 1px hsl(var(--h,240) 40% 86%);
  transition:transform var(--t-fast),box-shadow var(--t-fast);
}
.genre-card__icon{display:grid;place-items:center;width:28px;height:28px;
  border-radius:var(--r-full);background:hsl(var(--h,240) 55% 88%);
  color:hsl(var(--h,240) 55% 28%)}
.genre-card__name{font-weight:650;font-size:var(--fs-sm);line-height:1.2;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.genre-card__count{font-size:12px;color:hsl(var(--h,240) 30% 34%);
  font-variant-numeric:tabular-nums}
.genre-card:hover{transform:translateY(-2px);
  box-shadow:inset 0 0 0 1px hsl(var(--h,240) 45% 78%),var(--shadow)}
.genre-card:focus-visible{outline:2px solid var(--focus);outline-offset:2px}

@media (prefers-color-scheme:dark){
  :root:not([data-theme="light"]) .genre-card{
    background:linear-gradient(135deg,
      hsl(var(--h,240) 26% 17%),
      hsl(var(--h,240) 30% 12%));
    box-shadow:inset 0 0 0 1px hsl(var(--h,240) 25% 24%)}
  :root:not([data-theme="light"]) .genre-card__icon{
    background:hsl(var(--h,240) 30% 24%);color:hsl(var(--h,240) 70% 78%)}
  :root:not([data-theme="light"]) .genre-card__count{color:hsl(var(--h,240) 25% 68%)}
}
:root[data-theme="dark"] .genre-card{
  background:linear-gradient(135deg,
    hsl(var(--h,240) 26% 17%),
    hsl(var(--h,240) 30% 12%));
  box-shadow:inset 0 0 0 1px hsl(var(--h,240) 25% 24%)}
:root[data-theme="dark"] .genre-card__icon{
  background:hsl(var(--h,240) 30% 24%);color:hsl(var(--h,240) 70% 78%)}
:root[data-theme="dark"] .genre-card__count{color:hsl(var(--h,240) 25% 68%)}

/* Scroll arrows. Hidden on touch, where swiping is the obvious gesture. */
.rail-arrow{
  position:absolute;top:50%;transform:translateY(-50%);z-index:2;
  display:none;place-items:center;width:36px;height:36px;
  border:1px solid var(--border);border-radius:var(--r-full);
  background:var(--surface);color:var(--text);cursor:pointer;
  box-shadow:var(--shadow)}
.rail-arrow--prev{inset-inline-start:-8px}
.rail-arrow--next{inset-inline-end:-8px}
.rail-arrow[hidden]{display:none}
@media (hover:hover) and (min-width:900px){.rail-arrow{display:grid}}
.rail-arrow:hover{border-color:var(--muted)}

/* The flag sits inside the tile name, so the two align on the same baseline. */
.tile__name{display:flex;align-items:center;gap:8px;min-width:0}

@media (prefers-reduced-motion:reduce){
  .genre-rail{scroll-behavior:auto}
  .genre-card:hover{transform:none}
}

/* ====================================================================== */
/* Hero explainer + How it works                                          */
/* ====================================================================== */

.hero__explainer{margin:var(--s2) auto 0;max-width:56ch;color:var(--muted);
  font-size:var(--fs-md);line-height:1.5;text-align:center}

.how{margin-block:var(--s5)}
.how__steps{display:grid;gap:var(--s2);margin:var(--s3) 0 0;padding:0;
  list-style:none;counter-reset:none}
@media (min-width:768px){.how__steps{grid-template-columns:repeat(3,1fr);gap:var(--s3)}}
.how__step{position:relative;padding:var(--s3);border-radius:var(--r-lg);
  background:var(--surface-2);box-shadow:inset 0 0 0 1px var(--border)}
.how__num{display:grid;place-items:center;width:32px;height:32px;
  border-radius:var(--r-full);background:var(--accent);color:var(--accent-contrast);
  font-weight:800;font-size:var(--fs-sm);margin-bottom:var(--s2)}
.how__stepTitle{margin:0 0 6px;font-size:var(--fs-lg);line-height:1.25}
.how__stepBody{margin:0;color:var(--muted);font-size:var(--fs-sm);line-height:1.5}

/* ====================================================================== */
/* Empty states                                                           */
/* ====================================================================== */

.empty-state{display:grid;justify-items:center;gap:var(--s2);
  padding:var(--s5) var(--s3);text-align:center;color:var(--muted)}
.empty-state__icon{display:grid;place-items:center;width:56px;height:56px;
  border-radius:var(--r-full);background:var(--surface-2);color:var(--muted)}
.empty-state__title{margin:0;font-size:var(--fs-lg);color:var(--text)}
.empty-state__body{margin:0;max-width:44ch;font-size:var(--fs-sm)}
.empty-state__actions{display:flex;flex-wrap:wrap;gap:var(--s1);
  justify-content:center;margin-top:var(--s1)}

/* The language's own name, under the English one. Tagged with lang= in the
   markup so the browser picks a font that can actually draw the script. */
.country__native{display:block;font-size:12px;color:var(--muted);line-height:1.3}

/* Flag inside a search result. gap rather than a margin so it mirrors in RTL
   without a [dir] override. */
.typeahead__name{display:flex;align-items:center;gap:8px;min-width:0}
.typeahead__name .flag{flex:none}

/* ====================================================================== */
/* Tinted directory tiles - genres and languages                          */
/* ====================================================================== */

/*
  One hue per entry, from build.py. Everything below is derived from it, so a
  genre or language tile needs exactly one inline custom property and the
  stylesheet does the rest - in both themes, from the same number.
*/
.country--tinted{
  position:relative;
  border-color:hsl(var(--h,240) 45% 86%);
  background:linear-gradient(135deg,
    hsl(var(--h,240) 62% 98%),
    hsl(var(--h,240) 55% 95%));
}
.country--tinted:hover{
  background:linear-gradient(135deg,
    hsl(var(--h,240) 65% 96%),
    hsl(var(--h,240) 58% 92%));
  border-color:hsl(var(--h,240) 50% 76%);
}
/* A coloured edge on the leading side reads as a category marker without
   shouting. Logical inset so it mirrors in RTL for free. */
.country--tinted::before{
  content:"";position:absolute;inset-block:8px;inset-inline-start:0;
  width:3px;border-radius:0 3px 3px 0;
  background:hsl(var(--h,240) 65% 55%);
}
/* Size is in critical.css as well — the chip is 32px of layout in every row.
   Only the colour is left here. */
.country__icon{
  display:grid;place-items:center;flex:none;
  width:32px;height:32px;border-radius:var(--r-sm);
  background:hsl(var(--h,240) 55% 90%);
  color:hsl(var(--h,240) 60% 30%);
}
/* The language chip carries the language's own script instead of an icon, so
   it sets type rather than an SVG. The size is capped in em and the line-height
   pinned to 1, because a Devanagari or Thai glyph carries marks well above and
   below the Latin em box and would otherwise push the 32px chip out of shape. */
.country__icon--glyph{
  font-size:15px;line-height:1;font-weight:700;
  overflow:hidden;letter-spacing:-.01em;
}
.country--tinted .country__count{color:hsl(var(--h,240) 30% 38%)}

@media (prefers-color-scheme:dark){
  :root:not([data-theme="light"]) .country--tinted{
    border-color:hsl(var(--h,240) 25% 26%);
    background:linear-gradient(135deg,
      hsl(var(--h,240) 24% 16%), hsl(var(--h,240) 28% 12%));
  }
  :root:not([data-theme="light"]) .country--tinted:hover{
    background:linear-gradient(135deg,
      hsl(var(--h,240) 28% 20%), hsl(var(--h,240) 30% 15%));
    border-color:hsl(var(--h,240) 30% 36%);
  }
  :root:not([data-theme="light"]) .country__icon{
    background:hsl(var(--h,240) 30% 24%);color:hsl(var(--h,240) 70% 76%)}
  :root:not([data-theme="light"]) .country--tinted .country__count{
    color:hsl(var(--h,240) 25% 66%)}
}
:root[data-theme="dark"] .country--tinted{
  border-color:hsl(var(--h,240) 25% 26%);
  background:linear-gradient(135deg,
    hsl(var(--h,240) 24% 16%), hsl(var(--h,240) 28% 12%));
}
:root[data-theme="dark"] .country--tinted:hover{
  background:linear-gradient(135deg,
    hsl(var(--h,240) 28% 20%), hsl(var(--h,240) 30% 15%));
  border-color:hsl(var(--h,240) 30% 36%);
}
:root[data-theme="dark"] .country__icon{
  background:hsl(var(--h,240) 30% 24%);color:hsl(var(--h,240) 70% 76%)}
:root[data-theme="dark"] .country--tinted .country__count{
  color:hsl(var(--h,240) 25% 66%)}


/* ====================================================================== */
/* Country tiles - premium treatment                                      */
/* ====================================================================== */

/*
  The old tile was a flat card with three overlapping monogram squares, which
  read as a placeholder rather than a country. Three changes fix it: the collage
  now shows stations that actually have artwork (build.py), the thumbnails sit
  in a proper 2x2 block instead of a squashed overlap, and a soft wash lifts the
  whole thing off the page.
*/
.tile{
  position:relative;
  display:grid;
  overflow:hidden;
  grid-template-rows:auto 1fr;
  gap:var(--s2);
  padding:var(--s2);
  min-height:150px;
  border-radius:var(--r-md);
  background:var(--surface);
  border:1px solid hsl(var(--h,240) 40% 88%);
  color:var(--text);
  box-shadow:0 1px 2px rgba(16,16,28,.04);
  transition:border-color var(--t-fast), box-shadow var(--t-base),
             transform var(--t-base);
}
/* The country's own hue, as a wash rather than a fill. Each tile is tinted
   from the same single --h that the genre and language cards use, so twelve
   countries read as a palette instead of twelve identical grey boxes. */
.tile__wash{
  position:absolute;inset:0;pointer-events:none;
  background:
    radial-gradient(120% 80% at 100% 0%,
      hsl(var(--h,240) 68% 94%), transparent 62%),
    linear-gradient(180deg, hsl(var(--h,240) 58% 98%), transparent 55%);
  opacity:.85;transition:opacity var(--t-base);
}
.tile:hover{
  transform:translateY(-3px);
  border-color:hsl(var(--h,240) 55% 74%);
  box-shadow:0 10px 26px rgba(16,16,28,.12);
  color:var(--text);
}
.tile:hover .tile__wash{opacity:1}
.tile:focus-visible{outline:2px solid var(--focus);outline-offset:2px}

@media (prefers-color-scheme:dark){
  :root:not([data-theme="light"]) .tile{
    border-color:hsl(var(--h,240) 24% 26%);
    box-shadow:none}
  :root:not([data-theme="light"]) .tile__wash{
    background:
      radial-gradient(120% 80% at 100% 0%,
        hsl(var(--h,240) 34% 19%), transparent 62%),
      linear-gradient(180deg, hsl(var(--h,240) 26% 14%), transparent 55%);
    opacity:.85}
  :root:not([data-theme="light"]) .tile:hover{
    border-color:hsl(var(--h,240) 45% 42%);
    box-shadow:0 10px 26px rgba(0,0,0,.45)}
}
:root[data-theme="dark"] .tile{
  border-color:hsl(var(--h,240) 24% 26%);
  box-shadow:none}
:root[data-theme="dark"] .tile__wash{
  background:
    radial-gradient(120% 80% at 100% 0%,
      hsl(var(--h,240) 34% 19%), transparent 62%),
    linear-gradient(180deg, hsl(var(--h,240) 26% 14%), transparent 55%);
  opacity:.85}
:root[data-theme="dark"] .tile:hover{
  border-color:hsl(var(--h,240) 45% 42%);
  box-shadow:0 10px 26px rgba(0,0,0,.45)}

/* 2x2 block. Real logos, letterboxed on their own tint, never cropped. */
.tile__collage{
  position:relative;
  display:grid;
  grid-template-columns:repeat(2,1fr);
  gap:4px;
  width:84px;
}
.tile__art{
  width:100%;height:40px;
  border:1px solid var(--border);
  border-radius:var(--r-sm);
  background:var(--surface-2);
  object-fit:contain;
  padding:3px;
  display:grid;place-items:center;overflow:hidden;
}
.tile__art + .tile__art{margin-inline-start:0}
.tile__art .mono__letters{font-size:12px}

.tile__body{position:relative;display:grid;gap:2px;align-content:end}
.tile__name{display:flex;align-items:flex-start;gap:8px;min-width:0}
.tile__name .flag{margin-top:3px}
/* Two lines rather than an ellipsis. "The United States Of America" and
   "The Russian Federation" are real country names in this data and truncating
   them to "The United St..." helps nobody; the tile has the vertical room. */
.tile__label{
  font-weight:700;font-size:var(--fs-sm);line-height:1.3;
  /* Wrap, don't ellipsise. white-space is an inherited property, so it is
     stated here rather than left to whatever an ancestor happens to declare.
     A -webkit-line-clamp was tried and is pointless in this slot: .tile__body
     is a grid container, so a grid item's display:-webkit-box is blockified
     to flow-root and the clamp never applies. Two lines is what the names
     need and the grid row stretches every tile to match the tallest. */
  white-space:normal;overflow-wrap:anywhere;
}
@media (min-width:700px){.tile__label{font-size:var(--fs-md)}}
.tile__count{font-size:var(--fs-sm);color:var(--muted);font-variant-numeric:tabular-nums}

@media (min-width:700px){
  .tile{min-height:168px}
  .tile__collage{width:96px}
  .tile__art{height:46px}
}

@media (prefers-reduced-motion:reduce){
  .tile:hover{transform:none}
}

/* Station artwork tile: a faint wash of the station's own colour behind the
   logo, and a real edge. A logo on flat grey reads as unfinished; the tint is
   the same deterministic gradient the monogram would have used, at low enough
   opacity that it never competes with the artwork. */
.row__art{
  background:
    linear-gradient(140deg,
      color-mix(in srgb, var(--g1,#2A2A45) 16%, var(--surface-2)),
      color-mix(in srgb, var(--g2,#4E3E7A) 12%, var(--surface-2)));
  box-shadow:inset 0 0 0 1px var(--border), 0 1px 2px rgba(16,16,28,.05);
}
.row:hover .row__art{
  box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--g2,#4E3E7A) 32%, var(--border)),
             0 2px 6px rgba(16,16,28,.10);
}
/* The monogram fills the tile edge to edge, and an inset box-shadow paints
   underneath child content, so .row__art's own hairline vanished behind every
   monogram - a third of the list was a flat coloured block with no edge. The
   ring goes on the monogram itself, in white, because it sits on a dark
   gradient; tiles showing a real logo keep the border-coloured hairline. */
.row__art .mono{box-shadow:inset 0 0 0 1px rgba(255,255,255,.22)}
.card__art{
  background:
    linear-gradient(140deg,
      color-mix(in srgb, var(--g1,#2A2A45) 14%, var(--surface-2)),
      color-mix(in srgb, var(--g2,#4E3E7A) 10%, var(--surface-2)));
}

/* The header's Install control. Quieter than a primary button - it is an
   offer, not the main action - but clearly a button rather than a nav link. */
/* Selector and metrics are shared verbatim with critical.css, and
   CSS_PARITY in build.py fails the build if the two drift. It is written as
   .site-nav a.btn--install in both because the control is an <a> now and has
   to outrank `.site-nav a`, which would otherwise give it nav-link padding. */
.site-nav a.btn--install{
  min-height:36px;padding:0 14px;font-size:var(--fs-sm);font-weight:600;
  border-radius:var(--r-full);
  border:1px solid color-mix(in srgb, var(--accent) 45%, var(--border));
  background:color-mix(in srgb, var(--accent) 10%, var(--surface));
  color:var(--accent-ink);
}
.btn--install:hover{
  background:color-mix(in srgb, var(--accent) 18%, var(--surface));
  border-color:var(--accent);
}

/* The footer's install link. The header's button shape, at footer weight, so
   the one route that is always present on a phone still reads as an action. */
.footer-install{
  display:inline-flex;align-items:center;justify-content:center;
  align-self:start;min-height:36px;padding:0 14px;
  font-size:var(--fs-sm);font-weight:600;text-decoration:none;
  border-radius:var(--r-full);
  border:1px solid color-mix(in srgb, var(--accent) 45%, var(--border));
  background:color-mix(in srgb, var(--accent) 10%, var(--surface));
  color:var(--accent-ink);
}
.footer-install:hover{
  background:color-mix(in srgb, var(--accent) 18%, var(--surface));
  border-color:var(--accent);
}
.footer-install[hidden]{display:none}
a.btn--install{text-decoration:none}
a.btn--install[hidden]{display:none}

/* The install page's inline button is revealed by pwa.js only where the
   browser can actually prompt; the paragraph must not leave a gap otherwise. */
[data-install-inline]:has(> [hidden]){display:none}

/* Get the app (homepage): placeholder until the Play Store listing exists. */
.get-app__card {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s3);
  padding: var(--s4);
  border-radius: var(--r-lg);
  background: var(--surface);
  border: 1px solid var(--border);
}
.get-app__mark {
  display: grid;
  place-items: center;
  flex-shrink: 0;
  width: 64px;
  height: 64px;
  border-radius: var(--r-md);
  background: var(--accent);
  color: #FFFFFF;
}
.get-app__copy { flex: 1 1 260px; min-width: 0; }
.get-app__copy .section__title { margin: 0; }
.get-app__body { margin: var(--s1) 0 0; color: var(--muted); }
.get-app__store {
  display: inline-flex;
  align-items: center;
  gap: var(--s1);
  min-height: var(--tap);
  padding: 10px var(--s3);
  border-radius: var(--r-full);
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--muted);
  font: inherit;
  cursor: not-allowed;
}
.get-app__store-name { font-weight: 700; color: var(--text); }
.get-app__soon { font-size: var(--fs-sm); }

/* Cookie consent banner (analytics.js). Fixed, so it never shifts the page;
   above the player bar when one is showing. Accept uses Orange Deep: white
   on it is 4.9:1 (WCAG AA). */
.consent {
  position: fixed;
  z-index: 40;
  left: var(--s2);
  right: var(--s2);
  bottom: calc(var(--s2) + env(safe-area-inset-bottom, 0px));
  max-width: 720px;
  margin-inline: auto;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s1) var(--s2);
  padding: var(--s2);
  border-radius: var(--r-lg);
  background: var(--surface);
  border: 1px solid var(--border);
  box-shadow: var(--shadow);
}
.consent[hidden] { display: none; }
.player:not([hidden]) ~ .consent {
  bottom: calc(var(--player-h) + var(--s1) + env(safe-area-inset-bottom, 0px));
}
.consent__text { flex: 1 1 280px; margin: 0; font-size: var(--fs-sm); color: var(--text); }
.consent__text a { margin-inline-start: 0.3em; color: var(--accent-ink); text-decoration: underline; }
.consent__actions { display: flex; gap: var(--s1); margin-inline-start: auto; }
.consent__accept { background: var(--accent-strong, #C04D0B); color: #FFFFFF; }
.consent__accept:hover { filter: brightness(1.08); }
.footer-cookie-link { color: inherit; text-decoration: underline; }
