/* Metrobús live — minimalist control-board aesthetic.

   ─── design tokens ────────────────────────────────────────────────────
   Everything visual snaps to this small set of variables: ink colours,
   surface fills, line accents, the 4-px spacing grid, type ramp, radii,
   shadows, and motion. Components below should consume tokens, not raw
   values, so colour / type / spacing changes propagate from one place. */
:root {
  /* Surfaces */
  --bg: #f8f8f8;
  --bg-edge: #f0f0f0;
  --bg-card: #ffffff;
  --cdmx-fill: #eeeeee;
  --line-stroke: #969696;     /* lines + station off-state share this grey */
  --slice-off: #969696;
  --bus-fill: #1a1a1a;
  --btn-fill: #1a1a1a;

  /* Ink (text + iconography on light surfaces) */
  --ink-1: rgba(0, 0, 0, 0.92);   /* strong text — tooltip name, intro h1 */
  --ink-2: rgba(0, 0, 0, 0.78);   /* primary text — intro lead, line names */
  --ink-3: rgba(0, 0, 0, 0.55);   /* secondary text */
  --ink-4: rgba(0, 0, 0, 0.42);   /* tertiary text, hints */
  --ink-5: rgba(0, 0, 0, 0.45);   /* disabled / muted — bumped from 0.32 to clear WCAG 3:1 non-text contrast */
  --ink-border: rgba(0, 0, 0, 0.18);
  --ink-border-soft: rgba(0, 0, 0, 0.08);
  --ink-rule: rgba(0, 0, 0, 0.05);

  /* Accents */
  --live: #e63946;            /* live data dot */
  --live-glow: rgba(230, 57, 70, 0.35);
  --ready: #4f9e5b;           /* "listo" green */

  /* Alcaldías: a touch warmer than neutral grey, slightly more present */
  --alc-stroke: rgba(120, 100, 80, 0.14);

  /* Type ramp */
  --font-stack: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, system-ui, sans-serif;
  --fs-display: 22px;        /* intro h1 */
  --fs-body: 14px;           /* intro lead */
  --fs-small: 12.5px;        /* intro detail */
  --fs-header: 15px;         /* corner header */
  --fs-tooltip: 13px;        /* tooltip name */
  --fs-card: 13.5px;         /* line card name */
  --fs-card-meta: 11px;      /* line card role + intro status */
  --fs-meta: 11.5px;         /* intro button */
  --fs-tag: 9px;             /* tooltip line badge */
  --fs-hint: 10px;           /* intro hint */
  --fw-regular: 400;
  --fw-medium: 500;
  --fw-semibold: 600;
  --fw-bold: 700;

  /* Spacing — 4px grid */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 20px;
  --sp-6: 24px;
  --sp-7: 28px;
  --sp-8: 32px;
  --sp-9: 36px;

  /* Radii */
  --rad-sm: 4px;
  --rad-md: 6px;
  --rad-lg: 8px;
  --rad-pill: 999px;

  /* Shadows */
  --shadow-tooltip: 0 1px 2px rgba(0, 0, 0, 0.04), 0 4px 14px rgba(0, 0, 0, 0.08);
  --shadow-button:  0 1px 3px rgba(0, 0, 0, 0.05), 0 4px 12px rgba(0, 0, 0, 0.05);
  --shadow-panel:   0 1px 3px rgba(0, 0, 0, 0.05), 0 12px 32px rgba(0, 0, 0, 0.10);
  --shadow-medallion: 0 1px 2px rgba(0, 0, 0, 0.08);

  /* Motion */
  --ease-out: cubic-bezier(0.22, 0.61, 0.36, 1);
  --ease-in-out: cubic-bezier(0.45, 0.05, 0.55, 0.95);
  --dur-fast: 150ms;
  --dur-base: 220ms;
  --dur-slow: 700ms;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  /* Subtle radial vignette: very faint darkening at the edges. */
  background:
    radial-gradient(
      ellipse at center,
      var(--bg) 0%,
      var(--bg) 55%,
      var(--bg-edge) 100%
    );
  font-family: var(--font-stack);
  color: var(--ink-1);
  overflow: hidden;
}

#map {
  display: block;
  width: 100vw;
  height: 100vh;
  cursor: grab;
  /* Disable native browser gestures (page-scroll, double-tap-zoom) so
     d3.zoom can own pinch and pan unambiguously on touch devices. */
  touch-action: none;
}
#map:active { cursor: grabbing; }
/* The map carries tabindex=-1 so app.js can programmatically land focus
   here after the intro modal dismisses (instead of on a visible button,
   which flashed a focus ring on every page entry). Tab order skips it
   thanks to the negative tabindex, and we suppress any default outline
   for the rare case the SVG receives keyboard focus directly. */
#map:focus,
#map:focus-visible { outline: none; }

/* Live data row, top-left of viewport. The red dot is static now (no
   pulse) — restraint at rest, with a soft glow that signals "live". */
#header-corner {
  position: fixed;
  top: var(--sp-7);
  left: var(--sp-9, 30px);     /* 30px sits inside the 4px grid feel */
  pointer-events: none;
  user-select: none;
  z-index: 5;
}
.header-data {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  font-size: var(--fs-header);
  font-weight: var(--fw-medium);
  color: var(--ink-2);
  letter-spacing: 0.01em;
  font-feature-settings: "tnum" 1;
}
.header-sep {
  opacity: 0.4;
  margin: 0 2px;
}
.header-live-dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: var(--rad-pill);
  background: var(--live);
  /* Static glow ring — a single soft halo, no animation. */
  box-shadow:
    0 0 0 2px rgba(230, 57, 70, 0.10),
    0 0 6px 1px var(--live-glow);
  flex: none;
}

/* Secondary metadata row, stacked under the live-data row. Carries
   tempo (rolling 60s flash count, "32 ♩/min") and the time-of-day
   period (mañana / mediodía / tarde / noche / madrugada). Reads as
   secondary by typography (smaller, --ink-3) so it doesn't compete
   with the canonical live row above. Tabular numerals so the tempo
   doesn't jitter as it ticks. */
.header-meta {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin-top: 3px;
  font-size: var(--fs-meta);
  font-weight: var(--fw-regular);
  color: var(--ink-3);
  letter-spacing: 0.01em;
  font-feature-settings: "tnum" 1;
}
.header-meta-sep {
  opacity: 0.4;
  margin: 0 1px;
}
.header-tempo,
.header-period {
  white-space: nowrap;
}
.header-period {
  text-transform: lowercase;
  letter-spacing: 0.04em;
}
/* The musical-note glyph inside the tempo string sits a hair above
   the baseline — nudge it down so it visually centers in the row. */
.header-tempo .tempo-glyph {
  display: inline-block;
  font-size: 0.95em;
  margin-right: 1px;
  position: relative;
  top: -0.5px;
}

/* Mobile-only third row in #header-corner: the piece title rejoins
   the header when #wordmark-corner is hidden at narrow widths.
   Display:none by default so it doesn't render twice on desktop. */
.header-title-mobile { display: none; }

@media (max-width: 600px) {
  #header-corner  { top: var(--sp-5); left: var(--sp-5); }
  /* Mobile type scale: middle path between the original "small by
     design" editorial sizes and a fully accessible bump. Live row
     (the primary content — bus count + time) gets a small lift to
     14px so the numerals are easier on a sunny day. The secondary
     metadata (tempo + time-of-day) and the small typographic labels
     (mobile title row) revert to their original sizes — they're
     editorial labels and small type reads as deliberate there, not
     cramped. */
  .header-data    { font-size: 14px; gap: var(--sp-2); }
  .header-live-dot { width: 7px; height: 7px; }
  .header-meta    { font-size: 10.5px; margin-top: 2px; gap: 6px; }
  .header-title-mobile {
    display: block;
    margin-top: 4px;
    font-size: 9.5px;
    font-weight: var(--fw-semibold);
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: var(--ink-4);
  }
}

/* ─── Persistent piece identity ───────────────────────────────────────
   Two anchored corners that mirror the live-data block in the top-left
   and the audio cluster in the bottom-right — the four corners now all
   carry weight, so the live view reads as framed instead of empty.

   Both elements share the editorial vocabulary established by
   .intro-kicker / .intro-signature inside the intro modal: tracked
   uppercase, secondary/tertiary ink, hairline accents in --line-stroke.
   Hidden below 600px (the wordmark rejoins #header-corner via the
   .header-title-mobile row above; the signature lives in the about
   modal on phones). */
#wordmark-corner {
  position: fixed;
  top: var(--sp-7);
  right: var(--sp-9, 30px);
  pointer-events: none;
  user-select: none;
  z-index: 5;
  /* Right-aligned text block. Internal items align to the right edge
     so the rule sits flush with the wordmark below it. */
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 6px;
}
.wordmark-rule {
  width: 24px;
  height: 1px;
  background: var(--line-stroke);
  opacity: 0.5;
}
.wordmark-text {
  font-size: 10.5px;
  font-weight: var(--fw-semibold);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-4);
  line-height: 1;
}

#signature-corner {
  position: fixed;
  bottom: 22px;
  left: var(--sp-9, 30px);
  pointer-events: none;
  user-select: none;
  z-index: 5;
  /* Re-uses .intro-signature's hairline-on-either-side composition so
     the corner reads as the same family. Smaller and tighter — this is
     a corner anchor, not a title. */
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}
.signature-text {
  font-size: 9.5px;
  font-weight: var(--fw-semibold);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--ink-4);
  line-height: 1;
}
.signature-text::before,
.signature-text::after {
  content: "";
  display: inline-block;
  width: 14px;
  height: 1px;
  background: var(--line-stroke);
  opacity: 0.5;
  vertical-align: middle;
  margin: 0 6px 2px;
}

/* Below 600px: the wordmark moves into the header stack (via
   .header-title-mobile defined above), so its corner block is hidden.
   The signature stays — small but present in the bottom-left corner,
   matching the audio cluster on the bottom-right so the four-corner
   editorial frame survives onto mobile. Slightly tighter hairlines
   and a touch smaller type so it reads as a quiet anchor rather than
   competing with the score strip directly above. */
@media (max-width: 600px) {
  #wordmark-corner { display: none; }
  #signature-corner {
    /* Match the audio cluster's bottom anchor (18px) so signature
       and audio buttons share one baseline on the bottom edge. */
    bottom: 18px;
    left: var(--sp-4);     /* 16px gutter, matches mobile insets */
  }
  .signature-text {
    /* Tiny lift from 9px to 9.5px — keeps the editorial small-caps
       feel of a printed program signature while gaining a hair of
       readability outdoors. */
    font-size: 9.5px;
    letter-spacing: 0.20em;
  }
  .signature-text::before,
  .signature-text::after {
    width: 11px;
    margin: 0 5px 2px;
  }
}

/* Tooltip — line badges on top, station name below. Fades in/out
   instead of hard-toggling. */
#tooltip {
  position: fixed;
  pointer-events: none;
  background: var(--bg-card);
  color: var(--ink-1);
  padding: 7px 10px 8px;
  border-radius: var(--rad-sm);
  border: 1px solid var(--ink-rule);
  box-shadow: var(--shadow-tooltip);
  transform: translate(-50%, calc(-100% - 12px));
  white-space: nowrap;
  z-index: 10;
  font-feature-settings: "tnum" 1;
  /* Fade timing: the [hidden] attribute toggles display:none, so we use
     a separate .visible class to drive opacity transitions while still
     allowing display:none to remove the element from layout. */
  opacity: 0;
  transition: opacity 120ms var(--ease-out);
}
#tooltip.visible {
  opacity: 1;
}
.tt-tags {
  display: flex;
  gap: var(--sp-1);
  margin-bottom: 5px;
}
.tt-line-tag {
  font-size: var(--fs-tag);
  font-weight: var(--fw-bold);
  letter-spacing: 0.06em;
  color: #ffffff;
  /* Fallback grey only used if a lineId is missing from the lookup;
     darkened from #888 → #595959 so white text passes WCAG AA (~7:1). */
  background: #595959;
  padding: 1px 6px 2px;
  border-radius: 2px;
  line-height: 1.4;
  text-transform: uppercase;
}
.tt-name {
  font-size: var(--fs-tooltip);
  font-weight: var(--fw-medium);
  color: var(--ink-1);
  letter-spacing: 0.005em;
}
/* Tiny pointer below the tooltip */
#tooltip::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: -5px;
  transform: translateX(-50%) rotate(45deg);
  width: 8px;
  height: 8px;
  background: var(--bg-card);
  border-right: 1px solid var(--ink-rule);
  border-bottom: 1px solid var(--ink-rule);
}

/* Lines — uniform dark grey, no colour at rest. Colour only appears
   on the flashing station, not on the line itself. */
.line-group {
  opacity: 1;
  transition: opacity var(--dur-base) var(--ease-out);
}
.line-path {
  fill: none;
  stroke: var(--line-stroke);
  stroke-width: 1.4;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-opacity: 1;
  /* Slightly thicker invisible hit area so hover/tap on a 1.4px line
     stroke is forgiving without affecting visual width. */
  stroke-linejoin: round;
}
/* When a single line is being hovered (desktop only), dim the others.
   The hovered line itself gets .line-emphasized which keeps its
   opacity at 1; siblings inherit .line-dimmed semantics from the
   parent layer.  No effect on touch — pointer:fine guard below. */
@media (hover: hover) and (pointer: fine) {
  #layer-lines.line-hover-active .line-group { opacity: 0.32; }
  #layer-lines.line-hover-active .line-group.line-emphasized { opacity: 1; }
}

/* Stations: pie slices.
   Off-state: low opacity in line color. On-state: full opacity + bloom. */
.station-group { cursor: default; }
.station-hit {
  fill: rgba(0,0,0,0);  /* transparent hit target, slightly larger than slices */
  stroke: none;
  pointer-events: all;
}
/* Stations are TWO stacked paths per slice.
     .slice-base   — solid dark grey, always visible
     .slice-color  — line-coloured, fill-opacity 0 by default;
                     fades to 1 on flash (and adds the bloom).
   Animating opacity (rather than fill) is universally supported so the
   colour flash is reliable across browsers. */
.slice {
  pointer-events: none;
  stroke: none;
}
.slice-base {
  fill: var(--slice-off);
  fill-opacity: 1;
}
.slice-color {
  fill-opacity: 0;
  transform-box: fill-box;     /* transform-origin relative to the slice's own bbox */
  transform-origin: center;
  transform: scale(1);
  transition:
    fill-opacity 1500ms ease-out,
    filter      1500ms ease-out,
    transform   1500ms ease-out;
}
.slice-color.on {
  fill-opacity: 1;
  filter: url(#bloom);
  transform: scale(1.5);       /* pop outward 50% on flash, then shrink back */
  transition:
    fill-opacity 60ms ease-in,
    filter      60ms ease-in,
    transform   90ms ease-out;
}

/* Buses are tiny near-black dots that move along the lines. The grey
   fill makes them visually distinct from the station slices (which are
   filled in line colour on flash). */
.bus {
  fill: var(--bus-fill);
  stroke: none;
  pointer-events: none;
}

/* Alcaldía boundaries — present, but quiet. Slightly warm grey. */
.alc-border {
  fill: none;
  stroke: var(--alc-stroke);
  stroke-width: 0.5;
  vector-effect: non-scaling-stroke;
  pointer-events: none;
}

.cdmx-fill {
  fill: var(--cdmx-fill);
  stroke: none;
  pointer-events: none;
}

/* Make line / station / bus strokes resilient to zoom — they don't grow thicker */
.line-path,
.bus,
.slice {
  vector-effect: non-scaling-stroke;
}

/* ─── Intro modal ─────────────────────────────────────────────────────
   Covers the whole viewport until the user presses "Entrar" or hits
   Enter. Doubles as the loading screen: bus data finishes downloading
   while the user reads the copy. The content card has a subtle
   scale-in on first paint for a more crafted feel. */
#intro-modal {
  position: fixed;
  inset: 0;
  background: var(--bg);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 200;
  transition: opacity var(--dur-slow) var(--ease-out);
}
#intro-modal.fading {
  opacity: 0;
  pointer-events: none;
}

/* The content lives in a defined card now — white surface, hairline
   border, soft shadow — so the modal reads as an object on the page
   instead of free-floating text. Echoes the audio panel + tooltip
   surfaces so the modal sits in the same family. */
.intro-content {
  max-width: 480px;
  width: calc(100% - 48px);
  padding: 40px 40px 32px;
  text-align: center;
  background: var(--bg-card);
  border: 1px solid var(--ink-border-soft);
  border-radius: var(--rad-md);
  box-shadow: var(--shadow-panel);
  /* On-load entrance: fade + lift, then settle. */
  animation: intro-rise 800ms var(--ease-out) both;
}
@keyframes intro-rise {
  from {
    opacity: 0;
    transform: translateY(8px) scale(0.985);
  }
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

/* Kicker: small editorial label above the title. Echoes the header
   corner's "live" treatment — tracked uppercase, tertiary ink, with a
   tiny dot that mirrors the live-data dot on the map header. */
.intro-kicker {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: 10.5px;
  font-weight: var(--fw-semibold);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-4);
  margin-bottom: var(--sp-4);
}
.intro-kicker::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: var(--rad-pill);
  background: var(--live);
  box-shadow: 0 0 0 2px rgba(230, 57, 70, 0.10),
              0 0 4px 0 var(--live-glow);
  flex: none;
}

.intro-content h1 {
  font-size: 24px;
  font-weight: var(--fw-semibold);
  letter-spacing: 0.003em;
  color: var(--ink-1);
  line-height: 1.2;
  margin: 0;
}

/* Hairline rule under the h1 — a single short line that visually rhymes
   with the metro lines on the map: same neutral grey, slim, restrained. */
.intro-rule {
  width: 36px;
  height: 1px;
  background: var(--line-stroke);
  opacity: 0.5;
  margin: var(--sp-4) auto var(--sp-5);
}

/* Body paragraphs are siblings — same size, same weight, same colour
   so they read as a single piece of copy in two beats rather than a
   lead + caption hierarchy. */
.intro-body {
  font-size: 14px;
  line-height: 1.6;
  color: var(--ink-2);
  margin: 0 0 var(--sp-4) 0;
}
.intro-body:last-of-type {
  margin-bottom: var(--sp-7);
}

/* Author signature — echoes the .intro-kicker treatment at the top of
   the modal (tracked sans-serif, secondary-tertiary ink, centered) so
   the modal reads as framed by two editorial labels: kicker above the
   title, byline under the body copy. Hairlines on either side reuse
   the same --line-stroke as the .intro-rule under the h1 and the metro
   lines on the map — adds presence without introducing new tokens.
   Roman (not italic). Lowercase preserved as the author wrote it. */
.intro-signature {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-3);
  font-size: 10.5px;
  font-weight: var(--fw-semibold);
  letter-spacing: 0.18em;
  color: var(--ink-3);
  margin: calc(-1 * var(--sp-5)) 0 var(--sp-8) 0;
}
.intro-signature::before,
.intro-signature::after {
  content: "";
  width: 18px;
  height: 1px;
  background: var(--line-stroke);
  opacity: 0.5;
  flex: none;
}

/* Loader: an obvious horizontal bar with an animated 30%-wide segment
   that travels left-to-right while data is being fetched. When the
   modal flips to "ready", the bar fills 100% in green; when it flips
   to "error", it fills red. The bar replaces the previous tiny
   pulsing dot — clearer signal that the page is doing work. */
.intro-status {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-3);
  margin-bottom: var(--sp-6);
  color: var(--ink-3);
}
.intro-status-bar {
  width: 220px;
  height: 2px;
  background: var(--ink-rule);
  border-radius: 1px;
  overflow: hidden;
  position: relative;
}
.intro-status-bar-fill {
  position: absolute;
  top: 0;
  left: -30%;
  height: 100%;
  width: 30%;
  background: var(--ink-3);
  border-radius: 1px;
  animation: bar-slide 1.4s var(--ease-in-out) infinite;
  transition:
    left 400ms var(--ease-out),
    width 400ms var(--ease-out),
    background 200ms var(--ease-out);
}
@keyframes bar-slide {
  0%   { left: -30%; }
  100% { left: 100%; }
}
.intro-status.ready .intro-status-bar-fill {
  animation: none;
  left: 0;
  width: 100%;
  background: var(--ready);
}
.intro-status.ready .intro-status-text { color: var(--ready); }
.intro-status.error .intro-status-bar-fill {
  animation: none;
  left: 0;
  width: 100%;
  background: var(--live);
}
.intro-status.error .intro-status-text { color: var(--live); }

/* Smoothly cross-fade the status copy as it changes. The label is given
   .swap on update, which fades it out, swaps text, fades back. */
.intro-status-text {
  font-size: 13px;
  font-weight: var(--fw-medium);
  letter-spacing: 0.005em;
  color: var(--ink-3);
  transition: opacity 200ms var(--ease-out), color 200ms var(--ease-out);
  font-feature-settings: "tnum" 1;
}
.intro-status-text.swap { opacity: 0; }

/* Pill-shaped CTA: filled black when active, soft grey when waiting on
   data. The arrow nudges right on hover, the whole button lifts a hair
   on hover and settles on press — small but precise feedback. */
.intro-btn {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 13px 24px 13px 26px;
  font-size: 13.5px;
  font-weight: var(--fw-medium);
  letter-spacing: 0.005em;
  text-transform: none;
  border: 1px solid transparent;
  background: var(--ink-rule);
  color: var(--ink-4);
  border-radius: var(--rad-pill);
  cursor: not-allowed;
  font-family: inherit;
  transition:
    background var(--dur-base) var(--ease-out),
    color      var(--dur-base) var(--ease-out),
    border-color var(--dur-base) var(--ease-out),
    transform  var(--dur-fast) var(--ease-out),
    box-shadow var(--dur-base) var(--ease-out);
}
.intro-btn-label {
  line-height: 1;
}
.intro-btn-arrow {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: transform 220ms var(--ease-out), opacity 220ms var(--ease-out);
  opacity: 0.7;
}
.intro-btn:not([disabled]) {
  background: var(--btn-fill);
  color: #fff;
  border-color: var(--btn-fill);
  cursor: pointer;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06),
              0 6px 16px rgba(0, 0, 0, 0.10);
}
.intro-btn:not([disabled]) .intro-btn-arrow { opacity: 1; }
.intro-btn:not([disabled]):hover {
  background: #000;
  transform: translateY(-1px);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08),
              0 10px 22px rgba(0, 0, 0, 0.14);
}
.intro-btn:not([disabled]):hover .intro-btn-arrow {
  transform: translateX(3px);
}
.intro-btn:not([disabled]):active {
  transform: translateY(0);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06),
              0 4px 10px rgba(0, 0, 0, 0.10);
}
.intro-btn:focus-visible {
  outline: none;
  border-color: var(--ink-1);
  box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.10),
              0 6px 16px rgba(0, 0, 0, 0.10);
}

/* ─── Recent-notes score strip ────────────────────────────────────────
   Editorial running record of the last few station flashes. Rows insert
   at the top with a small slide-and-fade entrance, settle, and the
   bottom row drops out when the buffer is full. Reuses the medallion
   composition from .audio-line-num at smaller scale, and the kicker
   typography from .intro-kicker for the header label.

   Container behaviour:
   • desktop: docked left edge, vertically centered, ~200px wide.
   • mobile (≤600px): narrower (38vw, capped at 140px), pushed up so
     it sits in the upper-left third of the screen — leaves the lower
     half clear for map panning + the audio cluster bottom-right.
   • lines+stations layer (z-index 4 on map-overlay) sits below this.

   No background card — by sitting "on the map" rather than inside a
   chrome surface, the strip feels like part of the piece, not a UI
   panel. Each row carries its own subtle hairline above to delineate. */
#score-strip {
  position: fixed;
  top: 50%;
  left: var(--sp-7);
  transform: translateY(-50%);
  width: 200px;
  z-index: 6;
  pointer-events: none;
  user-select: none;
  /* The strip is a flex column so child alignment (kicker + rows) can
     be controlled with align-items. Without display:flex declared
     here, align-items on the mobile override below was a no-op and
     the kicker stayed left-aligned by default. */
  display: flex;
  flex-direction: column;
  /* Idle-state opacity is full when there's at least one row; the
     .quiescent class (toggled by app.js after >60s of silence) drops
     this to 0.5 so a stale strip recedes visually. */
  opacity: 1;
  transition: opacity var(--dur-base) var(--ease-out);
}
#score-strip.quiescent { opacity: 0.5; }
#score-strip.empty .score-kicker { opacity: 0.6; }
.score-kicker {
  font-size: 9.5px;
  font-weight: var(--fw-semibold);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-4);
  margin-bottom: 10px;
  padding-left: 2px;
  transition: opacity var(--dur-base) var(--ease-out);
}

.score-rows {
  display: flex;
  flex-direction: column;
  gap: 0;
  /* Fixed height — NOT min-height — so the score strip's bounding box
     is truly invariant regardless of how many rows are inside. With
     min-height the box could still grow if content exceeded the
     reservation; here we hard-cap the box and clip anything past it.
     Combined with the parent's translateY(-50%) centering, this is
     what keeps the kicker visually anchored when many stations fire
     at once.

     Sizing: 6 rows × ~48px (padding 9+9 + ~30px text-stack height) +
     5 × 1px borders ≈ 290px. Rounded up to 320px to absorb font
     metric and sub-pixel variation across browsers. Capacity in JS
     stays at 6 (SCORE_CAPACITY), so this height is never under-filled
     by overflow. The mobile override below uses its own fixed height
     since mobile caps at 4 rows. */
  height: 320px;
  overflow: hidden;
}

/* Each row: medallion (line ring) + stacked station name + role
   label. The hairline above each row (except the first) draws a soft
   horizontal rule the way a printed score has thin staff dividers.

   Subtle directional underglow: a soft darken anchored on the
   medallion side that fades to transparent at the far end. Lifts the
   station's identity off the page background without introducing a
   hard panel. On desktop the medallion sits on the LEFT of the row,
   so the gradient runs left→right (dark left, transparent right).
   The mobile override below flips the direction since rows are
   row-reverse on phones (medallion on the right). */
.score-row {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  /* Padding-left pulls the medallion away from the row's hard left
     edge so the gradient peak has somewhere to land that isn't flush
     against the strip boundary — without this, the gradient was
     reading as a sudden vertical band where the page background met
     the row, instead of an underglow centred on the circle. */
  padding: 9px 4px 9px 16px;
  border-top: 1px solid var(--ink-rule);
  /* Underglow: four-stop gradient with a fade-IN at the medallion
     side and a fade-OUT through the text. Both edges feather into
     the page background, so the row reads as a soft pool of darker
     tone behind the station rather than a hard rectangle. The peak
     (14%) is positioned to sit roughly under the medallion centre
     given the 16px padding-left and 22px medallion radius. */
  background: linear-gradient(
    to right,
    rgba(0, 0, 0, 0.000)  0%,
    rgba(0, 0, 0, 0.050) 14%,
    rgba(0, 0, 0, 0.035) 38%,
    rgba(0, 0, 0, 0.000) 100%
  );
  /* Entrance: slide down from -8px and fade in. The .entering class
     is added on insert and removed via rAF so the transition triggers. */
  opacity: 1;
  transform: translateY(0);
  transition:
    opacity var(--dur-base) var(--ease-out),
    transform var(--dur-base) var(--ease-out);
}
.score-row:first-child { border-top: none; }
.score-row.entering {
  opacity: 0;
  transform: translateY(-8px);
}
.score-row.leaving {
  opacity: 0;
  transform: translateY(6px);
}

/* Medallion — same vocabulary as .audio-line-num but at 22px so it
   reads as a satellite of the panel medallion, not a duplicate. The
   line colour comes from --line-color set inline by app.js. */
.score-medallion {
  width: 22px;
  height: 22px;
  border-radius: var(--rad-pill);
  background: #fff;
  flex: none;
  font-size: 9px;
  font-weight: var(--fw-bold);
  color: var(--ink-1);
  display: flex;
  align-items: center;
  justify-content: center;
  letter-spacing: 0.01em;
  font-feature-settings: "tnum" 1;
  box-shadow:
    inset 0 0 0 1.5px var(--line-color, var(--line-stroke)),
    var(--shadow-medallion);
}

.score-text {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.score-name {
  font-size: 12.5px;
  font-weight: var(--fw-medium);
  color: var(--ink-2);
  letter-spacing: 0.003em;
  /* Truncate long station names rather than wrap — keeps row height
     uniform so the strip reads as a clean score. The full name is
     still in the underlying .station-group tooltip. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Below the station name we render the line's instrument role
   ("melodía", "pedal A", "acorde", etc.) instead of a relative time.
   Italic + tertiary ink mirrors the .audio-line-role treatment in the
   audio panel, so the two surfaces speak the same vocabulary. */
.score-role {
  font-size: 10.5px;
  font-style: italic;
  color: var(--ink-4);
  letter-spacing: 0.015em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Repeat-count badge for the same-line/same-station retrigger case —
   collapses visual repetition when a busy line has back-to-back
   arrivals. Sits inline with the role label. */
.score-repeat {
  font-size: 10px;
  font-weight: var(--fw-semibold);
  font-style: normal;
  color: var(--ink-3);
  letter-spacing: 0.02em;
  margin-left: 4px;
  font-feature-settings: "tnum" 1;
}

@media (max-width: 600px) {
  #score-strip {
    /* Bottom-right placement on mobile. The audio cluster lives at
       bottom: 18px and is 38px tall, so its top edge is at
       viewport-bottom 56px. Anchoring the strip at bottom: 78px puts
       its bottom edge 22px above the cluster — generous breathing
       room so the row stack reads as resting near the buttons with
       deliberate padding, not stacked onto them. Combined with
       justify-content: flex-end on the .score-rows container below,
       the bottom-most row always sits at this anchor regardless of
       how many rows are visible.

       Right inset matches the audio cluster (18px) so the medallion
       column on this strip and the button column below it form one
       clean vertical edge against the viewport's right side.

       align-items: flex-end right-aligns the kicker, working only
       because the base #score-strip rule already declares display:flex. */
    top: auto;
    left: auto;
    bottom: 78px;
    right: 18px;
    transform: none;
    /* Width sized for the middle-path mobile type scale — at 12.5px
       station names need slightly more room than the original 190px
       allowed but less than the full 220px bump required. */
    width: min(200px, 62vw);
    align-items: flex-end;
  }
  .score-kicker {
    /* Reverted to base 9.5px — the "RECIÉN SONÓ" label is editorial
       and reads as deliberate at small size; only the row content
       (station name + role) needs the readability bump below. */
    font-size: 9.5px;
    padding-left: 0;
    /* Padding-right matches the row's padding-right (14px) so the
       end of "RECIÉN SONÓ" lines up vertically with the medallion's
       right edge below. Without this the kicker text reached 12px
       further right than the medallion column and read as misaligned. */
    padding-right: 14px;
    /* Pull the kicker tight to the row stack — the rows are
       bottom-anchored in .score-rows below, and any margin-bottom
       here adds visible whitespace between the label and the topmost
       row when the stack is full. 4px keeps the typographic boundary
       clean without floating the label off. */
    margin-bottom: 4px;
    /* Belt-and-suspenders: even if some other rule overrode the
       strip's flex alignment, text-align: right keeps the kicker
       label flush with the right edge of its block. */
    text-align: right;
  }
  /* Four rows max on mobile (was three) — slightly more room for the
     piece's running record without crowding the audio cluster below.
     Row padding is set in the .score-row block further down (6px
     vertical, was 7px) so 4 rows pack tightly in the container size
     declared next. */
  .score-rows .score-row:nth-child(n + 5) { display: none; }
  /* FIXED height (not min-height) for the same invariant-bbox reason
     as desktop: prevents the kicker from drifting as rows enter.
     Sized for the middle-path type scale: each row at 12.5px name +
     11px role + 22px medallion lands at ~46px tall, so 4 rows fill
     ~187px. Container at 190px gives just enough room without a
     visible buffer between kicker and topmost row.

     justify-content: flex-end pins the row stack to the BOTTOM of
     this container so the most recent activity stays visually
     adjacent to the audio buttons rather than floating mid-strip. */
  .score-rows {
    height: 190px;
    overflow: hidden;
    width: 100%;
    align-items: stretch;
    justify-content: flex-end;
  }
  /* Mirror the row on mobile-bottom-right: medallion sits on the RIGHT
     of each row (flush with the strip's right edge, in line with the
     audio buttons below), text reads from the right edge inward. The
     station name and role label are right-aligned so the strip feels
     anchored to the right column rather than left-floating with a
     ragged right edge. flex-direction: row-reverse swaps the visual
     order without touching the DOM order or the medallion/text classes.

     Padding-right pulls the medallion off the row's right edge for
     the same reason desktop has padding-left: gives the gradient peak
     room to land before the gradient hits the boundary.

     Underglow direction flips to right→left so the dark anchor stays
     under the medallion (now on the right) and fades out toward the
     leftmost edge of the row. Same four-stop shape as desktop. */
  .score-row {
    flex-direction: row-reverse;
    padding: 6px 14px 6px 0;
    gap: var(--sp-2);
    text-align: right;
    background: linear-gradient(
      to left,
      rgba(0, 0, 0, 0.000)  0%,
      rgba(0, 0, 0, 0.050) 14%,
      rgba(0, 0, 0, 0.035) 38%,
      rgba(0, 0, 0, 0.000) 100%
    );
  }
  .score-text {
    /* The text block was using its default cross-axis stretch; pin its
       items to the right so per-line content right-aligns inside the
       block too (otherwise short station names would float left of the
       block and read as misaligned). */
    align-items: flex-end;
    text-align: right;
  }
  /* Middle-path scale: only the user-facing CONTENT gets the bump
     (station name + role label + medallion numerals), not the
     editorial labels which kept their original "small by design"
     sizes above. The medallion box scales in proportion to its
     numerals so the ring still reads correctly around L1–L7. */
  .score-medallion { width: 22px; height: 22px; font-size: 9.5px; }
  .score-name { font-size: 12.5px; }
  .score-role { font-size: 11px; }
}

/* Reduced-motion: kill the slide; rows just appear/disappear. The
   media query at the bottom of the file already shortens transitions
   globally, but we explicitly null the transform so .entering doesn't
   leave rows offset. */
@media (prefers-reduced-motion: reduce) {
  .score-row.entering,
  .score-row.leaving { transform: none; }
}

/* ─── Edge-case overlay ───────────────────────────────────────────────
   Floats centred on the map. Used for "todas las líneas ocultas" when
   the user disables every line. Hidden by default; app.js toggles
   .visible. */
#map-overlay {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  z-index: 4;
  opacity: 0;
  transition: opacity 280ms var(--ease-out);
}
#map-overlay.visible { opacity: 1; }
#map-overlay .overlay-card {
  background: color-mix(in srgb, var(--bg-card) 80%, transparent);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  padding: var(--sp-4) var(--sp-6);
  border-radius: var(--rad-md);
  border: 1px solid var(--ink-rule);
  box-shadow: var(--shadow-tooltip);
  font-size: 12.5px;
  letter-spacing: 0.04em;
  color: var(--ink-3);
  text-align: center;
}

/* ─── Visible line legend (desktop) ──────────────────────────────────
   Vertical list of rows on the right side of the viewport. Each row
   is medallion + line name (avenue) + instrument role label — the
   full hamburger-panel vocabulary, surfaced as a permanent dock now
   that desktop has no panel to fall back to. Click anywhere on a row
   toggles the line's visibility + audio (wired in audio-panel.js).

   Right-side placement balances the score strip on the left — the
   live view reads as a triptych: score left, map centre, legend
   right. ~64px right inset leaves breathing room from the viewport
   edge. Hidden below 600px (phones use the hamburger panel). */
#legend-dock {
  position: fixed;
  top: 50%;
  right: 64px;
  transform: translateY(-50%);
  z-index: 6;
  /* Wrapper itself is transparent to pointer events so the map
     beneath stays hoverable in the gaps between rows. Without this
     the dock's full bounding box (a tall column on the right side)
     would intercept hover, blocking station tooltips for any station
     visually behind the dock. The row buttons override back to auto
     below so they remain interactive. */
  pointer-events: none;
  display: flex;
  flex-direction: column;
  align-items: stretch;
}
.legend-rows {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 4px;
  pointer-events: none;
}
/* Each row mirrors the .audio-line composition from the hamburger
   panel: medallion, then a stacked text block with avenue name on
   top and instrument role label below in italic. Implemented as a
   <button> for keyboard + screen-reader semantics since this is the
   primary line-control surface on desktop. */
.legend-row {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: 6px 10px 6px 6px;
  border: none;
  border-radius: var(--rad-md);
  cursor: pointer;
  font-family: inherit;
  text-align: left;
  /* Re-enable pointer events on the row buttons themselves — the
     dock wrapper sets pointer-events: none so map hover passes through
     in the gaps between rows. Each row still captures clicks here. */
  pointer-events: auto;
  /* Two-step background: semi-opaque card surface (var(--bg-card) at
     ~75%) tinted with a light wash of the line colour (~12%), so the
     row keeps a readable substrate when the user has zoomed into the
     map and station/line content sits behind it. The outer color-mix
     drops the whole result to ~75% opacity so the row still reads as
     subtle on the page's grey background — nothing about the rest
     state changes there. backdrop-filter softens whatever's behind so
     the line-coloured wash retains its identity even over busy content
     (matches the pattern already used for #map-overlay .overlay-card). */
  background-color: color-mix(
    in srgb,
    color-mix(in srgb, var(--line-color) 12%, var(--bg-card)) 75%,
    transparent
  );
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  transition:
    background-color var(--dur-base) var(--ease-out),
    transform var(--dur-fast) var(--ease-out);
}
.legend-row:hover {
  background-color: color-mix(
    in srgb,
    color-mix(in srgb, var(--line-color) 24%, var(--bg-card)) 85%,
    transparent
  );
  transform: translateX(-2px);
}
.legend-row:active { transform: translateX(0); }
.legend-row:focus-visible {
  outline: none;
  background-color: color-mix(
    in srgb,
    color-mix(in srgb, var(--line-color) 24%, var(--bg-card)) 85%,
    transparent
  );
  box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.10);
}
/* Muted: keep it visibly recessed but still readable when zoomed in.
   Drops the line-colour wash entirely and uses a near-transparent
   card surface so the row still reads as a row, just faded. */
.legend-row.muted {
  background-color: color-mix(in srgb, var(--bg-card) 50%, transparent);
}
/* Medallion: 36px on desktop now that it sits inside a row that also
   carries text. The ring is line-coloured and the label is L1–L7 on
   a white surface, same vocabulary as before but slightly smaller
   to give the avenue name typographic primacy alongside it. */
.legend-medallion {
  width: 36px;
  height: 36px;
  border-radius: var(--rad-pill);
  background: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  font-weight: var(--fw-bold);
  color: var(--ink-1);
  letter-spacing: 0.01em;
  font-feature-settings: "tnum" 1;
  flex: none;
  box-shadow:
    inset 0 0 0 2.5px var(--line-color),
    var(--shadow-medallion);
  transition:
    background var(--dur-base) var(--ease-out),
    color var(--dur-base) var(--ease-out),
    box-shadow var(--dur-base) var(--ease-out);
}
.legend-row.muted .legend-medallion {
  background: transparent;
  color: var(--ink-3);
  box-shadow: inset 0 0 0 1.5px var(--line-color);
}
.legend-text {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}
.legend-name {
  font-size: var(--fs-card);          /* 13.5px */
  font-weight: var(--fw-medium);
  color: var(--ink-2);
  letter-spacing: 0.003em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  transition: color var(--dur-base) var(--ease-out);
}
.legend-role {
  font-size: var(--fs-card-meta);     /* 11px */
  font-style: italic;
  color: var(--ink-3);
  letter-spacing: 0.015em;
  transition: color var(--dur-base) var(--ease-out);
}
.legend-role-glyph {
  display: inline-block;
  margin-right: 5px;
  color: var(--line-color);
  font-style: normal;
  letter-spacing: 0;
  opacity: 0.85;
}
.legend-row.muted .legend-name,
.legend-row.muted .legend-role { color: var(--ink-5); }
.legend-row.muted .legend-role-glyph { opacity: 0.4; }

/* On narrower desktop widths (< ~900px), tighten the right inset and
   step the row padding down so 7 stacked rows still leave breathing
   room above and below. Below 600px we hide the dock entirely
   (phones still use the hamburger panel). */
@media (max-width: 900px) {
  #legend-dock { right: 36px; }
  .legend-row { padding: 5px 8px 5px 5px; }
  .legend-medallion { width: 32px; height: 32px; font-size: 11px; }
  .legend-name { font-size: 12.5px; }
  .legend-role { font-size: 10.5px; }
}
@media (max-width: 600px) {
  #legend-dock { display: none; }
}

/* ─── Hamburger button — desktop hide ─────────────────────────────────
   On desktop the legend dock is the canonical line-control surface
   (each medallion is a clickable toggle), so the hamburger button
   that previously opened the same controls in a panel is redundant.
   Hiding it on viewports >600px declutters the bottom-right cluster.
   The audio panel itself is also hidden on desktop as belt-and-braces
   in case any code path tries to open it. Below 600px both stay
   visible, since the legend dock is the one that's hidden there. */
@media (min-width: 601px) {
  /* !important is required here because the grouped selector
     #about-btn, #audio-mute-btn, #audio-toggle { display: flex; }
     is declared further down in this file. Without !important, source
     order would let the grouped rule win on tied specificity (1,0,0)
     and the hamburger would still render. */
  #audio-toggle { display: none !important; }
  #audio-panel { display: none !important; }
}

/* One-time pulse on the hamburger button to draw attention to the
   line settings on first session entry. Two soft scale pulses, then
   the class is removed and never returns (state in localStorage,
   keyed in audio-panel.js). Skipped under prefers-reduced-motion. */
@keyframes ham-attention-pulse {
  0%, 100% { transform: scale(1); }
  20%      { transform: scale(1.08); }
  40%      { transform: scale(1); }
  60%      { transform: scale(1.08); }
  80%      { transform: scale(1); }
}
#audio-toggle.intro-pulse {
  animation: ham-attention-pulse 2.6s var(--ease-in-out) 1;
}
@media (prefers-reduced-motion: reduce) {
  #audio-toggle.intro-pulse { animation: none; }
}

/* ─── Recenter pill ──────────────────────────────────────────────────
   Appears only after the user has zoomed or panned away from the
   default fit (JS toggles the [hidden] attribute and a .visible class
   for the fade). Click resets the zoom transform back to identity.

   Placement:
   • desktop: bottom-left, just above the signature corner so it sits
     on the same vertical column as the editorial frame.
   • mobile: same bottom-left position, but offset upward so it clears
     the score strip on the upper-left and doesn't fight the audio
     cluster on the right. */
#recenter-btn {
  position: fixed;
  bottom: 50px;
  left: var(--sp-7);
  z-index: 7;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 7px 13px 7px 11px;
  font-family: inherit;
  font-size: 11.5px;
  font-weight: var(--fw-medium);
  letter-spacing: 0.04em;
  color: var(--ink-3);
  background: var(--bg-card);
  border: 1px solid var(--ink-border-soft);
  border-radius: var(--rad-pill);
  box-shadow: var(--shadow-button);
  cursor: pointer;
  /* JS adds [hidden] for layout removal + .visible for the fade-in.
     We use both so the pill can fade out before display:none clicks. */
  opacity: 0;
  transform: translateY(4px);
  transition:
    opacity var(--dur-base) var(--ease-out),
    transform var(--dur-base) var(--ease-out),
    color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out);
}
#recenter-btn[hidden] { display: none; }
#recenter-btn.visible {
  opacity: 1;
  transform: translateY(0);
}
#recenter-btn:hover {
  color: var(--ink-1);
  border-color: var(--ink-border);
}
#recenter-btn:focus-visible {
  outline: none;
  color: var(--ink-1);
  border-color: var(--ink-border);
  box-shadow:
    0 0 0 2px rgba(0, 0, 0, 0.08),
    var(--shadow-button);
}
.recenter-label { line-height: 1; }

@media (max-width: 600px) {
  #recenter-btn {
    bottom: 70px;
    left: var(--sp-4);
    font-size: 11px;
    padding: 6px 11px 6px 9px;
  }
}

/* ─── Audio control (jazz mode) ───────────────────────────────────────
   Two circular floating buttons + a settings panel that opens above. */
#audio-control {
  position: fixed;
  bottom: 18px;
  right: 18px;
  z-index: 20;
  font-family: inherit;
}

.audio-buttons {
  display: flex;
  gap: var(--sp-2);
}
#about-btn,
#audio-mute-btn,
#audio-toggle {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  border-radius: var(--rad-pill);
  border: 1px solid var(--ink-border-soft);
  background: var(--bg-card);
  color: rgba(0, 0, 0, 0.62);
  cursor: pointer;
  box-shadow: var(--shadow-button);
  transition:
    color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out),
    background var(--dur-fast) var(--ease-out),
    transform var(--dur-fast) var(--ease-out);
  padding: 0;
  position: relative;
}
#about-btn:hover,
#audio-mute-btn:hover,
#audio-toggle:hover {
  color: var(--ink-1);
  transform: translateY(-1px);
}
#about-btn:active,
#audio-mute-btn:active,
#audio-toggle:active { transform: translateY(0); }
#audio-toggle.expanded {
  color: var(--ink-1);
  border-color: var(--ink-border);
}
/* About button "active" state when its modal is open — mirror the
   hamburger's .expanded treatment so the button family stays
   visually consistent across the cluster. */
#about-btn.active {
  color: var(--ink-1);
  border-color: var(--ink-border);
}
/* Keyboard focus ring — replace the browser default (a hard black
   outline that's especially visible after the intro modal dismisses
   and programmatically lands focus on #audio-mute-btn) with a soft
   2px ring matching the rest of the design language (intro-btn,
   close-×, etc.). Only :focus-visible, so mouse clicks don't show it. */
#about-btn:focus-visible,
#audio-mute-btn:focus-visible,
#audio-toggle:focus-visible {
  outline: none;
  color: var(--ink-1);
  border-color: var(--ink-border);
  box-shadow:
    0 0 0 2px rgba(0, 0, 0, 0.08),
    var(--shadow-button);
}

/* When sound is muted, the note button: dims the icon, shows a
   diagonal slash across the circle. */
#audio-mute-btn.muted { color: var(--ink-5); }
#audio-mute-btn.muted::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 18%;
  right: 18%;
  height: 1.5px;
  background: rgba(0, 0, 0, 0.55);
  transform-origin: center;
  transform: translateY(-50%) rotate(-32deg);
  border-radius: 1px;
}

/* Music-note ambient pulse: while audio is on (not muted), the icon
   inside the button breathes gently. Stops when muted or audio
   suspended. The pulse is on the inner SVG so the button surface
   itself doesn't move. */
#audio-mute-btn.audible svg {
  animation: note-breathe 2.4s var(--ease-in-out) infinite;
  transform-origin: center;
}
@keyframes note-breathe {
  0%, 100% { transform: scale(1);    opacity: 0.92; }
  50%      { transform: scale(1.06); opacity: 1;    }
}

/* Hamburger morph — three lines that fold into an × when the panel
   opens. The icon SVG has three <line> elements (top/middle/bottom)
   so we can animate each independently. */
#audio-toggle .ham-line {
  transition:
    transform 260ms var(--ease-out),
    opacity   180ms var(--ease-out);
  transform-origin: center;
  transform-box: fill-box;
}
#audio-toggle.expanded .ham-line.top {
  transform: translateY(5px) rotate(45deg);
}
#audio-toggle.expanded .ham-line.mid {
  opacity: 0;
}
#audio-toggle.expanded .ham-line.bot {
  transform: translateY(-5px) rotate(-45deg);
}

#audio-panel {
  position: absolute;
  bottom: 48px;
  right: 0;
  width: 320px;
  background: var(--bg-card);
  border: 1px solid var(--ink-border-soft);
  border-radius: var(--rad-lg);
  box-shadow: var(--shadow-panel);
  padding: 14px var(--sp-3);
  font-size: 12px;
  /* Entrance: small lift + fade. The [hidden] attribute drives the
     display flip; the .visible class sequences opacity/transform
     on top of that. */
  opacity: 0;
  transform: translateY(6px) scale(0.985);
  transition:
    opacity 200ms var(--ease-out),
    transform 200ms var(--ease-out);
}
#audio-panel.visible {
  opacity: 1;
  transform: translateY(0) scale(1);
}

.audio-lines {
  display: flex;
  flex-direction: column;
  gap: 2px;
}
/* Each row is a colour-tinted card with a circular line medallion
   on the left, the avenue name + the line's musical role stacked,
   and a subtle line-coloured underline that ties it to the route on
   the map. Click anywhere to disable: medallion hollows, colour wash
   drains, text fades, and the line vanishes from the map. */
.audio-line {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: 9px var(--sp-3);
  border-radius: var(--rad-md);
  cursor: pointer;
  user-select: none;
  background: color-mix(in srgb, var(--line-color) 6%, transparent);
  transition:
    background var(--dur-base) var(--ease-out),
    opacity    var(--dur-base) var(--ease-out);
}
.audio-line:hover {
  background: color-mix(in srgb, var(--line-color) 13%, transparent);
}
.audio-line.muted {
  background: transparent;
}

/* Medallion: white circle with a thick line-coloured ring and a dark
   "L1", "L2", … label inside. We previously filled the medallion in
   the line colour and stamped white over the top, but several official
   Metrobús colours (L4 orange #FF9A03 → 2.13:1; L3 lime #7A9A01 →
   3.26:1; L6 pink #E44599 → 3.74:1) fail WCAG 4.5:1 contrast against
   white. Putting the colour on the *ring* preserves line identity
   while the label rides on a clean ~18:1 surface. This also matches
   the "colour only appears on accent" design philosophy already
   established by the line strokes (uniform grey at rest). */
.audio-line-num {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: var(--rad-pill);
  background: #fff;
  color: var(--ink-1);
  font-size: 11.5px;
  font-weight: var(--fw-bold);
  letter-spacing: 0.01em;
  font-feature-settings: "tnum" 1;
  flex: none;
  box-shadow:
    inset 0 0 0 2px var(--line-color),
    var(--shadow-medallion);
  transition:
    background var(--dur-base) var(--ease-out),
    color      var(--dur-base) var(--ease-out),
    box-shadow var(--dur-base) var(--ease-out),
    opacity    var(--dur-base) var(--ease-out);
}
.audio-line.muted .audio-line-num {
  background: transparent;
  color: var(--ink-3);
  box-shadow: inset 0 0 0 1.5px var(--line-color);
}

.audio-line-text {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}
.audio-line-name {
  font-size: var(--fs-card);
  font-weight: var(--fw-medium);
  color: var(--ink-2);
  letter-spacing: 0.003em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  transition: color var(--dur-base) var(--ease-out);
}
.audio-line-role {
  font-size: var(--fs-card-meta);
  font-style: italic;
  color: var(--ink-3);
  letter-spacing: 0.015em;
  transition: color var(--dur-base) var(--ease-out);
}
.audio-line-role-glyph {
  display: inline-block;
  margin-right: 5px;
  color: var(--line-color);
  font-style: normal;
  letter-spacing: 0;
  opacity: 0.85;
}
.audio-line.muted .audio-line-name,
.audio-line.muted .audio-line-role {
  color: var(--ink-5);
}
.audio-line.muted .audio-line-role-glyph {
  opacity: 0.4;
}

/* ─── About modal (colofón) ───────────────────────────────────────────
   Floating card opened by the ⓘ button in the bottom-right cluster.
   Unlike the intro modal (which is a full-screen takeover during
   loading), this modal sits above a translucent + blurred backdrop so
   the city stays visible behind it — muting is a separate action,
   reading the credits shouldn't pause the piece. Two-step show/hide:
   the [hidden] attribute drives display, .visible drives opacity +
   transform on top so we can transition in/out. */
#about-modal {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 150;
  /* Padding ensures the card never butts against the viewport edges
     and gives mobile a safe gutter when the card is at full responsive
     width. */
  padding: var(--sp-4);
}
/* The [hidden] attribute relies on the UA rule [hidden]{display:none},
   which loses to the author rule above. Re-assert display:none here
   so flipping modal.hidden in JS actually removes the element from
   layout and stops it eating clicks meant for the floating buttons. */
#about-modal[hidden] {
  display: none;
}
.about-backdrop {
  position: absolute;
  inset: 0;
  background: color-mix(in srgb, var(--bg) 55%, transparent);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-out);
}
#about-modal.visible .about-backdrop { opacity: 1; }

.about-content {
  position: relative;
  max-width: 480px;
  width: 100%;
  /* Constrain to viewport with internal scroll if copy ever gets
     longer than the available height. Mobile-safe. */
  max-height: calc(100vh - 2 * var(--sp-4));
  overflow-y: auto;
  padding: 36px 36px 28px;
  background: var(--bg-card);
  border: 1px solid var(--ink-border-soft);
  border-radius: var(--rad-md);
  box-shadow: var(--shadow-panel);
  text-align: center;
  opacity: 0;
  transform: translateY(8px) scale(0.985);
  transition:
    opacity var(--dur-base) var(--ease-out),
    transform var(--dur-base) var(--ease-out);
}
#about-modal.visible .about-content {
  opacity: 1;
  transform: translateY(0) scale(1);
}

/* Tighten padding on phones — 36px gutters waste real estate when the
   card is already narrow. */
@media (max-width: 600px) {
  .about-content { padding: 32px var(--sp-5) 24px; }
}

/* × in the top-right of the card. Subtle by default — fades to full
   ink on hover. Round hit area sized for touch. */
.about-close-x {
  position: absolute;
  top: var(--sp-3);
  right: var(--sp-3);
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: none;
  border-radius: var(--rad-pill);
  color: var(--ink-4);
  cursor: pointer;
  padding: 0;
  transition:
    color var(--dur-fast) var(--ease-out),
    background var(--dur-fast) var(--ease-out);
}
.about-close-x:hover {
  color: var(--ink-1);
  background: var(--ink-rule);
}
.about-close-x:focus-visible {
  outline: none;
  color: var(--ink-1);
  box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.10);
}

/* Top kicker — same family as the intro-kicker but without the live
   dot since this modal isn't about the live feed. Pure typographic
   label, no extra ornament. */
.about-kicker {
  display: inline-block;
  font-size: 10.5px;
  font-weight: var(--fw-semibold);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-4);
  margin-bottom: var(--sp-5);
}

/* Section kickers — slightly smaller and a touch more muted than the
   top kicker so the four sections read as sub-headers rather than
   four equal title-blocks. The top margin gives section breathing
   room now that the hairline rules between sections are gone. */
.about-section-kicker {
  font-size: 9.5px;
  font-weight: var(--fw-semibold);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-4);
  margin: var(--sp-8) 0 var(--sp-3);
}

.about-body {
  font-size: 13.5px;
  line-height: 1.65;
  color: var(--ink-2);
  margin: 0;
  text-align: left;
  /* Body copy is left-aligned for readability while the rest of the
     modal is centered. The asymmetry keeps section headers
     editorial-feeling without making prose lines hard to scan. */
}
/* Inline links inside body copy — subtle hairline underline, same
   ink as surrounding text by default, deepens on hover. Same vocab
   as the author-links below, but inheriting body color so the link
   doesn't break the prose colour. */
.about-body a {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px solid var(--ink-border-soft);
  padding-bottom: 1px;
  transition:
    color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out);
}
.about-body a:hover {
  color: var(--ink-1);
  border-bottom-color: var(--ink-border);
}
.about-body a:focus-visible {
  outline: none;
  color: var(--ink-1);
  border-bottom-color: var(--ink-1);
}

/* Author block at the bottom of the modal. Name lowercase to match the
   intro-signature treatment; links are a single inline row of small
   labels separated by middots. */
.about-author {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-2);
  margin-top: var(--sp-6);
}
.about-author-name {
  font-size: 13.5px;
  font-weight: var(--fw-medium);
  color: var(--ink-2);
  letter-spacing: 0.005em;
}
.about-author-links {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: var(--sp-2);
  font-size: 12px;
  color: var(--ink-3);
}
.about-author-links a {
  color: var(--ink-3);
  text-decoration: none;
  border-bottom: 1px solid var(--ink-rule);
  padding-bottom: 1px;
  transition:
    color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out);
}
.about-author-links a:hover {
  color: var(--ink-1);
  border-bottom-color: var(--ink-border);
}
.about-author-links a:focus-visible {
  outline: none;
  color: var(--ink-1);
  border-bottom-color: var(--ink-1);
}
.about-link-sep {
  color: var(--ink-4);
  user-select: none;
}

/* Reduced-motion: kill all entrance & ambient animations. Functional
   transitions (opacity for tooltips, modals dismiss) stay on but
   shortened. */
@media (prefers-reduced-motion: reduce) {
  .intro-content { animation: none; }
  #audio-mute-btn.audible svg { animation: none; }
  .intro-status-bar-fill { animation: none; left: 0; width: 100%; opacity: 0.6; }
  .about-content { transform: none !important; }
  *, *::before, *::after {
    transition-duration: 80ms !important;
  }
}
