/* ───────────────────────────────────────────────────────────
   Yamanote Line — station melody player
   Visual system lifted from the Figma source:
     bg        rgb(25,25,23)      warm near-black
     JR green  rgb(50,205,102)
     control   linear-gradient(180deg, #474745, #353533)
     type      Space Grotesk (Medium / Bold)
   ─────────────────────────────────────────────────────────── */

:root {
  /* ── themeable surface tokens (dark is the default) ──────────────
     Light mode is applied via html[data-theme="light"] below; the app
     boot script stamps the attribute before first paint (no flash). */
  --bg:        rgb(25, 25, 23);     /* warm near-black surface         */
  --fg:        #fff;                /* primary text / active icons     */
  --panel:     rgb(34, 34, 31);     /* elevated surface (info modal)   */
  --panel-edge: rgba(255, 255, 255, 0.07);
  --green:     rgb(50, 205, 102);   /* JR green — constant in both modes */
  --ctrl-top:  rgb(71, 71, 69);     /* button gradient — top stop      */
  --ctrl-bot:  rgb(53, 53, 51);     /* button gradient — bottom stop   */
  --ctrl-ring: rgba(255, 255, 255, 0.2);   /* inset top highlight      */
  --ctrl-shadow: 0 0 0 0 transparent;       /* extra lift in light mode */
  /* control surface as gradient tokens so the pressed state can flip the
     gradient vertically. --ctrl-bg-active is the same stops, reversed. */
  --ctrl-bg:        linear-gradient(180deg, var(--ctrl-top), var(--ctrl-bot));
  --ctrl-bg-active: linear-gradient(180deg, var(--ctrl-bot), var(--ctrl-top));
  --ctrl-inset:     inset 0 1px 0 0 var(--ctrl-ring);   /* edge highlights */
  --ctrl-blur:      none;                                /* glass blur (light only) */
  --dim:       rgba(255, 255, 255, 0.30);   /* inactive text            */
  --scrub-hover: rgba(255, 255, 255, 0.5);  /* scrub segment hover (lighter than --dim) */
  --scrub-chip-grey: rgb(140, 140, 138);    /* grey hover chip (mirrors the green chip) */
  --spine:     rgba(255, 255, 255, 0.30);   /* dotted-line dots         */
  --m-card:    rgba(255, 255, 255, 0.045);  /* modal grouped-card surface */
  --m-row-hover: rgba(255, 255, 255, 0.06); /* modal link-row hover */
  --m-track:   rgba(0, 0, 0, 0.28);         /* modal segmented-control track */
  --scroll-thumb:       rgba(255, 255, 255, 0.22);  /* modal scrollbar thumb */
  --scroll-thumb-hover: rgba(255, 255, 255, 0.36);

  /* corner padding for the desktop dark-mode / info buttons */
  --corner-pad: 36px;

  --badge:     64px;    /* JY number box size (overridden by JS)         */
  --dot:       10px;    /* dotted-line period (set by JS)               */

  /* shared "halo" that lets chrome occlude the dotted spine,
     matching the figma 0 0 10px 10px bg shadow */
  --halo: 0 0 10px 11px var(--bg);

  --ease: cubic-bezier(0.32, 0.72, 0, 1);
  --dur:  0.62s;
  --theme-dur: 0.45s;   /* dark/light cross-fade duration (central, tunable) */

  /* intro reveal — plays once, after the layout is measured */
  --intro-dur: 0.9s;
  /* ribbon/stations fade a full second slower than the chrome slide-in */
  --intro-fade-dur: 1.9s;

  /* ── chrome spacing — TUNE THESE ──────────────────────────────
     The loop toggle (top) and media controls (bottom) need DIFFERENT
     spacing in Safari's browser view vs. the installed fullscreen PWA,
     so each mode gets its own values. These defaults target Safari:
     the browser toolbars eat into the viewport, so we pull both the
     toggle and the controls close to the edges to undo the squashed feel. */
  --toggle-top:      max(20px, env(safe-area-inset-top, 0px) + 8px);
  --controls-bottom: max(20px, env(safe-area-inset-bottom, 0px) + 16px);
}

/* ── light mode ───────────────────────────────────────────────────
   Warm near-white surface, warm near-black ink — the dark palette
   inverted while the JR green accent stays put. Only the surface
   tokens change; every rule below reads them, so the whole app
   (ribbon, badges, eraser, halo, buttons, modal) re-themes at once. */
html[data-theme="light"] {
  --bg:        rgb(242, 241, 237);   /* warm near-white — figma surface  */
  --fg:        rgb(25, 25, 23);
  --panel:     rgb(252, 251, 248);
  --panel-edge: rgba(25, 25, 23, 0.08);
  /* figma "glass" control: a translucent grey gradient over the bg, frosted
     with a backdrop blur, edged with a white top highlight and a dark hairline
     at the bottom. A soft drop-shadow keeps it lifted off the near-white bg. */
  --ctrl-bg:        linear-gradient(180deg, rgba(204, 204, 204, 0.2) 0%, rgba(102, 102, 102, 0.2) 100%);
  --ctrl-bg-active: linear-gradient(180deg, rgba(102, 102, 102, 0.2) 0%, rgba(204, 204, 204, 0.2) 100%);
  --ctrl-inset:     inset 0 1px 0 0 rgb(255, 255, 255), inset 0 -1px 0 0 rgba(25, 25, 23, 0.3);
  --ctrl-blur:      blur(16px);
  --ctrl-shadow: 0 4px 18px rgba(25, 25, 23, 0.16), 0 1px 4px rgba(25, 25, 23, 0.12);
  --dim:       rgba(25, 25, 23, 0.3);
  --scrub-hover: rgba(25, 25, 23, 0.5);
  --scrub-chip-grey: rgb(120, 120, 116);
  --spine:     rgba(25, 25, 23, 0.22);
  --m-card:    rgba(0, 0, 0, 0.035);
  --m-row-hover: rgba(0, 0, 0, 0.05);
  --m-track:   rgba(0, 0, 0, 0.06);
  --scroll-thumb:       rgba(25, 25, 23, 0.22);
  --scroll-thumb-hover: rgba(25, 25, 23, 0.36);
}

/* installed PWA (Home Screen, fullscreen) ─────────────────────────
   Detected two ways for maximum coverage: the standalone display-mode
   media query (iOS 16.4+) and a `standalone` class that app boot stamps
   on <html> via navigator.standalone (older iOS). Either one wins.

   Here .app is deliberately over-extended past the bottom edge by the
   TOP inset (see the .app rule), so a plain `bottom:` lands too low and
   crowds the home indicator. We add that top inset back into the offset
   to cancel the over-extension, then sit a comfortable gap above the
   home indicator (its own bottom inset + breathing room). */
html.standalone {
  --toggle-top:      max(48px, env(safe-area-inset-top, 0px) + 24px);
  --controls-bottom: calc(env(safe-area-inset-top, 0px)
                        + env(safe-area-inset-bottom, 0px) + 38px);
}
@media (display-mode: standalone) {
  html {
    --toggle-top:      max(48px, env(safe-area-inset-top, 0px) + 24px);
    --controls-bottom: calc(env(safe-area-inset-top, 0px)
                          + env(safe-area-inset-bottom, 0px) + 38px);
  }
}

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }

html, body {
  margin: 0;
  height: 100vh;
  background: var(--bg);
  transition: background-color var(--theme-dur, 0.45s) var(--ease);
  overscroll-behavior: none;
  overflow: hidden;
  /* disable the double-tap-to-zoom gesture (iOS ignores user-scalable=no).
     In Safari it stops the zoom; in the installed PWA it prevents the
     standalone viewport from snapping to a zoomed, mis-aligned state where
     the controls fall out from under your taps. */
  touch-action: manipulation;
}

body {
  font-family: "Space Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  color: var(--fg);
  transition: color var(--theme-dur, 0.45s) var(--ease),
              background-color var(--theme-dur, 0.45s) var(--ease);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  user-select: none;
  /* pin the body to the viewport so a touch-swipe can never scroll the page
     (which is what flashes the iOS scrollbar). .app is position:fixed, so
     fixing the body too doesn't affect its layout. */
  position: fixed;
  inset: 0;
  width: 100%;
}

/* full-bleed app surface, centred on a phone-width column.
   In an iOS standalone PWA with a black-translucent status bar, content draws
   UNDER the status bar — and WebKit then under-reports every height reference
   (100vh, 100dvh, window.innerHeight) by the top safe-area inset, leaving a
   black gap at the BOTTOM. We add that inset back so the surface reaches the
   physical screen edge. In a normal browser the inset is 0, so this is a no-op. */
.app {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: calc(100vh + env(safe-area-inset-top, 0px));   /* fallback */
  height: calc(100dvh + env(safe-area-inset-top, 0px));  /* dynamic vh */
  overflow: hidden;
  background: var(--bg);
  transition: background-color var(--theme-dur, 0.45s) var(--ease);
  touch-action: manipulation;
}

.stage {
  position: absolute;
  inset: 0;
  margin: 0 auto;
  max-width: 460px;
}

/* ── dotted spine ─────────────────────────────────────────────
   Lives INSIDE the ribbon so it scrolls together with the names.
   --dot divides the slot height evenly, so the silent re-centre
   (a jump of N×slot) preserves the dot phase — no visible flicker. */
.spine {
  position: absolute;
  left: 50%;
  width: 4px;
  margin-left: -2px;
  transform: translateY(0);
  transition: transform var(--dur) var(--ease),
              opacity var(--theme-dur, 0.45s) var(--ease);
  background-image: radial-gradient(circle at center,
                    var(--spine) 1.5px, transparent 2px);
  background-size: 4px var(--dot, 10px);
  background-repeat: repeat-y;
  pointer-events: none;
  will-change: transform;
}

/* ── ribbon: static container; stations are positioned individually ─ */
.ribbon {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 0;
  transition: opacity var(--intro-fade-dur) var(--ease),
              transform var(--dur) var(--ease);
}
/* while a finger is down the ribbon must track instantly (no easing lag) */
.ribbon.dragging { transition: opacity var(--intro-fade-dur) var(--ease); }

/* ── intro reveal ─────────────────────────────────────────────
   While booting, the chrome is parked off-screen and the ribbon is
   transparent so the pre-font-load layout jump is never seen. JS drops
   the .booting class once the final layout is measured, and everything
   animates into place together. */
body.booting .ribbon { opacity: 0; }
body.booting .toggle {
  transform: translateX(-50%) translateY(calc(-100% - 140px));
}
body.booting .controls {
  transform: translateX(-50%) translateY(calc(100% + 160px));
}
.no-anim,
.no-anim * { transition: none !important; }

/* each station eases to the Y for its role (prev / current / next);
   far stations fade to 0 so only those three are ever visible */
.station {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 0;
  pointer-events: none;
  transition: transform var(--dur) var(--ease),
              opacity var(--dur) var(--ease);
  will-change: transform, opacity;
}
.station.far .name { pointer-events: none; }

/* Make station text bolder in light mode to compensate for the higher contrast. In dark mode the text is already quite punchy, so 500
   is plenty; in light mode the same weight looks thinner, so we boost it
   to 700. The JY badge text gets the same boost to keep it visually balanced. */
html[data-theme="light"] .station.active .name,
html[data-theme="light"] .station.active .badge .jy,
html[data-theme="light"] .station.active .badge .num { 
  font-weight: 700; 
}

/* a bg-coloured block that erases the dotted line around this
   station's content, leaving a clean vertical gap. It grows when
   the station becomes active to clear the name AND the JY badge. */
.station .eraser {
  position: absolute;
  left: 50%;
  width: 200px;
  transform: translateX(-50%);
  background: var(--bg);
  top: -24px;            /* 12px name half + 12px gap          */
  height: 48px;
  transition: top var(--dur) var(--ease),
              height var(--dur) var(--ease),
              background-color var(--theme-dur, 0.45s) var(--ease);
  pointer-events: none;
}
.station.active .eraser {
  top: calc(-50px - var(--badge));     /* clear box top + 16px gap        */
  height: calc(128px + var(--badge));  /* down past name to scrub bottom  */
}

/* the name sits exactly on the slot centre line */
.station .name {
  position: absolute;
  left: 0;
  right: 0;
  top: -4px;
  text-align: center;
  font-weight: 500;
  font-size: 36px;
  line-height: 1;
  letter-spacing: -0.01em;
  white-space: nowrap;
  color: var(--dim);
  transform: translateY(-50%) scale(0.667);   /* 36 → 24px when dim */
  transform-origin: center center;
  transition: color var(--dur) var(--ease),
              transform var(--dur) var(--ease);
  pointer-events: auto;
  cursor: pointer;
}

.station.active .name {
  color: var(--fg);
  transform: translateY(-50%) scale(1);
  cursor: default;
}

/* Hover feedback on the tappable prev/next names. Scoped to (hover: hover) so
   it only affects mouse users and never leaves a sticky highlight on touch;
   the active name is excluded since it isn't a jump target. */
@media (hover: hover) {
  .station:not(.active) .name:hover { color: var(--scrub-hover); }
}

/* Kanji station names (JP mode) use Zen Maru Gothic. data-namelang is set on
   <html> by applyLang; the weight follows the theme — medium in dark, bold in
   light — to keep the heavier strokes legible on the near-white surface. */
html[data-namelang="ja"] .station .name {
  font-family: "Zen Maru Gothic", "Space Grotesk", sans-serif;
  font-weight: 500;
  letter-spacing: 0;
}

/* the JY badge floats above the name, fading + scaling into view
   only while its station is the active one */
.station .badge {
  position: absolute;
  bottom: calc(50% + 34px);       /* 18px name half + 16px gap */
  left: 50%;
  width: var(--badge);
  height: var(--badge);
  margin-left: calc(var(--badge) / -2);
  border-radius: 6px;
  background: var(--green);
  opacity: 0;
  transform: scale(0.55);
  transform-origin: center bottom;
  transition: opacity calc(var(--dur) * 0.85) var(--ease),
              transform var(--dur) var(--ease);
}
.station.active .badge {
  opacity: 1;
  transform: scale(1);
}

.badge .inner {
  position: absolute;
  inset: 5px;
  border-radius: 3px;
  background: var(--bg);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  transition: background-color var(--theme-dur, 0.45s) var(--ease);
}
.badge .jy {
  font-size: 15px;
  font-weight: 500;
  line-height: 1;
  letter-spacing: 0.02em;
}
.badge .num {
  font-size: 31px;
  font-weight: 500;
  line-height: 0.86;
  letter-spacing: -0.02em;
}

/* ── top loop toggle ──────────────────────────────────────── */
.toggle {
  position: absolute;
  top: var(--toggle-top);
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: stretch;
  border-radius: 115px;
  background: var(--ctrl-bg);
  -webkit-backdrop-filter: var(--ctrl-blur);
  backdrop-filter: var(--ctrl-blur);
  box-shadow: var(--halo), var(--ctrl-shadow), var(--ctrl-inset);
  z-index: 5;
  transition: transform var(--intro-dur) var(--ease),
              box-shadow var(--theme-dur, 0.45s) var(--ease);
}
.toggle button {
  position: relative;
  appearance: none;
  border: 0;
  background: none;
  font-family: inherit;
  font-weight: 700;
  font-size: 20px;
  letter-spacing: -0.01em;
  white-space: nowrap;
  color: var(--dim);
  padding: 13px 24px;
  cursor: pointer;
  transition: color 0.35s var(--ease);
}
.toggle button.on { color: var(--fg); }
.toggle .divider {
  width: 1px;
  align-self: stretch;
  margin: 8px 0 8px -4px;
  background: var(--dim);
  opacity: 0.5;
}

/* ── bottom media controls ────────────────────────────────── */
.controls {
  position: absolute;
  bottom: var(--controls-bottom);
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 24px;
  z-index: 5;
  transition: transform var(--intro-dur) var(--ease);
}

.ctrl {
  appearance: none;
  border: 0;
  padding: 0;
  display: grid;
  place-items: center;
  cursor: pointer;
  color: var(--fg);
}
.ctrl svg { display: block; }

button#share svg {
  margin-top: -3px;
}

.ctrl.skip {
  width: 64px;
  height: 64px;
  border-radius: 50%;
  background: var(--ctrl-bg);
  -webkit-backdrop-filter: var(--ctrl-blur);
  backdrop-filter: var(--ctrl-blur);
  /* box-shadow: var(--halo), var(--ctrl-shadow), var(--ctrl-inset); */
  box-shadow: var(--halo), var(--ctrl-inset);
  transition: transform 0.18s var(--ease), filter 0.2s var(--ease),
              box-shadow var(--theme-dur, 0.45s) var(--ease);
}
.ctrl.play {
  width: 96px;
  height: 96px;
  border-radius: 50%;
  color: #fff;                 /* white glyph on JR green in both themes */
  background: var(--green);
  box-shadow: var(--halo), inset 0 1px 0 0 rgba(255, 255, 255, 0.4);
  transition: transform 0.18s var(--ease);
}

/* pressed feedback: shrink slightly and darken — consistent in both themes. */
.ctrl:active   { transform: scale(0.9); }
/* .ctrl.skip:active { filter: brightness(0.85); } */

/* play / pause icon swap */
.play .icon-pause { display: none; }
.play.playing .icon-play  { display: none; }
.play.playing .icon-pause { display: block; }

/* ── share + info buttons ─────────────────────────────────────────
   Two extra .ctrl.skip buttons. They are positioned WITHOUT transform
   (only left/right/bottom) so the shared .ctrl:active scale never
   fights their placement. Default (mobile) layout flanks the play
   button; the desktop media query relocates them to the corners. */
.corner-ctrl {
  position: absolute;
  z-index: 5;
  bottom: calc(var(--controls-bottom) + 16px);   /* centre 64px on 96px play */
  transition: transform 0.18s var(--ease), filter 0.2s var(--ease),
              background-color var(--theme-dur, 0.45s) var(--ease);
}
/* mobile: sit left & right of the play button (replacing prev/next).
   play centre is the viewport centre; centre-to-centre spacing is
   48 (play half) + 24 (gap) + 32 (skip half) = 104px. */
.share-btn    { left: calc(50% - 136px); }   /* left edge = centre − 104 − 32 */
.info-btn     { left: calc(50% + 72px);  }    /* left edge = centre + 104 − 32 */

/* mobile: swipe replaces prev/next, so hide them */
@media (max-width: 639px) {
  #prev, #next { display: none; }
}

/* desktop: keep prev/next in the centre row, banish toggle + info to
   the bottom corners with comfortable edge padding (safe-area aware). */
@media (min-width: 640px) {
  .corner-ctrl {
    bottom: max(var(--corner-pad), env(safe-area-inset-bottom, 0px) + 12px);
  }
  .share-btn {
    left: max(var(--corner-pad), env(safe-area-inset-left, 0px) + 12px);
  }
  .info-btn {
    left: auto;
    right: max(var(--corner-pad), env(safe-area-inset-right, 0px) + 12px);
  }
}

/* desktop: zoom the ribbon (station names, badges, scrub bars) to 125%.
   Uses the standalone `scale` property — NOT `transform` — because JS
   drives .ribbon's `transform` directly for the drag gesture (see app.js);
   `scale` composes independently so it never gets clobbered.
   transform-origin is pinned to --ribbon-mid (the vertical centre of the
   toggle↔controls band, set by measure() in app.js) so the active station
   stays put at its calculated position instead of drifting as it scales.
   .toggle / .controls / .corner-ctrl / .modal are siblings of .ribbon, not
   descendants, so they are completely unaffected and stay at 100%. */
@media (min-width: 640px) {
  .ribbon {
    scale: 1.25;
    transform-origin: 50% var(--ribbon-mid, 50%);
  }
}

/* corner buttons slide up with the rest of the chrome on boot — a pure
   transform slide (no opacity fade) so they move in perfect lock-step with
   the playback controls instead of fading in ahead of them. They stay hidden
   simply by being parked off-screen, exactly like .controls.
   JS adds .intro before dropping .booting so the entrance uses
   var(--intro-dur) rather than the 0.18s press duration. */
body.booting .corner-ctrl {
  transform: translateY(calc(100% + 160px));
}
.corner-ctrl.intro {
  transition: transform var(--intro-dur) var(--ease),
              filter 0.2s var(--ease),
              background-color var(--theme-dur, 0.45s) var(--ease) !important;
}

/* ── share fallback popover ────────────────────────────────────────
   Shown only when the Web Share API is unavailable (desktop Firefox,
   older browsers). JS positions it (position: fixed) anchored above the
   share button, so only the surface styling lives here — matched to the
   modal's grouped-card look (--panel / --panel-edge). */
.share-pop {
  position: fixed;
  z-index: 20;
  min-width: 196px;
  padding: 6px;
  background: var(--panel);
  border: 1px solid var(--panel-edge);
  border-radius: 14px;
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.35);
  display: flex;
  flex-direction: column;
  gap: 2px;
  opacity: 0;
  transform: translateY(6px);
  transition: opacity 0.16s var(--ease), transform 0.16s var(--ease);
}
/* The `display: flex` above would otherwise override the UA `[hidden]`
   rule, leaving the (transparent) popover rendered at its default fixed
   position — where it silently covers the left of the loop toggle on narrow
   viewports and swallows those taps. This keeps `hidden` authoritative. */
.share-pop[hidden] { display: none; }
.share-pop.open { opacity: 1; transform: translateY(0); }
.share-pop__item {
  display: flex;
  align-items: center;
  gap: 11px;
  width: 100%;
  border: 0;
  background: none;
  color: var(--fg);
  font-family: inherit;
  font-size: 13.5px;
  font-weight: 500;
  text-decoration: none;
  text-align: left;
  padding: 10px 12px;
  border-radius: 9px;
  cursor: pointer;
  transition: background-color 0.15s var(--ease);
}
.share-pop__item:hover { background: var(--m-row-hover); }
.share-pop__item > svg { color: var(--green); flex-shrink: 0; }

/* ── info modal ───────────────────────────────────────────────────
   A grouped, scrolling sheet: fixed header + scrollable body of labelled
   rounded-card sections. Floating card on desktop; full-bleed on mobile
   (see the max-width:639px block at the end). ──────────────────────── */
.modal[hidden] { display: none; }
.modal {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}
.modal-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.55);
  -webkit-backdrop-filter: blur(3px);
  backdrop-filter: blur(3px);
  opacity: 0;
  transition: opacity 0.3s var(--ease);
}
.modal.open .modal-backdrop { opacity: 1; }

.modal-card {
  position: relative;
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 480px;
  max-height: calc(100dvh - 48px);
  max-height: calc(100vh - 48px);
  overflow: hidden;
  background: var(--panel);
  color: var(--fg);
  border: 1px solid var(--panel-edge);
  border-radius: 22px;
  box-shadow: 0 28px 70px rgba(0, 0, 0, 0.5),
              inset 0 1px 0 0 rgba(255, 255, 255, 0.06);
  transform: translateY(18px) scale(0.97);
  opacity: 0;
  transition: transform 0.38s var(--ease), opacity 0.38s var(--ease);
}
.modal.open .modal-card { transform: none; opacity: 1; }

/* fixed header — the body below it scrolls */
.modal-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 15px 16px 14px 18px;
  border-bottom: 1px solid var(--panel-edge);
  flex-shrink: 0;
}
.modal-htitle { display: flex; align-items: center; gap: 10px; }
.modal-htitle b { font-size: 15px; font-weight: 700; letter-spacing: -0.01em; }
.modal-mark {
  width: 30px;
  height: 30px;
  border-radius: 8px;
  /* background: var(--green); */
  display: grid;
  place-items: center;
  flex-shrink: 0;
}
.modal-mark span {
  display: grid;
  place-items: center;
  width: calc(100% - 7px);
  height: calc(100% - 7px);
  border-radius: 5px;
  /* background: var(--panel); */
  color: var(--fg);
  font-weight: 700;
  font-size: 11px;
  letter-spacing: 0.03em;
  transition: background-color var(--theme-dur, 0.45s) var(--ease);
}
.modal-mark svg {
  height: 24px;
  width: auto;
}
.modal-mark svg .rails {
  fill: var(--fg);
}
html[data-theme="light"] .modal-mark svg .rails {
  fill: #191917;
}
.modal-close {
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 50%;
  background: rgba(127, 127, 127, 0.14);
  color: var(--fg);
  opacity: 0.6;
  cursor: pointer;
  flex-shrink: 0;
  transition: opacity 0.2s var(--ease);
}
.modal-close:hover { opacity: 1; }

.modal-scroll {
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: 18px 16px 22px;
  scrollbar-width: thin;                              /* Firefox */
  scrollbar-color: var(--scroll-thumb) transparent;
}
/* WebKit (Chrome/Safari/Edge): a slim, palette-matched thumb on a transparent
   track. The transparent border + padding-box clip insets the visible thumb so
   it reads as ~4px with breathing room rather than a full-width bar. */
.modal-scroll::-webkit-scrollbar { width: 9px; }
.modal-scroll::-webkit-scrollbar-track { background: transparent; }
.modal-scroll::-webkit-scrollbar-thumb {
  background-color: var(--scroll-thumb);
  border-radius: 999px;
  border: 2px solid transparent;
  background-clip: padding-box;
  transition: background-color 0.2s var(--ease);
}
.modal-scroll::-webkit-scrollbar-thumb:hover { background-color: var(--scroll-thumb-hover); }

/* grouped sections */
.m-grp { margin: 0 0 20px; }
.m-grp:last-of-type { margin-bottom: 14px; }
.m-lbl {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--dim);
  margin: 0 4px 8px;
}
.m-card {
  background: var(--m-card);
  border: 1px solid var(--panel-edge);
  border-radius: 14px;
  overflow: hidden;
}
.m-pad { padding: 14px 15px; }
.m-h { margin: 0 0 20px; font-size: 17px; font-weight: 700; letter-spacing: -0.01em; line-height: 1.15; }
.m-p { margin: 0; font-size: 14px; line-height: 1.55; color: var(--fg); opacity: 0.72; text-wrap: stable; }
.m-note {
  display: flex;
  gap: 8px;
  align-items: flex-start;
  background: rgba(50, 205, 102, 0.1);
  border-radius: 10px;
  padding: 9px 11px;
  margin: 12px 0 0;
  font-size: 12.5px;
  line-height: 1.45;
}
.m-note svg { color: var(--green); flex-shrink: 0; margin-top: 1px; }

/* rows (links + settings) */
.m-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 13px 15px;
  border-top: 1px solid var(--panel-edge);
  color: var(--fg);
  text-decoration: none;
  cursor: pointer;
}
.m-row:first-child { border-top: 0; }
.m-row > svg.lead { color: var(--green); flex-shrink: 0; }
.m-row > svg.trail { color: var(--dim); flex-shrink: 0; }
.m-txt { flex: 1; font-size: 13.5px; }
.m-sub { display: block; font-size: 11.5px; color: var(--dim); margin-top: 2px; }
.m-row--static { cursor: default; }
a.m-row:hover { background: var(--m-row-hover); }

/* melodies table */
.m-tbl { width: 100%; border-collapse: collapse; font-size: 12.5px; }
.m-tbl th {
  text-align: left;
  color: var(--dim);
  font-weight: 700;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  padding: 11px 14px 8px;
  background: rgba(127, 127, 127, 0.08);
}
.m-tbl td { padding: 9px 14px; border-top: 1px solid var(--panel-edge); }
.m-tbl .m-stn { font-weight: 700; }
.m-tbl .m-mel { color: var(--dim); }
.m-tbl .m-mel.shared { color: var(--green); }
.m-tbl tr.m-extra { display: none; }
.m-tbl.expanded tr.m-extra { display: table-row; }
.m-more {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  width: 100%;
  border: 0;
  border-top: 1px solid var(--panel-edge);
  background: rgba(127, 127, 127, 0.06);
  color: var(--green);
  font-family: inherit;
  font-size: 12.5px;
  font-weight: 700;
  padding: 12px;
  cursor: pointer;
}
.m-more svg { transition: transform 0.2s var(--ease); }
.m-more.open svg { transform: rotate(180deg); }

/* segmented language toggle */
.m-seg {
  display: flex;
  background: var(--m-track);
  border-radius: 999px;
  padding: 3px;
  flex-shrink: 0;
}
.m-seg button {
  border: 0;
  background: none;
  color: var(--dim);
  font-family: inherit;
  font-size: 12px;
  font-weight: 700;
  padding: 5px 13px;
  border-radius: 999px;
  cursor: pointer;
  transition: color 0.2s var(--ease), background-color 0.2s var(--ease);
}
.m-seg button.on { background: var(--green); color: #fff; }

/* offline-download pill button */
.m-pill {
  flex-shrink: 0;
  border: 0;
  background: var(--green);
  color: #fff;
  font-family: inherit;
  font-size: 12px;
  font-weight: 700;
  padding: 7px 14px;
  border-radius: 999px;
  cursor: pointer;
  transition: opacity 0.2s var(--ease), background-color 0.2s var(--ease), color 0.2s var(--ease);
}
.m-pill[disabled] { opacity: 0.45; cursor: default; }
.m-pill--ghost { background: var(--m-track); color: var(--fg); }

.m-foot {
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--dim);
  text-align: center;
  margin: 4px 8px 0;
}
.m-foot a { color: var(--green); text-decoration: none; }
.m-foot a:hover { text-decoration: underline; }

/* mobile: full-bleed sheet rather than a floating card */
@media (max-width: 639px) {
  .modal { padding: 0; }
  .modal-card {
    max-width: none;
    width: 100%;
    height: 100%;
    max-height: 100%;
    border: 0;
    border-radius: 0;
    box-shadow: none;
    transform: translateY(26px);
  }
  .modal.open .modal-card { transform: none; }
  .modal-head {
    padding-top: calc(15px + env(safe-area-inset-top, 0px));
    padding-left: calc(18px + env(safe-area-inset-left, 0px));
    padding-right: calc(16px + env(safe-area-inset-right, 0px));
  }
  .modal-scroll {
    padding-left: calc(16px + env(safe-area-inset-left, 0px));
    padding-right: calc(16px + env(safe-area-inset-right, 0px));
    padding-bottom: calc(22px + env(safe-area-inset-bottom, 0px));
  }
}

/* ── scrub bar ────────────────────────────────────────────────
   Lives INSIDE each .station element, positioned below the name.
   Layout: [current-time] [track (max 204px)] [total-time], centred.
   A chip label floats above the active segment; the thin playhead
   line shows exact position within the lit-up current section.
   Fades in/out with .station.active, just like the JY badge — but
   asymmetrically: hiding is quick and clean (the eye has already moved
   on to the incoming station), while showing eases in over the same
   duration as the slide so it arrives in step rather than snapping in
   ahead of it. ──── */
.station .scrub {
  position: absolute;
  top: 26px;    /* 18px name-half + 8px gap below name */
  left: 0;
  right: 0;
  opacity: 0;
  transition: opacity 0.15s var(--ease);
  pointer-events: none;
}

.station.active .scrub {
  opacity: 1;
  pointer-events: auto;
  transition: opacity calc(var(--dur) * 0.85) var(--ease);
}

/* Flex row — timestamps flank the track, whole row centred in the station */
.scrub-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding-top: 30px;    /* vertical space reserved for the chip above the track */
}

.scrub-time {
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.01em;
  color: var(--fg);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  min-width: 28px;
  transition: color var(--theme-dur, 0.45s) var(--ease);
}

.scrub-time-cur   { text-align: right; }
.scrub-time-total { text-align: left; }

/* Section chip — floats above the track, centred on the active segment */
.scrub-chip {
  background-color: var(--green);
  position: absolute;
  bottom: calc(100% + 12px);
  transform: translateX(-50%);
  padding: 2px 8px 3px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 500;
  letter-spacing: -0.04em;
  color: var(--bg);
  white-space: nowrap;
  pointer-events: none;
  z-index: 3;
  transition: color var(--theme-dur, 0.45s) var(--ease),
              border-color var(--theme-dur, 0.45s) var(--ease);
}

/* Grey twin shown above a hovered inactive segment (see scrub click/hover JS) */
.scrub-chip--hover {
  background-color: var(--scrub-chip-grey);
  z-index: 4;   /* above the active chip during the swap */
}
.scrub-chip.is-hidden { display: none; }

.scrub-track {
  position: relative;
  display: flex;
  gap: 4px;
  height: 4px;
  flex: 1;
  max-width: 204px;
}

.scrub-segment {
  flex-basis: 0;
  min-width: 0;
  position: relative;
  height: 4px;
  background: var(--dim);
  cursor: pointer;   /* click to jump to this segment's start */
  transition: background-color var(--theme-dur, 0.45s) var(--ease);
}
/* The bar is only 4px tall, so taps are fiddly. Extend the clickable/hover
   area well above and below with an invisible pseudo-element — it's part of the
   segment, so pointer events on it count as clicks on the segment, without
   changing how the 4px bar looks. (No overflow:hidden here, or it'd be clipped;
   the fill rounds its own corners via border-radius:inherit instead.) */
.scrub-segment::before {
  content: "";
  position: absolute;
  left: -2px;    /* extend into half of each 4px gap so adjacent hit areas tile
                    with no dead zone — keeps hover continuous across segments */
  right: -2px;
  top: -11px;
  bottom: -11px;
}
.scrub-segment:not(.scrub-segment--active):hover { background: var(--scrub-hover); }
/* the active segment is where you already are — not a seek target */
.scrub-segment--active { cursor: default; }

.scrub-segment--first {
  border-top-left-radius: 999px;
  border-bottom-left-radius: 999px;
}

.scrub-segment--last {
  border-top-right-radius: 999px;
  border-bottom-right-radius: 999px;
}

.scrub-fill {
  position: absolute;
  left: 0;
  top: 0;
  height: 100%;
  width: 0%;
  background: var(--green);
  border-radius: inherit;   /* match the segment's rounded ends (was clipped by overflow) */
  transition: background-color var(--theme-dur, 0.45s) var(--ease);
}

/* Transport head — a thin vertical line centred on the bar at the
   current playback position. margin-left / margin-top centre it. */
.scrub-head {
  position: absolute;
  width: 6px;
  height: 12px;
  margin-left: -1px;
  margin-top: -4px;    /* (12 − 4) / 2 — centres head on the 4px bar */
  border-radius: 1px;
  border-left: 2px solid var(--bg);
  border-right: 2px solid var(--bg);
  background: var(--fg);
  pointer-events: none;
  z-index: 2;
  left: 0;
  transition: background-color var(--theme-dur, 0.45s) var(--ease);
}


/* ── landscape guard (mobile browser + installed PWA) ──────────────
   The layout is tuned for a portrait phone column, so we cover landscape
   with a "rotate back" prompt rather than letting it break. Shown on any
   touch device in landscape — (hover: none) and (pointer: coarse) keep this
   off desktop, where landscape is the normal, supported orientation. The
   standalone rules below remain so older iOS (which has no orientation lock
   API and ignores the manifest's portrait lock) is covered there too. */
.rotate-lock {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 100;            /* above the info modal (modal sits below) */
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 18px;
  padding: 24px;
  text-align: center;
  background: var(--bg);
  color: var(--fg);
  font-family: "Space Grotesk", system-ui, sans-serif;
}
.rotate-lock p {
  margin: 0;
  max-width: 16em;
  font-size: 16px;
  font-weight: 500;
  line-height: 1.4;
  color: var(--dim);
}
.rotate-lock svg { color: var(--green); }

@media (orientation: landscape) and (hover: none) and (pointer: coarse) {
  .rotate-lock { display: flex; }
}
@media (orientation: landscape) {
  html.standalone .rotate-lock { display: flex; }
}
@media (display-mode: standalone) and (orientation: landscape) {
  .rotate-lock { display: flex; }
}

@media (prefers-reduced-motion: reduce) {
  .spine, .station, .station .name, .station .badge { transition-duration: 0.001s; }
  .ribbon, .toggle, .controls { transition-duration: 0.001s; }
  body.booting .toggle      { transform: translateX(-50%); }
  body.booting .controls    { transform: translateX(-50%); }
  body.booting .corner-ctrl { transform: none; }
  .corner-ctrl.intro        { transition-duration: 0.001s !important; }
  .modal-backdrop, .modal-card { transition-duration: 0.001s; }
  .station .scrub { transition-duration: 0.001s; }
}
