/* =============================================================================
 * app.css - TriviaWorks Capture - car-first show capture player.
 *
 * Form: night-drive media controls with show-led album art, big glance targets,
 * restrained dark surfaces, and TriviaWorks stage-gold as the main accent.
 *
 * Landscape (horizontal) is a first-class layout - for an in-car phone mount:
 *   - the player becomes a two-column "now playing" (art left, controls right);
 *   - the bottom tab bar becomes a slim left rail to reclaim vertical space.
 *
 * Every class/id the JS touches is preserved (the play-ring SVG is kept in the
 * DOM and simply hidden; #progressFill carries the playhead knob).
 * ========================================================================== */
:root {
  /* ── METRO, ADOPTED ────────────────────────────────────────────────────────
     Every name below is KEPT so the ~900 lines of selectors underneath keep
     working untouched; only what each name RESOLVES TO changes. That is the
     decompose-not-rewrite route: the design language moves, the layout and the
     capabilities do not.

     The app stamps data-theme="dark" on <html>, so the Metro dark palette
     applies directly and this surface needs no theme logic of its own.

     ⚠ NOTHING here may be a colour literal. If a value below is ever a hex
     again, the Studio has two palettes and this file is one of them. */
  --bg:        var(--m-ground);
  --bg-grad:   var(--m-tile);      /* retained name; the gradients are gone */
  --elev:      var(--m-tile);
  --elev-2:    var(--m-tile-2);
  --elev-3:    var(--m-tile-open);
  --gold:      var(--m-accent);
  --gold-hi:   var(--m-accent-2);
  --gold-dim:  var(--m-accent);
  /* ★ AN ACCENT HAS THREE VALUES, NOT ONE. --gold is the FILL; --gold-text is
     the accent used as TYPE. They resolve identically on this dark-only
     surface today, which is exactly why the distinction has to be made HERE
     rather than discovered later: the moment any car surface renders on a
     light ground, a fill value used as text fails contrast outright. */
  --gold-text: var(--m-accent-text);
  --ink:       var(--m-ink);
  --ink-soft:  var(--m-ink-2);
  --ink-faint: var(--m-ink-3);
  --line:      var(--m-edge);
  --line-2:    var(--m-edge);
  --ok:        var(--m-ok);
  --warn:      var(--m-warn);
  --error:     var(--m-err);
  --blue:      var(--m-knowledge);
  --red:       var(--m-err);
  --amber:     var(--m-warn);
  /* Metro has NO rounded corners. Kept as names rather than deleted so a
     future edit that reaches for --r gets zero instead of reintroducing one. */
  --r-sm: 0;
  --r:    0;
  --r-lg: 0;
  --pad:  var(--m-pad);
  /* ★ CAR GLANCE TARGET. 56px, comfortably past the 44px floor, because this
     is read and hit at arm's length in motion. Metro has no touch-target
     token at all - that is a GAP in the language, logged as such, not a
     deviation: a minimum size is not a colour or a shape rule and nothing in
     Metro forbids it. */
  --tap:  56px;
  --font: var(--m-sans);
  --show-art-url: none;
}


* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
html, body {
  margin: 0; padding: 0;
  height: 100%;
  background-color: var(--bg);
  color: var(--ink);
  font-family: var(--font);
  overscroll-behavior: none;
  user-select: none;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
body {
  display: flex; flex-direction: column;
  min-height: 100vh; min-height: 100dvh;
  padding-top: env(safe-area-inset-top);
  /* room for the fixed bottom tab bar (portrait) */
  padding-bottom: calc(68px + env(safe-area-inset-bottom));
  /* subtle top tint over the solid near-black base (no background-attachment:
     fixed - it is buggy on iOS Safari and can leave white bleed) */
  background-image: var(--m-tile);
  background-repeat: no-repeat;
}
body::before { content: none; }  /* Metro: the ground is flat and unlit. */
body::after { content: none; }   /* Metro: no scrim over a flat ground. */

/* ---- topbar (slim, flat) ---- */
.topbar {
  /* ★ A long status chip ("WAKE-LOCK DENIED") overran the wordmark and clipped
     "Capture" mid-word. The bar is brand-then-status with the brand winning:
     the status may shrink and ellipsise, the identity may not. */
  /* At 375px a long status ("WAKE-LOCK DENIED" + "STREAMING") cannot share a
     line with the wordmark - ellipsising it hides WHICH state is wrong, which
     is the only thing a status chip is for. So the bar WRAPS: identity keeps
     the first line, status takes the second and stays fully readable. */
  /* ★ ONE LINE (Andrew, 2026-08-28). An earlier fix let this bar WRAP so a
     long status ("WAKE-LOCK DENIED") could not clip the wordmark. Wrapping
     solved the clipping and cost a whole line of vertical space on every
     screen, for a state that is usually two short chips. The bar now holds one
     line and the STATUS shrinks instead: smaller type and tighter padding at
     phone width, so even the longest status fits beside the mark. */
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: nowrap; gap: 8px;
  padding: 12px var(--pad) 10px;
  position: sticky; top: 0; z-index: 10;
  /* ★ HARMONISED WITH THE HOST HEADER (2026-09-14). Andrew: "make sure that the
     Capture Tool is wholly harmonised design-wise with the Host and Player
     surfaces". The host's .host-header is a TILE bar ruled off the ground by a
     1px edge; this bar was transparent, so scrolled show art slid visibly under
     the wordmark and the two surfaces opened on different chrome. Same two
     tokens as the host, not copied values. */
  background: var(--m-tile);
  border-bottom: 1px solid var(--m-edge);
}
.brand {
  /* Reads brand-then-product, with the product name subordinate: smaller,
     muted, and divided by a hairline rather than a gap.
     ★ WHEN THE BAR IS TIGHT, SOMETHING HAS TO YIELD, AND IT IS NOT THE
     IDENTITY. Forcing one line without saying what gives clipped "Capture"
     mid-word behind a long status. So the wordmark never shrinks, and the
     PRODUCT NAME ellipsises and then disappears - a truncated product label is
     survivable, a truncated identity is not, and a wrapped bar costs a line on
     every screen for a state that is usually two short chips. */
  display: flex; align-items: baseline; gap: 8px; min-width: 0; flex: 0 1 auto;
}
.brand-mark { flex: 0 0 auto; }
/* ★ ONE COLOUR, ALL WHITE (Andrew, 2026-09-14: "White on capture AND host AND
   player"). The mark wears tw-brand--stage, as host.html's header does, and
   the shared tw-brand.js module draws that modifier as ONE white tone. The
   gold-WORKS venue lockup adopted earlier the same day is REVERTED, and so is
   the older local ink override: the colour is ruled in exactly one home
   (tw-brand.js), so nothing here restates it. */
.brand-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
@media (max-width: 400px) {
  .brand-name { display: none; }
}
.brand-mark {
  /* ★ Was a filled accent tile reading "TW" - a local invention, and the last
     accent slab left in the header once the show tiles were fixed. Capture was
     the only surface not using the ONE canonical wordmark (tw-brand.js, Track A
     2026-06-10): thin TRIVIA + bold WORKS, which under data-theme="dark" - set
     on this page - renders ink TRIVIA and accent WORKS. The accent is spent on
     half a wordmark instead of a block, which is the same correction applied to
     the show tiles. Size only here; the module owns weight and colour, so the
     mark stays identical across surfaces. */
  font-size: 12px; letter-spacing: .2px; line-height: 1;
  background: none; padding: 0; border-radius: 0; white-space: nowrap;
}
.brand-name {
  /* Micro, uppercase, tracked: the host labels its chrome this way
     (.quiz-name, the login labels). Was an off-ladder 600 13px mixed case. */
  font: var(--m-micro); letter-spacing: var(--m-track); text-transform: uppercase;
  color: var(--ink-soft);
  padding-left: 8px; border-left: 1px solid var(--line); white-space: nowrap;
}
.status {
  display: flex; gap: 6px; min-width: 0; flex: 0 1 auto; justify-content: flex-end;
  flex-wrap: nowrap;
}
.status .m-chip {
  white-space: nowrap;
  /* ★ AS THE HOST'S STATUS BADGE (2026-09-14): a tile with a 1px edge, micro
     type. They were borderless ground chips that vanished on the new tile bar. */
  background: var(--m-tile-2); border: 1px solid var(--m-edge);
}
@media (max-width: 460px) {
  /* the only place the two must compete for room */
  /* Micro stays 11px, as on the host at 390px: the phone yields PADDING and
     tracking, never drops below the micro step (it was 10px, off the ladder). */
  .status .m-chip { padding: 3px 6px; letter-spacing: .06em; }
  .brand-mark { font-size: 11px; }
  .brand-name { padding-left: 6px; }
}


.pill::before {
  content: ""; width: 6px; height: 6px; border-radius: 0;
  background: var(--ink-faint); flex: 0 0 auto;
}
.pill.ok   { color: var(--ok);   background: var(--m-ok-bg); }
.pill.ok::before   { background: var(--ok); }
.pill.warn::before { background: var(--warn); }

/* ---- views ---- */
.view {
  display: none; flex: 1; flex-direction: column;
  padding: 4px var(--pad) var(--pad); gap: 16px; overflow-y: auto;
}
.view.active { display: flex; animation: viewIn .28s cubic-bezier(.22,.61,.36,1); }
@keyframes viewIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
@media (prefers-reduced-motion: reduce) { .view.active { animation: none; } * { scroll-behavior: auto; } }

/* =============================================================================
 * EPISODES - flat Spotify-style list rows
 * ========================================================================== */
/* ---- Shows browse view ---- */
.shows-head { padding: 6px 0 4px; }
.shows-title { font-size: 26px; font-weight: 800; letter-spacing: -0.02em; }
.shows-sub { color: var(--ink-soft); font-size: 13px; margin-top: 2px; }

/* Continue-watching / resume banner */
.resume-card {
  /* ★ PAINTED BY THE KIT (.m-tile.m-tile--fill). This rule states only what is
     true of THIS card - its internal arrangement. Background, border, radius
     and the on-fill ink all come from the house, which is why the ink cannot
     be wrong here: nobody chooses it. */
  /* Direction comes from .m-tile--row in the kit. */
  gap: 14px; width: 100%; text-align: left;
  margin: 12px 0 4px; cursor: pointer; font-family: inherit;
  /* ★ NO LONGER .m-tile--fill (Andrew, 2026-08-28, from a device photo). It
     was a full-bleed accent slab 116px tall - on a 328px landscape screen,
     a third of the view painted solid gold before any content. Same objection
     he raised about TAP: "it's also very bright yellow". The accent now marks
     the card rather than filling it - an accent EDGE and an accent kicker,
     which is the treatment he approved for TAP. The accent means ACT; it is
     not a background. */
  border-color: var(--gold);
}
.resume-kicker { color: var(--gold-text); }
.resume-card:active { transform: scale(.99); }
.resume-icon {
  flex: 0 0 auto; width: 46px; height: 46px; border-radius: 0;
  display: flex; align-items: center; justify-content: center;
  background: var(--gold); color: var(--m-accent-ink);
}
.resume-icon svg { width: 22px; height: 22px; margin-left: 2px; }
.resume-body { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.resume-kicker {
  /* colour comes from .m-tile--fill .m-micro in the kit */
}
.resume-ep {
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.resume-meta { display: flex; gap: 8px; }
.resume-show { overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
               opacity: .72; }
.resume-at { flex: 0 0 auto; }
/* ★ THE BAR HAD TO FLIP WITH THE GROUND. It was accent-INK on a dimmed
   accent-ink track, which is correct on a gold fill and INVISIBLE on a dark
   tile - dark ink on a dark ground. Now the accent is the fill and the tile's
   own edge is the track, which is the same pair the transport scrubber uses. */
.resume-bar { margin-top: 6px; height: 3px; border-radius: 0;
              background: var(--m-edge); overflow: hidden; }
.resume-fill { display: block; height: 100%; width: 0; background: var(--gold); border-radius: 0; }
.resume-chev { flex: 0 0 auto; font-size: 26px; line-height: 1; opacity: .8; }

.show-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 10px; margin-top: 12px; padding-bottom: 24px;
}
.show-card {
  /* .m-tile paints it. This is the card's own arrangement only. */
  display: flex; flex-direction: column; gap: 0; padding: 0; overflow: hidden;
  cursor: pointer; text-align: left; font-family: inherit;
  /* The monogram sizes to the tile, so the TILE is the query container.
     Declaring container-type on .sc-art itself would resolve its own cqw
     against an ancestor container instead - a silent no-op here. */
  container-type: inline-size;
}
.show-card {
  /* ★ FLAT, AS EVERY HOST TILE (2026-09-14). A <button> keeps the UA's 2px
     OUTSET border unless told otherwise, and .m-tile does not tell it: every
     show card rendered a bevelled frame, the only bevel in the estate. */
  border: 0;
}
.show-card:hover { background: var(--elev-2); }
.show-card:active { transform: scale(.98); }
.sc-art {
  /* ★ The artwork panel is a PLACEHOLDER far more often than not: every one of
     the 79 show manifests ships without an artwork field, so this state is the
     norm, not a brief flash before a poster loads. It was painted
     background: var(--m-accent) - a full-bleed 1:1 accent square, multiplied by
     79 tiles, which is why the app read as a black-and-yellow one despite every
     colour here being a Metro token. Metro adherence was true at the token
     layer and false at the layer that decides how much screen a token covers.
     The placeholder is now a NEUTRAL tile and the accent is spent on the
     monogram alone. */
  display: flex; align-items: center; justify-content: center;
  aspect-ratio: 1 / 1; border-radius: 0; font-weight: 800;
  /* The monogram now carries whole words ("MASTERMIND UK"), not a five-letter
     cut, so it must size to fit rather than overflow a 150px tile. */
  font-size: clamp(12px, 11cqw, 20px);
  line-height: 1.15; text-align: center; text-wrap: balance;
  padding: 0 10px; overflow-wrap: anywhere; hyphens: manual;
  letter-spacing: .06em; color: var(--m-accent);
  background: var(--elev-2);
  background-size: cover; background-position: center;
}
.sc-art.with-art { color: transparent; background-color: var(--elev-2); }
.sc-body {
  /* ★ Labels sat hard against the card's left border - .show-card has
     padding: 0 and nothing here reinstated it. Padding belongs on the text
     block rather than the card, so the artwork above still bleeds edge to
     edge. */
  display: flex; flex-direction: column; gap: 2px; min-width: 0;
  padding: 8px 10px 10px;
}
.sc-title { font-weight: 700; font-size: 14px; line-height: 1.25;
  overflow: hidden; text-overflow: ellipsis; display: -webkit-box;
  -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
.sc-count { color: var(--ink-faint); font-size: 12px; }
.back-btn {
  flex: none; width: 34px; height: 34px; border-radius: 0;
  background: var(--elev-2); border: 1px solid var(--line); color: var(--ink);
  font-size: 22px; line-height: 1; cursor: pointer; padding: 0 0 2px 0;
}
.back-btn:hover { background: var(--elev-3); }

.show-head { display: flex; align-items: center; gap: 14px; padding: 6px 0 2px; }
.show-chip {
  width: 52px; height: 52px; border-radius: 0; flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  font-weight: 900; font-size: 16px; letter-spacing: .5px; color: var(--m-accent-ink);
  background: var(--m-accent);
}
.show-chip.with-art {
  background-size: cover;
  background-position: center;
  color: transparent;
}
.show-name { font-size: 26px; font-weight: 800; color: var(--ink); letter-spacing: -.2px; }
.show-sub { color: var(--ink-faint); font-size: 13px; margin-top: 3px; font-weight: 600; }
.search {
  /* .m-input paints it. The car needs a taller hit area than the desk does. */
  min-height: 48px;
}
.search::placeholder { color: var(--ink-faint); }
.search:focus-visible { outline: 2px solid var(--m-accent); outline-offset: 1px; background-color: var(--elev-2); }
.search::-webkit-search-cancel-button { -webkit-appearance: none; }

/* search + the two pickers on one row; search takes the slack, each picker keeps
   a full tap target (this is a car UI - the buttons must not shrink below it) */
.ep-tools { display: flex; gap: 8px; align-items: stretch; }
.ep-tools .search { flex: 1 1 auto; min-width: 0; }
.pick-btn {
  flex: 0 0 auto; display: flex; align-items: center; gap: 7px;
  padding: 0 16px; min-height: var(--tap); border-radius: 0;
  border: 1px solid var(--line-2, var(--m-edge));
  background: var(--elev-2, var(--m-edge)); color: var(--ink);
  font-size: 14px; font-weight: 700; white-space: nowrap; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background .14s, transform .14s;
}
.pick-btn:hover { background: var(--elev-3, var(--m-tile-open)); }
.pick-btn:active { transform: scale(.96); }
.pick-btn .ic { width: 18px; height: 18px; flex: 0 0 auto; }
/* labels are the first thing to go when the row gets tight (small phones /
   landscape); two icon buttons still leave the search box usable at 375px */
@media (max-width: 560px) { .pick-btn .lbl { display: none; } .pick-btn { padding: 0 14px; } }

/* season selector - horizontal scroll of chips above the episode list */
.season-bar {
  display: flex; gap: 8px; overflow-x: auto; padding: 4px 2px 10px;
  -webkit-overflow-scrolling: touch; scrollbar-width: none;
  flex: 0 0 auto;  /* never let the flex column crush the chips (landscape) */
}
.season-chip { flex: 0 0 auto; }
/* header + search must also hold their height so only the ep-list scrolls */
#view-episodes .show-head, #view-episodes .ep-tools { flex: 0 0 auto; }
.season-bar::-webkit-scrollbar { display: none; }
.season-chip {
  flex: 0 0 auto; padding: 8px 14px; border-radius: 0;
  border: 1px solid var(--line-2, var(--m-edge));
  background: var(--elev-2, var(--m-edge)); color: var(--ink);
  font-size: 13px; font-weight: 700; white-space: nowrap; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.season-chip.active { background: var(--gold); color: var(--m-accent-ink); border-color: transparent; }

.ep-list, .cap-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.ep {
  /* Direction comes from .m-tile--row. The CAR requirement is the 64px row -
     a thumb-sized target in a moving vehicle. */
  position: relative; border: none; padding: 9px 10px;
  cursor: pointer; min-height: 64px; gap: 13px;
  transition: background .14s;
}
.ep::after {
  content: "";
  position: absolute;
  left: 75px;
  right: 8px;
  bottom: 0;
  height: 1px;
  background: var(--m-tile);
}
.ep:active { background: var(--elev-2); }
.ep-art {
  width: 52px; height: 52px; border-radius: 0; flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  font-size: 20px; color: var(--ink-soft);
  background: var(--m-tile);
  background-size: cover;
  background-position: center;
  border: 1px solid var(--m-edge);
}
.ep-body { flex: 1; min-width: 0; }
.ep-label { font-size: 16px; font-weight: 700; color: var(--ink); letter-spacing: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ep-sub { font-size: 13px; color: var(--ink-faint); margin-top: 3px; font-weight: 600; }
/* Win episode. Sits inline after the label rather than out at the right edge,
   where .ep-count already lives - two right-aligned badges would read as one
   control. The label ellipsises on overflow, so the pill is a flex sibling that
   cannot be squeezed out (flex-shrink 0) or the marker would vanish on exactly
   the long titles that need it most. */
.ep-win {
  flex: 0 0 auto; margin-left: 8px; vertical-align: 1px;
  font-size: 10px; font-weight: 800; line-height: 1;
  color: var(--gold-text); border: 1px solid var(--gold); border-radius: 0;
  padding: 2px 4px; letter-spacing: .04em;
}
/* The label text is wrapped in .ep-name so it stays a real element: a bare text
   node inside a flex container becomes an anonymous item that no rule can target,
   which would silently drop the ellipsis on long titles. */
.ep-label { display: flex; align-items: center; }
.ep-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ep-count {
  flex: 0 0 auto; margin-left: auto;
  font-size: 12px; font-weight: 800; color: var(--gold-text);
  display: inline-flex; align-items: center; gap: 4px;
}
.ep-count::before { content: "\2605"; font-size: 11px; }
.m-empty { padding: 24px; text-align: center; color: var(--m-ink-3);
            border: 1px dashed var(--m-edge); }
/* ★ AUDIO MODE IS NOT A SILENT VIDEO MODE.
   The now-playing art was a 1:1 square at min(88vw, 470px) in BOTH modes - on a
   375px phone that is ~330px of static picture above the fold, which pushed the
   TAP capture target, the whole point of the tool, off the bottom of the screen.
   Andrew: "the Audio version of the tool is ineffective."
   In audio there is nothing moving to look at, so the art becomes a compact
   thumbnail beside the episode text and the reclaimed height goes to the
   transport and the capture target. Video mode is untouched: the moment
   .video-on is set the square returns and the picture is the point again. */
.now-card { display: block; }
/* ★ In video mode the show name sat flush against the bottom of the picture.
   The card is a picture ABOVE a text block, so the gap belongs to the card. */
.now-card.video-mode { display: flex; flex-direction: column; gap: 14px; }
.now-card.video-mode .now { padding-top: 2px; }
.now-card:not(.video-mode) {
  display: flex; align-items: center; gap: 12px;
}
.now-card:not(.video-mode) .now-artwork {
  width: 84px; flex: 0 0 84px; aspect-ratio: 1 / 1;
}
.now-card:not(.video-mode) .now-artmono { font-size: 11px; }
/* ★ The fallback monogram was printing ON TOP of real artwork - "CHASE UK"
   ghosted across the picture. It is a FALLBACK: it shows only when there is no
   picture behind it, exactly as the show-tile monogram does. */
.now-artwork.has-art .now-artmono,
.now-artwork.has-art .now-artmark { display: none; }
.now-card:not(.video-mode) .now { min-width: 0; flex: 1 1 auto; }
.now-card:not(.video-mode) .av-toggle { margin-top: 8px; }

/* ═══ PORTRAIT AUDIO: A POSTER, THEN THE CONTROLS ON THE BOTTOM EDGE ══════
   Andrew, 2026-08-28: "In audio mode the controls should be pinned to the
   bottom of the screen, we should take inspiration from tools like Spotify."

   ★ THE ORIGINAL BUG IS NOT BEING REOPENED, AND IT MATTERS THAT IT IS NOT.
   Audio art became an 84px thumbnail because a `min(88vw, 470px)` square -
   a FIXED height - pushed TAP off the bottom of a 375px phone, and Andrew
   reported the audio tool as ineffective. What made that fatal was the square
   being rigid: it took its height first and the capture target got what was
   left, which on a short phone was nothing.

   Here the picture is the ONLY flexible element in the column. It takes what
   the controls do not, in that order, so it can be large on a tall phone and
   small on a short one and can never push anything off the screen. Same
   picture, opposite priority.

   Metro, not Spotify's chrome: no ambient blur behind the art, no rounded
   corner, no shadow. The house language says the ground is flat and unlit,
   and the beauty here is a full-bleed poster and big type, not a gradient. */
.now-card:not(.video-mode) {
  flex: 1 1 auto; min-height: 0;
  display: flex; flex-direction: column; align-items: stretch; gap: 14px;
}
.now-card:not(.video-mode) .now-artwork {
  width: 100%; flex: 1 1 auto; min-height: 96px;
  aspect-ratio: auto; height: auto;
  background-size: cover;
}
.now-card:not(.video-mode) .now { flex: 0 0 auto; }
/* the monogram is a fallback for a missing poster; centre it in the big box */
.now-card:not(.video-mode) .now-artmono { font-size: 13px; }

/* show album-art square */
.now-artwork {
  position: relative;
  width: min(88vw, 470px); aspect-ratio: 1 / 1; height: auto;
  border-radius: 0;
  display: flex; align-items: center; justify-content: center;
  background: var(--m-tile);
  background-size: cover;
  background-position: center;
  overflow: hidden;
}
.now-artwork::after {
  content: "";
  position: absolute;
  inset: 0;
  background: transparent;
  pointer-events: none;
}
/* paired video fills the artwork square; hidden until video mode */
.now-video {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: contain; background: var(--m-ground); display: none; z-index: 2;
  cursor: pointer; /* tap to toggle fullscreen */
}
.now-artwork.video-on { aspect-ratio: 16 / 9; }
.now-artwork.video-on .now-video { display: block; }
.now-artwork.video-on::after { display: none; }
.now-artwork.video-on .now-artmono { display: none; }
/* audio/video switch pill */
.av-toggle {
  margin-top: 12px;
  display: inline-flex; align-items: center; gap: 6px;
  padding: 7px 14px; border-radius: 0;
  border: 1px solid var(--m-edge);
  background: var(--m-edge); color: var(--ink);
  font-size: 12px; font-weight: 800; letter-spacing: .4px;
  cursor: pointer; -webkit-tap-highlight-color: transparent;
}
.av-toggle::before { content: "\25B6"; font-size: 10px; color: var(--gold-text); }
.av-toggle.is-video { background: var(--gold); color: var(--m-accent-ink); border-color: transparent; }
.av-toggle.is-video::before { content: "\266A"; color: var(--m-accent-ink); }
/* Picture-in-picture control - 2026-09-19: moved off the picture (was a
   permanent overlay sitting on top of the video at all times). Auto-enter on
   backgrounding is now the primary path (visibilitychange, app.js), so this
   is the manual fallback for iOS Safari only - a small pill beside avToggle,
   same row, no new row added. 44x44 tap target kept via padding, not a fixed
   box. Shown only in video mode; app.js also gates it on
   document.pictureInPictureEnabled. */
.pip-btn {
  margin-top: 12px; margin-left: 8px;
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; padding: 0;
  border: 1px solid var(--m-edge); border-radius: 0;
  background: var(--m-edge); color: var(--ink);
  cursor: pointer; -webkit-tap-highlight-color: transparent;
}
.pip-btn .ic { width: 22px; height: 22px; }
.pip-btn.is-pip { background: var(--gold); color: var(--m-accent-ink); border-color: transparent; }
.now-artmark {
  display: none;
  font-weight: 900; letter-spacing: 1px; line-height: 1; font-size: 20px; color: var(--m-accent-ink);
  background: var(--gold); padding: 9px 12px; border-radius: 0;
}
.now-artmono {
  z-index: 1;
  position: absolute; bottom: 14px; left: 0; right: 0;
  font-size: 12px; font-weight: 800; letter-spacing: 3px;
  color: var(--m-ink-3); text-transform: uppercase;
}
.now { width: 100%; }
.now-show { color: var(--gold-text); font-size: 11px; font-weight: 800; letter-spacing: 1.6px; text-transform: uppercase; }
.now-episode { font-size: 22px; font-weight: 800; margin-top: 6px; color: var(--ink); line-height: 1.2; letter-spacing: -.2px; }
.now-date { margin-top: 6px; font-size: 13px; color: var(--ink-soft); font-weight: 600; }

/* transport - thin progress bar w/ playhead knob + minimal controls --------- */
.transport {
  padding: 16px;
  border: 1px solid var(--m-edge);
  border-radius: 0;
  background: var(--m-tile);
}
.times {
  display: flex; justify-content: space-between; font-size: 12px;
  color: var(--ink-faint); font-variant-numeric: tabular-nums; font-weight: 600;
  margin-top: 6px;
}
.progress {
  position: relative; height: 5px; background: var(--m-edge);
  border-radius: 0; margin: 0;
}
.progress-fill {
  position: relative; height: 100%; width: 0;
  background: var(--m-accent);
  border-radius: 0;
  transition: width .12s linear;
}
/* playhead knob rides the end of the fill (moves with playback) */
.progress-fill::after {
  content: ""; position: absolute; right: -6px; top: 50%; transform: translateY(-50%);
  width: 12px; height: 12px; border-radius: 0; background: var(--m-ink);
}
/* transparent range overlay -> drag anywhere on the bar to seek */
.scrubber {
  display: block; width: 100%; height: 26px; margin: -15px 0 0;
  -webkit-appearance: none; appearance: none; background: transparent; cursor: pointer;
}
.scrubber::-webkit-slider-runnable-track { background: transparent; height: 26px; }
.scrubber::-webkit-slider-thumb { -webkit-appearance: none; width: 26px; height: 26px; border-radius: 0; background: transparent; }
.scrubber::-moz-range-track { background: transparent; height: 26px; }
.scrubber::-moz-range-thumb { width: 26px; height: 26px; border: none; border-radius: 0; background: transparent; }

.transport-btns { display: flex; align-items: center; justify-content: center; gap: 30px; margin-top: 8px; }
.t-btn {
  background: var(--m-edge); color: var(--ink); border: 1px solid var(--m-edge);
  border-radius: 0;
  font-size: 14px; font-weight: 800; letter-spacing: .3px;
  min-width: var(--tap); min-height: var(--tap); padding: 0 8px;
  display: flex; align-items: center; justify-content: center;
  transition: transform .1s, color .12s;
}
.t-btn:active { transform: scale(.9); color: var(--gold-text); }
/* the play-ring SVG is kept in the DOM for the JS, but not shown (minimalist) */
.play-wrap { position: relative; width: 72px; height: 72px; display: flex; align-items: center; justify-content: center; flex: 0 0 auto; }
.play-ring { display: none; }
.t-btn.play {
  position: relative; z-index: 1;
  background: var(--gold); color: var(--m-accent-ink); border: none;
  min-width: 72px; min-height: 72px; width: 72px; height: 72px;
  border-radius: 0; font-size: 26px;
  display: flex; align-items: center; justify-content: center; padding-left: 3px;
  transition: transform .1s, background .12s;
}
.t-btn.play:active { transform: scale(.94); background: var(--gold-hi); }

/* ---- inline SVG icons (replaces glyph/emoji text) ---- */
.t-btn { padding: 0; }
.t-btn .ic { width: 22px; height: 22px; display: block; }
.t-skip { flex-direction: column; gap: 1px; }
.t-skip .ic { width: 18px; height: 18px; }
.t-skip .t-num { font-size: 11px; font-weight: 800; line-height: 1; }
.t-btn.play .ic { width: 30px; height: 30px; }
.t-btn.play .ic-pause { display: none; }
.t-btn.play.playing .ic-play { display: none; }
.t-btn.play.playing .ic-pause { display: block; }
.tap-icon svg { width: 22px; height: 22px; display: block; }
.cap-btn .ico svg { width: 21px; height: 21px; display: block; }
.nav-btn .nico svg { width: 22px; height: 22px; display: block; }

/* big TAP target - the capture core (kept prominent, flattened) ------------- */
.tap-target {
  /* ★ AN ACKNOWLEDGED DEPARTURE FROM THE DESIGN LANGUAGE (Andrew, 2026-08-28).
     The kit paints a call-to-act as a filled accent block, and this control WAS
     that: a full-bleed accent slab, the single brightest object on a dark
     screen. It is now accent TEXT ON GROUND.

     The reason is glare, not contrast - and the numbers matter here because the
     obvious justification is the wrong one. Measured: dark ink on the accent
     fill is 10.76:1; accent text on this ground is 9.18:1. Contrast very
     slightly DROPS, and both pass AA for large text several times over. What
     changes is the AREA of bright emissive surface, which is what actually
     hurts at night and in a dark car, and which a contrast ratio does not
     express at all.

     The CAR requirement survives the shrink: at 64px this is still well beyond
     the 48px comfortable-touch floor, and it remains the only accent object in
     its region, so it is still findable without looking. Smaller, not quieter.

     Recorded as a deliberate exception, not a drift: any future Metro pass that
     "restores" the fill here is undoing an accessibility decision. */
  position: relative; overflow: hidden; flex: 0 0 auto; width: 100%;
  /* The kit's fill tile carries generous padding for a full-bleed call to
     act. Shrinking the WORD without shrinking the BOX just moves the problem,
     so the box is sized here too - still far above the 48px touch floor. */
  min-height: 64px; max-height: 92px; padding: 12px 16px;
  align-items: center; justify-content: center; gap: 2px;
  cursor: pointer; text-align: center;
  background: var(--elev-2) !important;
  color: var(--gold) !important;
  border: 1px solid var(--m-accent);
  transition: transform .1s, background .14s;
}
.tap-target:active { transform: scale(.985); background: var(--elev-3) !important; }
.tap-target.flash { animation: tapflash .5s cubic-bezier(.22,.61,.36,1); }
@keyframes tapflash {
  /* The confirmation flash was a full accent field - the same glare, once per
     capture. It now flashes the BORDER and ground, keeping the accent on the
     type where the resting state put it. */
  0%   { background: var(--elev-3); color: var(--gold-hi); }
  /* Ends on the ELEVATED GROUND, not on the fill - so here --gold-text is the
     correct choice, and deliberately differs from the resting rule above. */
  100% { background: var(--elev-2); color: var(--gold); }
}
.tap-ripple {
  position: absolute; left: 50%; top: 50%; width: 24px; height: 24px; margin: -12px 0 0 -12px;
  border-radius: 0; background: var(--m-ink-3); opacity: 0; pointer-events: none;
}
.tap-target.rippling .tap-ripple { animation: ripple .55s ease-out; }
@keyframes ripple { 0% { opacity: .5; transform: scale(0); } 100% { opacity: 0; transform: scale(16); } }
.tap-icon { font-size: 15px; line-height: 1; opacity: .9; color: var(--gold); }
/* ★ THE HOUSE DISPLAY TYPE, AT ONE CAR EXCEPTION.
   .m-display carries the size (76px) and tracking - "type is the interface",
   and this is the most important word on the surface, so it should be the
   biggest. The house sets display at weight 100 (Gotham Thin), which is right
   on a desk at reading distance and WRONG through a windscreen: a hairline at
   76px disappears in direct sun and at a glance.
   So the size, family and tracking are the house's; only the WEIGHT is
   overridden, and it is recorded in the friction log as a finding about METRO
   rather than treated as licence to restyle the control. */
.tap-big {
  /* Was the house display face at 76px - the biggest word on the surface. At a
     third of that it still reads at a glance without being the light source. */
  font-weight: 600; font-size: 22px !important; letter-spacing: .06em; line-height: 1.1;
  color: var(--gold);
}
/* Sits on the same accent fill as .tap-big, so it takes the same on-fill ink.
   --ink-soft is a ground colour and was heading for the same collision. */
.tap-sub {
  /* ★ This was BLACK ON DARK GREY and unreadable - measured rgba(0,0,0,.62) on
     #252639, about 1.5:1. The control carries .m-micro from the kit, whose
     micro text assumes a LIGHT surface; when TAP stopped being an accent fill
     and became a dark tile, that assumption inverted and my plain
     `color: var(--ink-soft)` lost to it on specificity. Scoped through
     .tap-target and marked important so the kit cannot re-darken it.
     A caption nobody can read is not a caption. */
  font-size: 11px; letter-spacing: .6px; font-weight: 600;
}
.tap-target .tap-sub,
.tap-target .tap-sub.m-micro { color: var(--ink-soft) !important; opacity: 1; }
/* NOTE: an earlier attempt wrote var(--ink-2), which does not exist in this
   file - the token is --ink-soft. An unknown custom property makes the whole
   declaration INVALID and it is dropped silently, !important and all, so the
   caption simply inherited the control's accent. A colour that "does not
   apply" and a colour that "is wrong" look identical from the outside; only a
   computed-style read tells them apart. */
.tap-target:active .tap-sub { color: var(--ink-soft) !important; }

/* secondary captures -------------------------------------------------------- */
.action-bar { display: flex; flex-direction: column; gap: 12px; }
.capture-row { display: flex; gap: 12px; }
.cap-btn {
  /* .m-btn gives the frame, type and focus ring. The CAR requirement is the
     52px minimum - a thumb, in motion, without looking. */
  position: relative; overflow: hidden; flex: 1; min-height: 52px;
  display: flex; align-items: center; justify-content: center; gap: 9px;
  font-family: inherit; cursor: pointer;
}
.cap-btn .ico { font-size: 20px; line-height: 1; }
/* ★ NO HUE DISTINCTION BETWEEN THESE TWO. They were amber and blue - which
   resolve to --m-warn (a STATE colour) and --m-knowledge (a FAMILY colour).
   So a Voice button at rest was painted in the house's warning colour, and a
   Note button in another bank's identity. That is exactly the collision the
   rule "a filled block is always identity, a small mark is always state"
   exists to prevent, and no palette juggling makes it safe.
   The two are told apart by their ICON and their LABEL - which is what "type
   is the interface" means - and both take house ink. */
.cap-btn .ico { color: var(--m-ink-2); }
.cap-btn:hover .ico { color: var(--m-ink); }
.cap-btn:active { transform: scale(.97); background: var(--elev-2); }
.cap-btn.voice.recording { background: var(--m-accent); color: var(--m-accent-ink); animation: recpulse 1.3s ease-in-out infinite; }
.cap-btn.voice.recording .ico { color: var(--m-ink); }
@keyframes recpulse { 0%,100% { opacity: 1; } 50% { opacity: .72; } }
.rec-timer { font-size: 12px; font-variant-numeric: tabular-nums; font-weight: 800; }

/* =============================================================================
 * CAPTURES VIEW
 * ========================================================================== */
.cap-summary { display: flex; gap: 12px; }
.stat { flex: 1; border-radius: 0; padding: 16px 12px; text-align: center; background: var(--elev); }
.stat .num { display: block; font-size: 30px; font-weight: 900; color: var(--gold-text); line-height: 1; }
.stat .lbl { display: block; margin-top: 6px; font-size: 11px; color: var(--ink-faint); text-transform: uppercase; letter-spacing: .8px; font-weight: 700; }
.cap-actions { display: flex; gap: 12px; }
.act {
  /* .m-btn paints it; .m-btn--key marks the primary one. */
  flex: 1; min-height: 48px; cursor: pointer; font-family: inherit;
}
.act:active { transform: scale(.97); opacity: .9; }

@keyframes capIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
/* ★ THE KIND STRIPES ARE GONE, and they were DEAD before they were wrong.
   Three rules set border-left-COLOR on rows whose border-left-width is 0, so
   they had never drawn a pixel - measured, not assumed. Two of them named
   --blue (another bank's family) and --amber (the warning colour), so had they
   ever rendered, a voice capture would have been striped as a warning in a
   list where a red dot means a failed sync.
   Deleted rather than repaired: the BADGE already states the kind, and the
   house spends colour on identity through the accent alone. A second identity
   channel is redundant, and re-opens the identity/state collision the badge
   rule below was written to close. */
.cap-head { display: flex; align-items: center; gap: 9px; }
.cap-badge { font-size: 11px; font-weight: 800; letter-spacing: .6px; padding: 3px 8px; border-radius: 0; color: var(--ink-soft); background: var(--elev-2); }
/* Capture KIND is identity. Three invented hues would collide with the
   six-family scheme the whole language rests on, so only the accent is spent,
   on the one kind that is a deliberate mark. Ink ON a fill is --m-accent-ink;
   using the fill value as text is the mistake the token trio prevents. */
.cap-tap   .cap-badge { color: var(--m-accent-ink); background: var(--m-accent); }
.cap-text  .cap-badge { color: var(--m-ink-2);      background: var(--m-tile-2); }
.cap-voice .cap-badge { color: var(--m-ink-2);      background: var(--m-tile-2); }
.cap-ts { font-size: 14px; color: var(--gold-text); font-variant-numeric: tabular-nums; font-weight: 800; }
.cap-sync { width: 9px; height: 9px; border-radius: 0; margin-left: auto; flex: 0 0 auto; background: var(--warn); }
.cap-sync.synced  { background: var(--ok); }
.cap-sync.error   { background: var(--error); }
.cap-sync.pending { background: var(--ink-faint); }
.cap-ep { font-size: 13px; color: var(--ink-soft); margin: 7px 0 0; font-weight: 600; }
.cap-text { font-size: 15px; color: var(--ink); line-height: 1.45; margin-top: 7px; }
.cap-play {
  display: inline-flex; align-items: center; gap: 7px; background: var(--elev-2); color: var(--ink);
  border: none; border-radius: 0; padding: 9px 15px; font-size: 14px; font-weight: 700; margin-top: 9px;
  transition: transform .1s, background .12s;
}
.cap-play:active { transform: scale(.96); background: var(--elev-3); }
.cap-del {
  margin-top: 9px; margin-left: 9px; background: transparent; color: var(--error);
  border: none; border-radius: 0; padding: 8px 14px; font-size: 13px; font-weight: 700;
  transition: transform .1s, background .12s;
}
.cap-del:active { transform: scale(.96); background: var(--m-err-bg); }

/* =============================================================================
 * MINI-PLAYER - persistent now-playing bar (non-player views)
 * ========================================================================== */
.miniplayer {
  position: fixed; left: 0; right: 0; z-index: 19;
  bottom: calc(66px + env(safe-area-inset-bottom));
  display: flex; align-items: center; gap: 10px;
  margin: 0 10px; padding: 8px 10px;
  background: var(--m-tile);
  border: 1px solid var(--line-2);
  border-radius: 0;
  animation: miniIn .24s cubic-bezier(.22,.61,.36,1);
}
.miniplayer[hidden] { display: none; }
@keyframes miniIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
.mini-open {
  flex: 1; min-width: 0; display: flex; align-items: center; gap: 12px;
  background: none; border: 0; color: var(--ink); text-align: left; padding: 0;
  -webkit-tap-highlight-color: transparent;
}
.mini-open:active { opacity: .8; }
.mini-art {
  width: 42px; height: 42px; border-radius: 0; flex: 0 0 auto;
  background: var(--m-tile);
  background-size: cover; background-position: center;
  border: 1px solid var(--m-edge);
}
.mini-meta { display: flex; flex-direction: column; min-width: 0; gap: 2px; }
.mini-title { font-size: 14px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.mini-show { font-size: 10px; font-weight: 800; letter-spacing: .6px; text-transform: uppercase; color: var(--gold-text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.mini-toggle {
  flex: 0 0 auto; width: 46px; height: 46px; border-radius: 0;
  background: var(--gold); color: var(--m-accent-ink); border: none;
  display: flex; align-items: center; justify-content: center;
  transition: transform .1s, background .12s;
}
.mini-toggle .ic { width: 22px; height: 22px; display: block; }
.mini-toggle .ic-pause { display: none; }
.mini-toggle.playing .ic-play { display: none; }
.mini-toggle.playing .ic-pause { display: block; }
.mini-toggle:active { transform: scale(.94); background: var(--gold-hi); }

/* =============================================================================
 * BOTTOM NAV (portrait)  ->  LEFT RAIL (landscape)
 * ========================================================================== */
.bottomnav {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 20; display: flex;
  background: var(--m-tile);
  border-top: 1px solid var(--line);
  padding-bottom: env(safe-area-inset-bottom);
}
.nav-btn {
  flex: 1; min-height: 58px; background: transparent; border: none;
  /* ★ LABELLED AS THE HOST LABELS (2026-09-14): micro, UPPERCASE, --m-track,
     at the cockpit weight. Was mixed-case 700 11px at .2px tracking - the one
     label style on the surface that matched nothing on the host. */
  color: var(--ink-faint); font: var(--m-micro); font-weight: 700;
  letter-spacing: var(--m-track); text-transform: uppercase;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px;
  position: relative; transition: color .15s;
}
.nav-btn .nico { font-size: 20px; line-height: 1; transition: transform .18s cubic-bezier(.22,.61,.36,1); }
.nav-btn.active { color: var(--ink); }
.nav-btn.active .nico { color: var(--gold-text); transform: scale(1.06); }
.nav-btn:active .nico { transform: scale(.9); }
.nav-badge {
  position: absolute; top: 7px; right: 50%; transform: translateX(22px);
  background: var(--gold); color: var(--m-accent-ink); font-size: 10px; font-weight: 900;
  min-width: 16px; height: 16px; border-radius: 0; padding: 0 4px; line-height: 16px;
}
.nav-badge:empty { display: none; }

/* =============================================================================
 * TEXT SHEET
 * ========================================================================== */
.sheet {
  position: fixed; inset: 0; z-index: 30; display: none; align-items: flex-end;
  background: var(--m-ground);
}
.sheet.open { display: flex; animation: sheetFade .2s ease-out; }
@keyframes sheetFade { from { opacity: 0; } to { opacity: 1; } }
.sheet-panel {
  width: 100%; background: var(--elev); border-top: 2px solid var(--gold);
  border-radius: 0; padding: 10px var(--pad) calc(20px + env(safe-area-inset-bottom)); animation: sheetUp .28s cubic-bezier(.22,.61,.36,1);
}
@keyframes sheetUp { from { transform: translateY(100%); } to { transform: translateY(0); } }
.sheet-grip { width: 40px; height: 4px; border-radius: 0; background: var(--line-2); margin: 0 auto 14px; }
.sheet-title { font-size: 16px; font-weight: 800; margin-bottom: 12px; }
.sheet-hint { font-size: 12px; color: var(--ink-faint); font-weight: 500; margin-left: 6px; }
.sheet-input {
  width: 100%; background: var(--bg); color: var(--ink); border: 1px solid var(--line-2);
  border-radius: 0; padding: 14px; font-size: 16px; font-family: var(--font);
  resize: none; user-select: text; line-height: 1.5; transition: border-color .15s;
}
.sheet-input:focus-visible { outline: 2px solid var(--m-accent); outline-offset: 1px; border-color: transparent; }
.sheet-input::placeholder { color: var(--ink-faint); }
.sheet-btns { display: flex; gap: 12px; margin-top: 14px; }

/* =============================================================================
 * TOAST
 * ========================================================================== */
.toast {
  position: fixed; left: 50%; bottom: 88px; transform: translate(-50%, 20px);
  background: var(--elev-2); color: var(--ink); border: 1px solid var(--line-2);
  padding: 12px 20px; border-radius: 0; font-size: 14px; font-weight: 700;
  opacity: 0; pointer-events: none; transition: opacity .2s, transform .26s cubic-bezier(.22,.61,.36,1);
  z-index: 40; max-width: 86%; text-align: center;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
.toast.ok    { border-color: var(--ok);    color: var(--ok); }
.toast.warn  { border-color: var(--warn);  color: var(--warn); }
.toast.error { border-color: var(--error); color: var(--error); }

/* =============================================================================
 * LANDSCAPE (horizontal) - in-car mount. Two-column player + left rail nav.
 * Trigger on landscape + short viewport so big tablets in landscape still get
 * the roomy portrait-style stack.
 * ========================================================================== */
@media (orientation: landscape) and (max-height: 600px) {
  /* left rail instead of a bottom bar -> reclaim vertical space */
  body {
    padding-bottom: env(safe-area-inset-bottom);
    padding-left: calc(74px + env(safe-area-inset-left));
    background-image: var(--m-accent);
  }
  .bottomnav {
    top: 0; bottom: 0; right: auto; width: calc(74px + env(safe-area-inset-left));
    flex-direction: column; justify-content: center; gap: 10px;
    border-top: none; border-right: 1px solid var(--line);
    padding: 0 0 0 env(safe-area-inset-left);
  }
  .nav-btn { flex: 0 0 auto; min-height: 66px; font-size: 10px; letter-spacing: .04em; } /* uppercase must fit the 74px rail */
  .nav-btn .nico { font-size: 21px; }
  .nav-badge { right: auto; left: 50%; transform: translateX(6px); top: 6px; }

  /* mini-player clears the left rail and sits at the very bottom (no bottom nav) */
  .miniplayer {
    bottom: calc(10px + env(safe-area-inset-bottom));
    left: calc(74px + env(safe-area-inset-left));
    right: 0; margin: 0 16px;
  }

  .topbar { padding: 10px 18px 8px 24px; }
  .brand-name { font-size: 14px; }

  /* ★ THE VIEWPORT HEIGHT HAD TWO HOMES AND ONLY ONE WAS EVER CORRECT.
     The player capped itself with `calc(100dvh - 62px)`, where 62 was a guess
     at the top bar. The bar is not 62px - it changes with the status chips -
     and on Andrew's device the sum overran, so `.view`'s own `overflow-y:auto`
     produced a small vertical scroll. A literal that must agree with a
     measured element drifts the moment the element does.

     So the SHELL owns the height: the body is exactly one viewport and does
     not scroll, `.view` takes what is left with `min-height: 0` (without it a
     flex item refuses to shrink below its content), and the player asks for
     100% of that. Nothing has to know how tall the bar is. */
  html, body { height: 100dvh; overflow: hidden; }
  body { min-height: 0; }
  /* ★ THE VIEW'S BOTTOM PADDING IS THE PORTRAIT ONE AND IT DOES NOT FIT HERE.
     `--pad` is 20px, sized for a portrait page with a tab bar beneath it. In
     landscape the view already ends on the screen edge, so those 20px hang
     past it - measured LIVE as 8px of document scroll that the harness did not
     reproduce, because the harness renders no status chips and its top bar is
     therefore shorter than the real one. Trimmed to 8, which is both the fix
     and 12px handed back to the picture. `overflow: hidden` on html above is
     the belt: a landscape shell that is exactly one screen should not be able
     to scroll at all. */
  .view { min-height: 0; padding-bottom: 8px; }
}
/* ^ closes the landscape block opened at line 684.
 * It was MISSING until 2026-08-22. Browsers auto-close at EOF so nothing
 * rendered wrongly, but the file was malformed and anything appended to it
 * landed INSIDE the landscape media query - which is how a red-watch plant
 * silently failed to apply in portrait and made a gate look sound when it
 * had not been exercised at all. */

/* ═══ LANDSCAPE: VIDEO LEFT, CONTROLS RIGHT ══════════════════════════════
   Andrew, 2026-08-28. In landscape the player stacked exactly as it does in
   portrait, so a 16:9 video took the full width and every control - transport,
   TAP, voice and note - sat below the fold. On a phone held sideways that is
   the whole screen spent on the picture and nothing to press.

   The markup is NOT rearranged: .now-card is dissolved with `display: contents`
   so the artwork and the text block become direct children of the player grid,
   and each is placed by column. Reordering the DOM would have broken portrait
   and the tab order; this leaves both intact.

   Scoped to landscape AND a short viewport, so a landscape TABLET - which has
   the height to stack comfortably - keeps the portrait arrangement. */
@media (orientation: landscape) and (max-height: 560px) {
  /* ★ LANDSCAPE: VIDEO LEFT AND LARGE, EVERYTHING ELSE RIGHT.
     Andrew, 2026-08-28: title to the right, scrubber smaller, Voice/TAP/Note on
     one compact line - "then the video can be larger". Each of those buys
     vertical space in the right-hand column, and the video takes what they free.

     HISTORY WORTH KEEPING, because three revisions failed the same way. The
     picture spans every row. A spanning item DISTRIBUTES its height across the
     rows it spans, so any time the picture is the tallest thing, it inflates
     the rows and pushes the transport off the screen. Rev 1 got away with it
     because the column happened to be taller. Rev 2 did not. Rev 3 "capped" it
     with `max-height: 100%`, which is circular - the rows are sized by the
     picture, so 100% IS the picture - and shipped 37px of scroll.
     The picture now has NO intrinsic height at all: it stretches to its column
     and shapes its content inside itself. There is nothing left to inflate. */
  /* ★ .active IS LOAD-BEARING, NOT DECORATION. `.view` hides every view with
     `display: none` (0,1,0) and `.view.active` reveals one (0,2,0). This
     selector carries an ID, so a bare `#view-player { display: grid }` is
     (1,1,0) and BEATS the hiding rule - the player rendered underneath the
     Shows list whenever the viewport was landscape, active or not, and Andrew
     photographed exactly that: "Pick an episode" and a transport sitting under
     the show list. Third specificity accident in this block; the first two
     were rules that silently LOST, this one silently WON. */
  #view-player.active {
    display: grid;
    /* The right column is sized to the WIDEST thing it must hold - the
       three-up action row - and no wider. Everything else goes to the video. */
    grid-template-columns: minmax(0, 1fr) 262px;
    /* FOUR rows, and the SPRING IS THE FIRST ONE. Everything in the right
       column is bottom-weighted: the slack collects at the top, and title,
       scrubber, transport and TAP sit as one block on the bottom edge - which
       is the Spotify reading of a now-playing screen, and the reason the
       controls are "pinned" without anything being positioned.

       An earlier arrangement put the spring in the middle, which pinned the
       controls correctly and left a hole between the title and the scrubber:
       the title floated at the top with nothing under it. Same pinning, worse
       composition. The slack belongs where nothing is looking.

       Andrew asked for this in audio; it is applied to BOTH modes, because two
       arrangements that differ for no reason the user can see is not a design,
       and because in video the slack goes to the picture, which is the same
       thing he asked for when he said "then the video can be larger". */
    grid-template-rows: 1fr min-content min-content min-content;
    gap: 8px 12px;
    height: 100%; min-height: 0;
    overflow: hidden;
    /* the view's own side padding is width the video could be using */
    padding-left: 4px; padding-right: 4px;
  }
  /* ★ Must out-specify the PORTRAIT rules `.now-card.video-mode` and
     `.now-card:not(.video-mode)` (0,2,0). A media query adds no specificity, so
     a bare `.now-card` here silently loses and the card never dissolves - which
     left the title in the left column and the controls stacked off-screen. */
  .now-card,
  .now-card.video-mode,
  .now-card:not(.video-mode) { display: contents; }
  /* ★ ID-ANCHORED ON PURPOSE. The portrait rules are
     `.now-card:not(.video-mode) .now-artwork` (0,3,0), and a media query adds
     no specificity of its own, so a class-only selector here loses and the art
     silently keeps its portrait size - exactly how the landscape card failed
     to dissolve two revisions ago. #view-player + #nowArt is (2,1,0).

     ★ NO ASPECT RATIO IN EITHER MODE, AND THAT IS THE FIX FOR THE SCROLL.
     A 16:9 box with `max-height: 100%` looks capped and is not: the item spans
     every row, so the rows are sized BY the picture and 100% resolves to the
     picture's own height. The cap is circular and caps nothing - measured at
     889x328 as 37px of scroll inside the player, which is the small vertical
     scroll Andrew reported. An earlier version escaped this with
     `calc(100dvh - 84px)`, which worked only because it did NOT refer to the
     thing it capped - and that is the top-bar guess this pass removes.

     So the box STRETCHES to its column and the picture is shaped INSIDE it:
     the <video> already carries `object-fit: contain`, so it letterboxes onto
     the flat ground, and the poster is `cover`. Nothing can inflate a row,
     because nothing is sized by its own content.

     ★ AND THE AUDIO POSTER IS NO LONGER A 92px THUMBNAIL IN A 518px COLUMN.
     The rule that shrank it was written for PORTRAIT, where a big square once
     pushed TAP off the bottom and made the tool useless ("the Audio version of
     the tool is ineffective"). That reason does not transfer here: the art is
     the only thing that stretches and the controls are pinned by the spring
     row, so it cannot push anything anywhere. */
  #view-player #nowArt {
    grid-column: 1; grid-row: 1 / -1;
    width: 100%; max-width: none;
    align-self: stretch; height: 100%; min-height: 0;
    aspect-ratio: auto; flex: none;
    background-size: cover;
  }
  .now        { grid-column: 2; grid-row: 2; min-width: 0; align-self: end; }
  .transport  { grid-column: 2; grid-row: 3; }
  .action-bar { grid-column: 2; grid-row: 4; }

  /* ── the title block, now on the right ──────────────────────────────────
     ★ THE MODE TOGGLE SHARES THE EYEBROW'S LINE. It had a row of its own
     under the date, which cost 32px of a column that was already 9px too
     tall - and it is a MODE SWITCH, so the top-right corner of the block it
     switches is where it belongs anyway. Four stacked rows become three.

     Specificity, again: the base rule is `.now-card:not(.video-mode)
     .av-toggle` (0,2,0), so a bare `.av-toggle` here would lose its
     margin-top and the toggle would sit 8px below its own row. Both
     selectors are named. */
  .now {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    column-gap: 8px;
  }
  .now-show    { grid-column: 1; grid-row: 1; font-size: 10px; }
  .now-episode { grid-column: 1 / -1; grid-row: 2; font-size: 16px; line-height: 1.15; margin-top: 4px; }
  .now-date    { grid-column: 1 / -1; grid-row: 3; font-size: 11px; margin-top: 3px; }
  .av-toggle,
  .now-card:not(.video-mode) .av-toggle {
    grid-column: 2; grid-row: 1; justify-self: end;
    margin-top: 0; padding: 4px 9px; font-size: 11px;
    position: relative;
  }
  /* ★ THE CHIP IS 23px TALL AND THE TAP TARGET IS 45. Padding it to 44 would
     hand back most of the height this pass just reclaimed, so the HIT AREA is
     extended past the painted box instead - the target clears the floor while
     the chip stays a chip. It grows DOWN and LEFT only. UP is the top bar - an
     11px extension above the chip hit-tested to the BAR, not the button. RIGHT
     is the screen edge, and an absolutely positioned box DOES count toward
     scrollable overflow, so an overhang there is horizontal scroll on any
     screen where the toggle sits flush to the margin - which is every screen,
     because it is right-aligned. The title below is not interactive, so
     growing over it costs nothing.

     Worth knowing WHY this was never caught: the metro gate measures every
     VISIBLE control against the 44px floor, and this button is `hidden` unless
     the episode has both an audio and a video source - so in the harness it
     has never once been visible to be measured. Same shape as the gate that
     was never registered: the check exists and the case never reaches it. */
  .av-toggle::after { content: ""; position: absolute; inset: -4px 0 -24px -16px; }

  /* ── a smaller scrubber and a smaller transport ─────────────────────────
     ★ THE PLAY BUTTON SETS THE WHOLE ROW'S HEIGHT. It carries an explicit
     72x72 in the base rule, and the previous landscape pass only lowered its
     min-height - which does nothing against an explicit height, so the row
     stayed 72px and the column stayed off the screen. Both axes and the
     .play-wrap that reserves space for it have to move together. 52 keeps it
     the largest control in the transport and clears the 44px floor. */
  .transport { padding: 4px 10px 6px; }
  .times { font-size: 10px; margin-top: 2px; }
  .progress { height: 3px; }
  .scrubber { height: 22px; }
  .transport-btns { gap: 10px; margin-top: 2px; }
  .t-btn { min-height: 44px; }
  .play-wrap { width: 52px; height: 52px; }
  .t-btn.play { min-width: 52px; min-height: 52px; width: 52px; height: 52px; font-size: 20px; }
  .t-btn.play .ic { width: 22px; height: 22px; }

  /* Voice | TAP | Note on ONE line */
  .action-bar  { display: flex; flex-direction: row; align-items: stretch; gap: 8px; }
  .capture-row { display: contents; }
  .tap-target  { order: 2; flex: 1 1 44%; min-height: 46px; max-height: 50px; padding: 5px 10px; }
  .cap-btn.voice { order: 1; }
  .cap-btn.text  { order: 3; }
  .cap-btn { flex: 1 1 26%; min-height: 46px; max-height: 50px; padding: 5px 6px; gap: 3px; }
  .cap-btn .lbl { font-size: 10px; letter-spacing: .02em; }
  .cap-btn .ico svg { width: 15px; height: 15px; }
  .tap-big  { font-size: 16px !important; }
  .tap-sub  { display: none; }              /* the label carries it; the caption does not fit */
  .tap-icon { font-size: 12px; }
}



/* ═══ THE HOUSE FONT REACHES THE CONTROLS ════════════════════════════════
   Andrew, 2026-08-28: "Episode, Listen, and Captures should also be in the
   house font."

   They were in ARIAL - and so was every other control. Form elements do not
   inherit font-family from their ancestors; the UA sheet sets its own. So the
   page was Gotham and every button, chip and input was the browser default,
   which is why the tab bar looked subtly foreign next to the headings.

   Measured before fixing, because the instruction named three labels and the
   fault was wider: 42 controls across 11 classes - nav-btn, season-chip x28,
   t-btn, av-toggle, back-btn, pick-btn, mini-open, mini-toggle, scrubber.
   One inherited rule is the fix; eleven patches would have left the twelfth. */
button, input, select, textarea, .nav-btn, .t-btn, .season-chip, .av-toggle {
  font-family: inherit;
}

/* The episode a Bank door opened at (lane Z, 26 Sept). */
.ep[data-target-episode] { box-shadow: inset 4px 0 0 var(--m-ink); background: var(--m-tile-open, var(--m-tile)); }
