/* ============================================================
   Blazing Design System
   Background: #0A0A0A  |  Accent: #FF3D47  (Blazing red)

   #FF3D47, not #E50914. Markus, 27 Aug: "the color scheme on the apple tv seems
   so different and it shouldnt be like that." Apple TV was not the odd one —
   Roku, Fire TV and Apple TV all carry #FF3D47, and Colors.kt says in as many
   words that "the Roku is the reference". The WEB and the Samsung app were the
   two left behind on the older Netflix-ish #E50914, so those are what moved.
   Motion: the six-name table in :root, mirroring Theme.brs ThemeMotion()
   ============================================================ */

:root {
  --bg: #0A0A0B;
  /* surface/muted/quiet were #141416 / #a3a3aa / #6d6d75 — the same "grey
     plate" and blue-tinted greys Markus called out twice ("No gray. I'm
     getting tired of seeing all this gray") and that the Roku's Theme.brs,
     the Apple TV's Theme.swift and Fire TV's Colors.kt already carry the fix
     for: a warm near-black surface instead of a neutral grey, and plain
     white at reduced opacity (60%/35%) instead of a blue-tinted solid.
     --surface-focus and --surface-soft are left as they are — they are hover
     states and a third neutral tier this app invented on its own, with no
     matching token on any other client to copy a corrected value from. */
  --surface: #0E0A0C;
  /* The warm ladder, matched to the Roku's Theme.brs rather than invented here:
     bg 0A0A0B -> surface 0E0A0C -> surface-soft -> surface-focus 171012, which
     is literally Roku's surfaceHi. These two were #1c1c1f and #18181b -- COLD
     neutral greys, the exact "grey plate" Markus has now called out twice, and
     the reason a focused card on the web reads blue-black next to the Roku's. */
  --surface-soft: #140F11;
  --surface-focus: #171012;
  /* Roku draws EVERY hairline in the accent at 20% (Theme.brs `line 0xFF3D4733`),
     not in white. The web had no border token at all, so each rule invented its
     own white rgba and the app grew a grey-line grid the TVs do not have. */
  --border: rgba(255, 61, 71, .20);
  /* Roku's `text` tier, 0xFFFFFFCC. Body copy sits between --text (headings) and
     --muted (secondary); without it every body paragraph picked one or the
     other and the type had two tiers where the contract has four. */
  --text-body: rgba(255, 255, 255, .80);
  /* Roku fills a FOCUSED row with the accent at 78% (Theme.brs focusFill
     0xFF3D47C8) and edges it with the solid accent. The web only ever glowed at
     35%, so the selected thing on a television read as a grey plate with a red
     halo instead of a red plate. This is the token that closes that. */
  --focus-fill: rgba(255, 61, 71, .78);
  /* The content gutter. DESIGN.md was measured on a 1920 desktop with a 1740
     content track, i.e. 90px each side, and Theme.brs uses the same 90 on the
     Roku. Small screens keep the tight 18 they were built with; the 90 arrives
     only when there is room for it. */
  --gutter: 18px;
  --text: #f7f7f8;
  --muted: rgba(255,255,255,.6);
  --quiet: rgba(255,255,255,.35);
  --accent: #FF3D47;
  --accent-strong: #E11D2B;
  --accent-glow: rgba(255,61,71, 0.35);
  /* DESIGN-V2 §2.5 `good`: cached, positive, "live now", a followed team.
     ONE green. Live TV (§2.14) is its first user on this client. */
  --good: #4CD48A;
  --radius: 16px;
  /* THE FOCUS CURVE. Roku ramps focusPercent LINEARLY over its own ~0.25s
     animation and shapes the VALUE with an outCubic (Theme.brs
     ThemeFocusEase: 1 - (1-f)^3). cubic-bezier(.215,.61,.355,1) is that same
     curve, and 250ms is that same duration, so a card grows at the same rate
     on a television and in a browser. Do not "improve" it — DESIGN-V2.md §2.6
     says three other client builds are written against exactly this curve. */
  --focus-ease: 250ms cubic-bezier(.215, .61, .355, 1);

  /* ── THE MOTION TABLE — DESIGN-V2.md §2.7 ────────────────────────────────
     Six names, mirroring Theme.brs ThemeMotion() value for value, and this is
     the ONLY timing table in this client. What used to sit here was
     `--transition: 300ms ease`, driving 29 rules — a duration and a curve that
     appear nowhere else in the product, so every button on the web settled at
     a different rate from the same button on a television.

       SETTLE    140ms outQuad    non-list focus: buttons, chips, drawer rows
       ARRIVE    220ms linear     screen enter. A plate 1.0 -> 0.0. A DIP TO
                                  BLACK, never a slide, never a cross-fade
       GLIDE     300ms outQuint   any translation: details scroll, row raise
       PANEL     200ms outQuad    drawer and StreamSelect slide-in + the scrim
       DWELL     900ms outQuint   the room dim, going down. The signature
       DWELL_UP  180ms outQuad    the room coming back

     The two beziers are the CSS spellings of Roku's easeFunction names. Do NOT
     reach for the keyword `ease-out` — that is cubic-bezier(0,0,.58,1), a
     third curve, and swapping it in is how one client comes to feel slower
     than another at an identical duration.

     SETTLE IS NOT THE CARD CURVE, and this is the one mistake the table
     invites. Card, tile and thumb focus is §2.6 and rides --focus-ease above:
     250ms outCubic, the shape of the Roku firmware ramp, written against by
     four client builds. Retiming a poster to 140ms breaks all of them.

     GLIDE, DWELL and DWELL_UP have no consumer in this client yet. GLIDE wants
     a scroll this app performs instantly; DWELL is the Home idle room dim —
     nav leaves, rows go under a rising scrim — which is a Roku behaviour
     nobody has built for a browser. They are declared anyway because the table
     mirrors ThemeMotion() EXACTLY: a table missing rows is how a second table
     gets written the day somebody needs one of them. */
  --ease-out-quad:  cubic-bezier(.25, .46, .45, .94);
  --ease-out-quint: cubic-bezier(.23, 1, .32, 1);
  --dur-settle: 140ms;
  --dur-arrive: 220ms;
  --dur-glide: 300ms;
  --dur-panel: 200ms;
  --dur-dwell: 900ms;
  --dur-dwell-up: 180ms;
  --settle:   var(--dur-settle)   var(--ease-out-quad);
  --arrive:   var(--dur-arrive)   linear;
  --glide:    var(--dur-glide)    var(--ease-out-quint);
  --panel:    var(--dur-panel)    var(--ease-out-quad);
  --dwell:    var(--dur-dwell)    var(--ease-out-quint);
  --dwell-up: var(--dur-dwell-up) var(--ease-out-quad);

  /* THE TYPE LADDER — five sizes, and it is closed.
     The Roku ships exactly five (18/22/26/40/78) forced through one
     ThemeType() function, and this file shipped TWENTY-ONE. That is why one
     heading role was painted 19px, 20px, 22px and 17px on four screens and
     nothing looked like it came from the same app.
     Two rungs are fixed by DESIGN.md:57-62, which measured the site and gives
     the TVs a 1.6x licence: body 14 -> Roku 22, and the section heading
     16 -> Roku 26. The rest follow that ratio. */
  --fs-caption: 12px;   /* badges, durations, meta      -> Roku 18 */
  --fs-body: 14px;      /* body copy, buttons, chips    -> Roku 22 */
  --fs-title: 16px;     /* every section heading        -> Roku 26 */
  --fs-display: 26px;   /* dialog and sheet titles      -> Roku 40 */
  --fs-hero: 42px;      /* the hero title, and only it  -> Roku 78 */
  /* META is not a sixth size. DESIGN-V2 §2.2 META is 22 on the 1920 canvas,
     and the same 1.6x licence that turns body 14 into Roku 22 makes META the
     body rung here. It is named because .row-more and .viewall-count asked for
     var(--fs-meta) while nothing defined it, so both fell back to whatever
     font size they inherited. */
  --fs-meta: var(--fs-body);
  /* THE SCREEN TITLE — every screen's <h1>, and the View All title, at ONE
     size. It was clamp(28px,5vw,52px) in one rule, clamp(30px,6vw,48px) in a
     later rule that overrode it, clamp(28px,3.5vw,44px) on Anime & Manga and a
     fixed 26px on View All: four sizes for one role. It is the only fluid rung
     because a screen title is the one line that has to hold from a phone to a
     television; this is the value .page-heading h1 already rendered at. */
  --fs-page-title: clamp(30px, 6vw, 48px);

  /* THE WEIGHT LADDER — four, and system-ui really has these four.
     650/750/850/900 were being synthesised by the engine rather than selected,
     which is the exact split-rendering hazard the font-stack comment below was
     written to prevent. DESIGN.md:57 sets headings at 600. */
  --fw-regular: 400;
  --fw-medium: 500;
  --fw-semibold: 600;
  --fw-bold: 700;

  /* THE PILL. DESIGN.md:50 — "Buttons and chips: fully rounded". Cards, tiles
     and panels take --radius; buttons and chips take this; avatars take 50%.
     Those three are the whole vocabulary. There were seventeen. */
  --radius-pill: 999px;
  --safe-top: env(safe-area-inset-top, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  min-height: 100%;
  background: var(--bg);
  color: var(--text);
  /* No Inter. DESIGN.md: the platform SYSTEM font, not a shipped face -- and
   nothing here ever loaded Inter (no @font-face, no Google Fonts link), so
   naming it first meant the app rendered in one typeface on a designer's
   Mac and another on everyone else's machine. */
  font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  -webkit-font-smoothing: antialiased;
  overscroll-behavior-y: none;
}

button, input { font: inherit; }
button, a { -webkit-tap-highlight-color: transparent; }
button { cursor: pointer; }
body.no-scroll { overflow: hidden; }
[hidden] { display: none !important; }

/* ── ARRIVE ──────────────────────────────────────────
   DESIGN-V2.md §2.7. A screen enter is a DIP TO BLACK and nothing else: one
   flat, full-viewport plate sitting at opacity 1 and fading to 0 over 220ms
   linear. Not a slide, not a scale, not two screens cross-fading.

   ONE plate, ONE rule, ONE hook — arrive() at the foot of showRoute() in
   app.js, which is the only place in this app a screen changes. A fade written
   per screen is how a product ends up with twenty different enters and no
   contract at all.

   A <div> rather than an opacity or a filter on #app-main, because §2.7 says
   prefer one flat rectangle's opacity over a container's: a container alpha
   composites every card underneath it off-screen, every frame.

   z-index 1000, and it is 1000 because of ONE element. This started at 90,
   which clears the player (60), .wp-join-layer (85), the toast host and the
   profile overlay (80) and the drawer layer (40) — every layer this app had
   been thinking about. It does NOT clear `.media-audio-player` at 900, and
   that one is the only layer in the product that is BOTH above the plate and
   alive during a route change: media-library.js's leave() never hides
   #media-player-host, so the audio bar is a persistent mini-player that
   survives every navigation with a track loaded. MEASURED in headless Comet
   with the plate frozen at opacity 1 — the bar went on painting, fixed, across
   the foot of a screen that is supposed to be entirely black. A dip with a lit
   strip in it is not a dip. 1000 is the full census plus headroom; nothing else
   in styles.css, profile.js or watch-party.js declares anything above 900.

   THE CENSUS IS NOW A TEST, and it had to become one: this paragraph carried a
   bar height ("51px tall") that no harness had ever taken, and re-measuring it
   on 16 Sep 2026 read 117px at 1440x900 — a made-up number sitting inside a
   real finding. arrive.smoke.mjs seeds a saved track, walks off the books room
   and catches the bar mid-dip — not hidden, still painting, under a plate that
   is up — then freezes the plate at opacity 1 and reads back EVERY PIXEL IT
   COVERS: 1,282,500 of them, 0 lit, brightest channel 0. It also enumerates the
   live z-index of every element, so a future layer declared above 1000 is named
   rather than discovered on a TV.

   1,282,500 AND NOT 1,296,000, and the difference is the whole lesson of this
   paragraph repeating itself. 1440x900 is the WINDOW. `position: fixed; inset:
   0` resolves against the LAYOUT viewport, which on the Movies route is
   1425x900 because the page overflows and grows a classic 15px scrollbar — so
   the plate covers 1425 columns and the sixteenth is browser chrome it is not
   supposed to reach. 1,296,000 was arithmetic done on the window size by
   somebody who had not run the census, in the same breath as the "51px" above.
   The harness prints its own width and height every run for exactly this
   reason; read the run, do not multiply.

   A <dialog> opened with showModal() lives in the top layer where no z-index
   reaches it, 1000 included. That is fine and it is checked: every
   route-changing control in this app — `button[data-view]` in the topnav and
   the drawer, and the delegated [data-discover-slug] handler — sits outside
   all five <dialog>s, so showRoute() is never called with one open. A plate
   that covered a modal would be a worse bug than one that does not.

   It is `hidden` between screens, so the rule above takes it out of the
   compositor entirely rather than leaving a full-screen transparent layer
   parked over the app for the life of the session.

   `opacity: 0` on the base rule is the FAIL-SAFE and is not decoration. If the
   animation shorthand below ever fails to resolve — an engine that cannot read
   `var(--arrive)`, say — the element falls back to this rule, which is
   invisible. Were the base opacity 1, the same failure would leave a permanent
   black screen over the whole app. */
#arrive-plate {
  position: fixed;
  inset: 0;
  z-index: 1000;
  background: #000;
  opacity: 0;
  /* It must never eat a click, including during the 220ms it is painting. */
  pointer-events: none;
}
#arrive-plate.arriving { animation: arrive var(--arrive) forwards; }
@keyframes arrive { from { opacity: 1; } to { opacity: 0; } }

@media (prefers-reduced-motion: reduce) {
  /* Under `reduce` ARRIVE is an INSTANT CUT, not a slower fade. A 220ms black
     wipe is the single largest piece of motion on the screen, so "less motion"
     has to mean none of it. app.js never adds the class in the first place;
     this rule is the belt for a viewer who flips the OS switch while the app is
     open — macOS toggles it live, which is the same reason the trailer band
     below carries a CSS half.

     The rule is the web lane's, NOT DESIGN-V2.md §2.7's. §2.7 is the Roku
     contract and says nothing about prefers-reduced-motion — grep it. Do not
     go looking for the citation that used to be on this line; it was wrong.

     THE PANEL HALF OF THIS OVERRIDE IS NOT HERE. It cannot be: it has to be
     declared AFTER the rule it beats. See the block under @keyframes panel-in
     below, and the paragraph there, before moving either of them. */
  #arrive-plate.arriving { animation: none; opacity: 0; }
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

button:focus-visible, a:focus-visible, input:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 3px;
}

/* ── Topbar ─────────────────────────────────────────────── */
.topbar {
  position: sticky;
  top: 0;
  z-index: 20;
  /* A PILL, so it shrinks to its contents: DESIGN.md measures the bar at ~808
     wide on a 1920 page, not as a full-width slab. It was min(1280px, ...),
     which drew a 1280px plate with the chips huddled at the left and a lot of
     empty bar to their right -- the single thing that most made this read as a
     web header rather than as the same floating bar the televisions have. */
  width: max-content;
  max-width: calc(100% - 24px);
  height: 58px;
  /* 16, matching the contract's y=16. It was 10, so the bar sat closer to the
     top edge than every other client's. */
  margin: calc(16px + var(--safe-top)) auto 0;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 5px 7px;
  border: 1px solid rgba(255, 255, 255, .10);
  /* 16, not 20: DESIGN.md gives ONE radius and every card, panel and the bar
     share it. */
  border-radius: var(--radius);
  /* Was rgba(10,10,10,.88) -- the near-black GROUND at near-opacity, so the bar
     did not read as a floating pane at all. The contract is the SURFACE at 55%
     with a 40px blur, which is what makes it float. */
  background: rgba(14, 10, 12, .55);
  box-shadow: 0 14px 40px rgba(0, 0, 0, .5);
  backdrop-filter: blur(40px) saturate(150%);
  -webkit-backdrop-filter: blur(40px) saturate(150%);
  transition: background var(--settle);
}

.brand, .icon-button, .topnav button, .drawer-nav button {
  border: 0;
  color: var(--text);
  background: transparent;
  transition: color var(--settle), background var(--settle);
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  flex: 0 0 auto;
  min-height: 44px;
  padding: 7px 9px;
  font-size: var(--fs-title);
  font-weight: var(--fw-bold);
  letter-spacing: -.03em;
}

.brand-mark {
  display: inline-grid;
  place-items: center;
  width: 27px;
  height: 27px;
  border-radius: var(--radius);
  color: #fff;
  background: linear-gradient(145deg, var(--accent), var(--accent-strong));
  box-shadow: 0 4px 16px var(--accent-glow);
  font-size: var(--fs-body);
  font-weight: var(--fw-bold);
}

.icon-button {
  display: inline-grid;
  place-items: center;
  flex: 0 0 auto;
  width: 44px;
  height: 44px;
  border-radius: var(--radius-pill);
  transition: background var(--settle);
}
.icon-button:hover { background: var(--surface-focus); }
.icon-button svg, .player-close svg { width: 21px; height: 21px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }

.topnav {
  display: flex;
  align-items: center;
  gap: 2px;
  min-width: 0;
  overflow-x: auto;
  scrollbar-width: none;
}
.topnav::-webkit-scrollbar { display: none; }
.topnav button, .drawer-nav button {
  display: inline-flex;
  align-items: center;
  flex: 0 0 auto;
  min-height: 44px;
  border-radius: var(--radius);
  padding: 9px 10px;
  color: var(--muted);
  font-size: var(--fs-body);
  font-weight: var(--fw-bold);
  transition: color var(--settle), background var(--settle);
}
/* Hover is a surface; ACTIVE is the accent. These shared one grey plate, which
   is the "it highlights grey and not red" note against the TVs -- the same bug,
   on the web, months later. Hover stays a quiet accent wash so the two states
   are still told apart. */
.topnav button:hover { color: #fff; background: rgba(255, 61, 71, .20); }
.topnav button.active { color: #fff; background: var(--focus-fill); }
/* No margin-left:auto. That pushed the magnifier to the far right of a
   full-width bar; now the bar is max-content, an auto margin would stretch it
   back out to the max-width and undo the pill. */
.search-button { margin-left: 0; }

/* The profile chip is the only child of .topbar with an unbounded width: it prints
   the active profile's NAME. Logged out it reads "Connect profile" (126px); a long
   name can exceed that without limit, which is what makes the nav breakpoint below
   unprovable by measuring one profile. Cap it at the logged-out width so the
   topbar's requirement is a FIXED number, then the breakpoint can be measured once
   and stay true. See profile.js:2310, which creates this id. */
#profile-connect-button {
  max-width: 126px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ── Drawer ──────────────────────────────────────────────── */
.drawer-layer { position: fixed; inset: 0; z-index: 40; }
.drawer-backdrop { position: absolute; inset: 0; width: 100%; border: 0; background: rgba(0, 0, 0, .65); }
.drawer {
  position: relative;
  z-index: 1;
  width: min(340px, calc(100% - 32px));
  max-height: calc(100vh - 32px);
  max-height: calc(100dvh - 32px);
  margin: 16px;
  padding: 24px;
  overflow-y: auto;
  border: 1px solid rgba(255,255,255,.08);
  border-radius: var(--radius);
  /* rgba(14,14,16) LOOKS like --surface #0E0A0C and is not: it is the old cold
     near-black, one blue step off. At 95% over a warm page that difference is
     visible along the drawer's edge. */
  background: rgba(14, 10, 12, .95);
  box-shadow: 0 25px 80px rgba(0,0,0,.75);
  backdrop-filter: blur(24px) saturate(160%);
  -webkit-backdrop-filter: blur(24px) saturate(160%);
}
.drawer-heading { display: flex; align-items: center; gap: 10px; font-size: var(--fs-display); font-weight: var(--fw-bold); letter-spacing: -.03em; }
.drawer-copy { margin: 13px 0 24px; color: var(--muted); font-size: var(--fs-body); }
.drawer-nav { display: grid; gap: 6px; }
.drawer-nav [data-discover-filters] { display: contents; }
.drawer-nav button { padding: 13px 14px; text-align: left; font-size: var(--fs-body); }
.drawer-nav button:hover { background: rgba(255, 61, 71, .20); color: #fff; }
.drawer-nav button.active { background: var(--focus-fill); color: #fff; }
.drawer-section { margin: 26px 0 8px; color: var(--quiet); font-size: var(--fs-caption); font-weight: var(--fw-bold); letter-spacing: .13em; text-transform: uppercase; }

/* PANEL — §2.7, 200ms outQuad for the drawer and its scrim together.
   An ANIMATION rather than a transition, because openDrawer() opens this by
   clearing `hidden`: display goes none -> block, which no transition can see,
   and an animation runs the moment the element starts being displayed. So the
   slide costs no class to add, no rAF to wait for, and no second piece of state
   in app.js to keep in step with the attribute.
   ENTER ONLY. An exit has to outlive `hidden = true`, which is a JS change this
   does not need. A TV engine too old for the keyframes gets today's hard cut. */
.drawer-layer:not([hidden]) .drawer-backdrop { animation: panel-scrim var(--panel) both; }
.drawer-layer:not([hidden]) .drawer { animation: panel-in var(--panel) both; }
@keyframes panel-scrim { from { opacity: 0; } }
/* The drawer is pinned to the left edge, so it arrives from the left. 24px and
   not its full width: 200ms cannot carry 340px without reading as a snap, and
   §2.7's ceiling forbids buying the distance with a longer duration. */
@keyframes panel-in { from { opacity: 0; transform: translateX(-24px); } }

@media (prefers-reduced-motion: reduce) {
  /* PANEL under `reduce` is an instant open, same law as ARRIVE above.

     THIS BLOCK HAS TO LIVE HERE, under the two rules it cancels, and moving it
     up beside the ARRIVE override is the one edit that silently breaks it.
     A media query adds NO specificity. `.drawer-layer:not([hidden]) .drawer` is
     (0,3,0) whether it sits in a query or not, so against the identical
     selector two lines above, the cascade is decided by source order alone and
     the LAST one wins.

     It was written up there, and it did nothing at all. MEASURED in headless
     Comet with reducedMotion: 'reduce' — the drawer opened with
     animationName: "panel-in", backdropAnimationName: "panel-scrim", both
     animations live, and the panel caught mid-slide at
     transform: matrix(1, 0, 0, 1, -24, 0). A viewer who asks the OS for less
     motion got the whole slide. ARRIVE's override worked only by luck of
     ordering: #arrive-plate.arriving is (1,1,0) in both places and the reduce
     copy happens to come second.

     So: an override goes under its rule, never in one tidy motion block at the
     top of the file. */
  .drawer-layer:not([hidden]) .drawer,
  .drawer-layer:not([hidden]) .drawer-backdrop { animation: none; }
}

/* ── Main layout ─────────────────────────────────────────── */
/* Was min(1440px, 100%). EVERY size in DESIGN.md was measured at 1920, so a
   1440 cap made all of them unreachable at once -- a 160px poster in a 1740
   track and the same poster in a 1404 track are different designs, and this one
   cap is why the web could not look like the Roku no matter what the cards did. */
#app-main { width: min(1920px, 100%); margin: 0 auto; padding-bottom: calc(42px + var(--safe-bottom)); }
.view { padding-top: 18px; }
.hero {
  position: relative;
  min-height: min(52vw, 520px);
  margin: 0 12px 32px;
  overflow: hidden;
  border: 1px solid rgba(255,255,255,.07);
  border-radius: var(--radius);
  background: linear-gradient(145deg, var(--surface-soft), #0a0a0a);
  isolation: isolate;
}
.hero-art, .detail-art { position: absolute; inset: 0; background-position: center right; background-size: cover; background-repeat: no-repeat; opacity: .9; z-index: -1; }
.hero::after { content: ""; position: absolute; inset: 0; z-index: -1; background: linear-gradient(0deg, rgba(10,10,10,.90), transparent 58%); }
.hero-loading .hero-art { animation: pulse 1.4s ease-in-out infinite alternate; background: linear-gradient(110deg, #100C0E, var(--surface-focus), #100C0E); background-size: 180% 100%; }
.hero-content { display: flex; flex-direction: column; justify-content: end; min-height: inherit; width: min(560px, 100%); padding: 48px 24px 28px; }
.eyebrow { margin: 0 0 8px; color: var(--muted); font-size: var(--fs-caption); font-weight: var(--fw-bold); letter-spacing: .12em; text-transform: uppercase; }
.hero h1, .page-heading h1 { margin: 0; font-size: var(--fs-page-title); line-height: 1.02; letter-spacing: -.05em; }
.hero-content > p:not(.eyebrow), .page-heading > p:not(.eyebrow) { max-width: 520px; margin: 12px 0 0; color: var(--muted); font-size: var(--fs-body); line-height: 1.5; }
.hero-actions, .detail-actions { display: flex; flex-wrap: wrap; gap: 9px; margin-top: 22px; align-items: center; }

/* ── The full-bleed home hero ─────────────────────────────────────────────
   Markus, 3 Sep 2026: "i absolutly love how on the home page the continue
   watching fills the whole screen. all devices should look like this and do
   this." See the long override note above .card-backdrop for what this
   replaces and what it deliberately does not.

   FULL-BLEED OUT OF A CENTRED COLUMN. #app-main is width: min(1440px, 100%)
   with auto margins, so on any screen up to 1440px a plain width:100% is
   already edge to edge and nothing else is needed. Above that the band has to
   escape its own container, which is the negative-margin trick — and the
   trick has one well-known bug worth naming, because it produces a horizontal
   scrollbar on the exact desktops this rule is FOR:

     100vw includes the classic vertical scrollbar; the containing block does
     not. On a 1920px display with a 15px scrollbar, 100vw is 1920 while the
     page is only 1905 wide, so the band overhangs 7.5px each side and the
     browser grows a horizontal scrollbar to reach it.

   --scrollbar is measured once in app.js (window.innerWidth minus
   documentElement.clientWidth) and re-measured on resize. It is 0 on macOS and
   iOS overlay scrollbars, 15-17px on Windows, and 0 if the JS never runs — in
   which case this degrades to the ordinary trick with its ordinary overhang,
   not to a broken layout. `overflow-x: hidden` on body was the other option
   and was rejected: the top nav is position: sticky, and a scroll container on
   an ancestor is what stops sticky working. */
:root { --scrollbar: 0px; }
.home-hero {
  position: relative;
  width: calc(100vw - var(--scrollbar));
  max-width: calc(100vw - var(--scrollbar));
  margin: 0 0 30px;
  margin-inline: calc(50% - 50vw + var(--scrollbar) / 2);
  /* 16:9 up to a ceiling. A hero taller than the viewport hides the first row
     completely, and a home screen whose first scroll reveals that there ARE
     rows is a worse home screen — the rows are the app. 74svh, not 74vh:
     mobile Safari's vh is the tallest the toolbar ever gets, so vh alone
     overshoots by the toolbar height on the phone this most matters on. */
  height: min(56.25vw, 74svh, 620px);
  min-height: 320px;
  overflow: hidden;
  background: linear-gradient(145deg, var(--surface-soft), #0a0a0a);
  isolation: isolate;
}
.home-hero-media { position: absolute; inset: 0; z-index: 0; }
.home-hero-art,
.home-hero-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  /* cover, so a 16:9 trailer fills a band that is not 16:9 on a phone rather
     than pillarboxing. Same call the Roku could not make — its Video node
     refuses to crop, which is why HomeScreen.xml had to change the CARD to
     1920x1080 instead. A browser can simply crop. */
  object-fit: cover;
  object-position: center 22%;
}
.home-hero-video {
  /* Invisible until it is genuinely painting frames. app.js adds .playing on
     the video's own `playing` event, so a trailer that 404s, stalls or is
     refused autoplay leaves the artwork on screen instead of a black hole. */
  opacity: 0;
  transition: opacity 700ms ease;
}
.home-hero-video.playing { opacity: 1; }
/* Two scrims, because one cannot do both jobs: the bottom one carries the text
   over whatever the trailer is doing, the left one keeps it readable when the
   action is on that side. Both are gradients rather than a flat wash — the
   Roku learned that the hard way ("these grey boxes need to be removed"). */
.home-hero-scrim {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background:
    linear-gradient(0deg, var(--bg) 2%, rgba(10,10,10,.78) 26%, rgba(10,10,10,.10) 62%, transparent 100%),
    linear-gradient(90deg, rgba(10,10,10,.86) 0%, rgba(10,10,10,.34) 38%, transparent 66%);
}
.home-hero-content {
  position: relative;
  z-index: 2;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  height: 100%;
  /* 900, not 620, since 9 Oct 2026: the synopsis below needs the room. The
     bottom scrim spans the whole band, so the wider copy stays readable. */
  width: min(900px, 100%);
  padding: 0 24px 26px;
}
.home-hero-title {
  margin: 0;
  font-size: clamp(26px, 4.6vw, 54px);
  font-weight: var(--fw-bold);
  line-height: 1.02;
  letter-spacing: -.045em;
  text-shadow: 0 2px 18px rgba(0,0,0,.75);
}
.home-hero-meta { margin: 10px 0 0; color: var(--muted); font-size: var(--fs-body); font-weight: var(--fw-bold); letter-spacing: .02em; }
/* READY TO PLAY: the dwell search found a link that answered (app.js, Play Now,
   BLZ-0108). `good` #4CD48A, DESIGN-V2 §2.5's one green — the Roku's resume
   tag — so "this plays on one press" reads the same on every client. */
.home-hero-ready { color: #4CD48A; letter-spacing: .08em; margin-right: 18px; }
.home-hero-ready[hidden] { display: none; }
/* 860px and 3 lines: ~360 characters. At 560px a synopsis stopped after ~240.
   Markus, on the 55" Roku, 8 Oct 2026: "the full description is cut off" — the
   Roku now fits ~350 (DESIGN-V2 parity, roku channels 6dd9b67). */
.home-hero-synopsis {
  margin: 10px 0 0;
  max-width: 860px;
  color: var(--muted);
  font-size: var(--fs-body);
  line-height: 1.45;
  text-shadow: 0 1px 8px rgba(0,0,0,.8);
  display: -webkit-box;
  overflow: hidden;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
}
.home-hero-actions { display: flex; flex-wrap: wrap; gap: 9px; margin-top: 20px; align-items: center; }
.home-hero-progress { width: min(320px, 70%); height: 4px; margin-top: 16px; border-radius: var(--radius-pill); background: rgba(255,255,255,.22); overflow: hidden; }
.home-hero-progress-fill { height: 100%; border-radius: var(--radius-pill); background: var(--accent); }
.home-hero-mute {
  position: absolute;
  right: 20px;
  bottom: 22px;
  z-index: 3;
  /* 44px, for the same reason every other button in this file is: it is under
     a thumb as often as under a pointer. */
  min-width: 44px;
  min-height: 44px;
  padding: 10px 14px;
  border: 1px solid rgba(255,255,255,.28);
  border-radius: var(--radius-pill);
  background: rgba(10,10,10,.6);
  color: #fff;
  font-size: var(--fs-caption);
  font-weight: var(--fw-bold);
  letter-spacing: .04em;
  backdrop-filter: blur(6px);
}
.home-hero-mute:hover { background: rgba(10,10,10,.85); border-color: var(--accent); }

@media (max-width: 640px) {
  .home-hero { height: min(74svh, 460px); min-height: 300px; }
  .home-hero-content { padding: 0 18px 20px; }
  .home-hero-synopsis { -webkit-line-clamp: 2; font-size: var(--fs-body); }
  .home-hero-mute { right: 14px; bottom: 16px; }
}

/* THE SHELF CINEMA (BLZ-0110, app.js). The Home hero's composition over the
   whole viewport: the same still, the same two washes (.home-hero-scrim), the
   same words at the same width. Roku enterShelfCinema, Apple TV ShelfCinema.

   z-index 50: over the sticky top bar (20), under the player (60) and the
   toasts (80). pointer-events none, and nothing in it can take focus — the card
   under it keeps both, which is the whole design (see app.js). The page colour
   behind the picture, so no row shows through a still that has not loaded. */
.shelf-cinema {
  position: fixed;
  inset: 0;
  z-index: 50;
  overflow: hidden;
  background: var(--bg);
  pointer-events: none;
  opacity: 0;
  transition: opacity 300ms cubic-bezier(.215, .61, .355, 1);
}
.shelf-cinema.open { opacity: 1; }
.shelf-cinema-media { position: absolute; inset: 0; z-index: 0; }
.shelf-cinema-art,
.shelf-cinema-video video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 22%;
}
/* Invisible until it paints frames, exactly as .home-hero-video: a trailer
   that 404s or stalls leaves the still, never a black hole. */
.shelf-cinema-video video { opacity: 0; transition: opacity 700ms ease; }
.shelf-cinema-video video.playing { opacity: 1; }
.shelf-cinema-content {
  position: absolute;
  left: 0;
  bottom: 0;
  z-index: 2;
  display: flex;
  flex-direction: column;
  width: min(900px, 100%);
  padding: 0 24px max(40px, 7vh);
}
/* Faces, not buttons. Same pill as the hero's buttons; a span has no default
   display, so it is given the one a <button> has. */
.shelf-cinema-pill { display: inline-flex; align-items: center; }
.shelf-cinema-pill.secondary-button { color: #fff; }
.shelf-cinema-pill[hidden] { display: none; }

/* NO MOVING PICTURE AT ALL when the viewer has asked for none. This is the
   whole rule, not a slower fade: app.js checks the same media query before it
   ever requests a trailer, so under `reduce` the fleet is never called and the
   <video> stays empty. The CSS half exists so the band is still correct if the
   query flips after the page loaded — macOS toggles it live. */
@media (prefers-reduced-motion: reduce) {
  .home-hero-video { display: none; }
  .home-hero-video.playing { opacity: 0; }
  .shelf-cinema { transition: none; }
  .shelf-cinema-video { display: none; }
}

/* ── Buttons ─────────────────────────────────────────────── */
/* 44px, not 42: every one of these was a hair under the smallest reliable
   touch target (WCAG 2.5.5 / iOS HIG both say 44), and this app is used from a
   phone as often as from a couch. Costs two pixels on desktop. */
.primary-button, .secondary-button, .text-link {
  min-height: 44px;
  border-radius: var(--radius-pill);
  padding: 10px 16px;
  font-size: var(--fs-body);
  font-weight: var(--fw-bold);
  line-height: 1.2;
  text-decoration: none;
  transition: filter var(--settle), background var(--settle), box-shadow var(--settle);
}
/* WATCH NOW IS A WHITE PILL. DESIGN.md:172 — "white fill, #0A0A0B text, pill,
   14px/600, padding 10x20". The Roku ships it exactly, as a baked pill asset
   with a 0x0A0A0BFF label (HomeScreen.xml:352-356). This shipped as an accent
   gradient with white text at weight 800 instead, which is why the one button
   the eye lands on first was the one that matched the TV least.
   The accent still belongs to focus. It is not the resting colour of the
   primary action — §2.6 calls accent "the only focus colour in the product",
   and a button painted accent at rest cannot then show focus in accent. */
.primary-button {
  border: 1px solid #fff;
  color: #0A0A0B;
  background: #fff;
  padding: 10px 20px;
  font-size: var(--fs-body);
  font-weight: var(--fw-semibold);
  box-shadow: 0 8px 22px rgba(0,0,0,.35);
}
.primary-button:hover { background: #fff; filter: brightness(.92); box-shadow: 0 12px 28px rgba(0,0,0,.45); }
.primary-button:disabled { cursor: wait; opacity: .55; }
.secondary-button { border: 1px solid var(--border); color: #fff; background: var(--surface); }
.secondary-button:hover { background: var(--surface-focus); }
.text-link { display: inline-flex; align-items: center; color: #fff; background: transparent; }
.text-link:hover { color: var(--accent); }
/* "Already done" treatment */
.secondary-button.is-spent { border-color: rgba(255,61,71,.5); background: rgba(255,61,71,.16); }
.secondary-button.is-spent:hover { background: rgba(255,61,71,.16); }

/* ── Rows / Cards ────────────────────────────────────────── */
.rows { display: flex; flex-direction: column; gap: 29px; }
.row[hidden] { display: none; }
.row-title { margin: 0 0 11px var(--gutter); font-size: var(--fs-title); font-weight: var(--fw-bold); letter-spacing: -.03em; }

/* ── VIEW ALL — DESIGN-V2 §2.11 ──────────────────────────────────────────────
   The heading moved inside `.row-head` so the button can sit on its baseline.
   Safe by inspection rather than by hope: every `.row-title` rule in this file
   and in the six sibling scripts is a DESCENDANT selector, so none of them
   breaks on the extra wrapper. `.row-title` keeps its own left gutter, which is
   why this row is not padded — padding it would double that margin and push
   every shelf heading out of line with the cards beneath it. */
.row-head { display: flex; align-items: baseline; gap: 12px; }
.row-head .row-title { flex: 1 1 auto; min-width: 0; }
.row-more {
  flex: 0 0 auto;
  margin: 0 var(--gutter) 11px 0;
  padding: 6px 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--muted);
  font-size: var(--fs-meta);
  font-weight: var(--fw-bold);
  /* SETTLE, not the card law. §2.6 is explicit: a button, chip or row control
     is 140ms outQuad; 250ms outCubic is for a card, tile, poster or thumb.
     "Focus is focus" is the reasoning §2.6 exists to refuse. */
  transition: color var(--settle), background var(--settle), border-color var(--settle);
}
.row-more:hover { color: #fff; background: var(--surface-focus); border-color: var(--accent); }
.row-more:focus-visible { outline: none; color: #fff; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-glow); }

.viewall-head { margin: 0 var(--gutter) 18px; }
.viewall-title { margin: 0; font-size: var(--fs-page-title); font-weight: var(--fw-bold); letter-spacing: -.03em; }
.viewall-count { margin: 4px 0 0; color: var(--muted); font-size: var(--fs-meta); }
.row-track, .result-grid {
  display: flex;
  gap: 12px;
  overflow-x: auto;
  padding: 3px var(--gutter) 9px;
  scroll-snap-type: x proximity;
  scrollbar-width: none;
}
.row-track::-webkit-scrollbar, .result-grid::-webkit-scrollbar { display: none; }

/* ── Poster card ─────────────────────────────────────────── */
.card {
  position: relative;
  flex: 0 0 auto;
  width: 132px;
  aspect-ratio: 2 / 3;
  overflow: hidden;
  padding: 0;
  border: 1px solid rgba(255,255,255,.07);
  border-radius: var(--radius);
  color: #fff;
  background: var(--surface);
  box-shadow: 0 8px 22px rgba(0,0,0,.3);
  scroll-snap-align: start;
  text-align: left;
  /* opacity joins these because the focus law below ramps it, and the curve is
     the Roku's outCubic rather than the app's generic 300ms ease — the growth
     and the brightening have to arrive together or the card reads as two
     separate events.

     AND THAT ARGUMENT REACHES EVERY PART OF A CARD, which is why --focus-ease
     now also drives .card-image, .card-backdrop, .card-hero-content,
     .card-mute-btn, .row-hero .card, .yt-thumb, .livetv-card and
     .livetv-guide-block below. All eight were left on the deleted generic
     token, so the poster grew on one curve while the artwork under it faded on
     another. These are §2.6 list-item focus and they stay at 250ms: SETTLE is
     140ms and is for NON-list focus — buttons, chips, drawer rows. Retiming a
     card to SETTLE breaks the four client builds written against §2.6. */
  transition: transform var(--focus-ease), opacity var(--focus-ease),
              border-color var(--focus-ease), box-shadow var(--focus-ease);
}
/* ── THE FOCUS LAW ───────────────────────────────────────────
   DESIGN-V2.md §2.6, "Focus — one law, one colour, everywhere". The Roku
   implements it in Theme.brs (ThemeFocusRamp / ThemeDimFloor / ThemeFocusEase)
   and every item component asks that one function. This is the same law.

   The contract's three tiers:

     | Property        | Focused | Sibling in focused row | Cold row |
     |-----------------|---------|------------------------|----------|
     | Artwork opacity | 1.00    | 0.65                   | 0.45     |
     | Card scale      | 1.00    | 0.93                   | 0.93     |
     | Accent bar      | 1.00    | 0                      | 0        |

   THE ONE DELIBERATE ADAPTATION, and it is not a shortcut. On a d-pad
   something is ALWAYS focused, so Roku can dim from a resting state of 0.45
   and never look wrong. A browser is frequently focused on NOTHING — a page
   just loaded, a mouse sitting still — and a literal port would leave the
   whole app dimmed to 45% forever with nothing to un-dim it.

   So the dim is conditional on engagement rather than resting: with nothing
   engaged the grid sits at 1.00, and the instant any card takes hover or
   keyboard focus the full three-tier ramp applies. Every value below is the
   contract's; only the trigger differs.

   This also degrades safely. An engine without :has() simply never matches the
   two dimming rules and gets the old flat behaviour plus the accent bar —
   never a page stuck dim.

   DELETED HERE, on purpose: `box-shadow: 0 0 0 2px var(--accent)`. §2.6's
   chrome list is exhaustive and ends "Nothing else. No rings, no white halos,
   no lavender, no per-screen colour." That ring was the ring. The accent bar
   below is what replaces it, and it is what the Roku actually draws. */

/* Cold row — every card dims once anything is engaged. Contract: 0.45 / 0.93. */
.rows:has(.card:hover, .card:focus-visible) .card,
.result-grid:has(.card:hover, .card:focus-visible) .card {
  opacity: .45;
  transform: scale(.93);
}

/* Sibling inside the engaged row. Contract: 0.65, and it keeps the 0.93 scale. */
.row:has(.card:hover, .card:focus-visible) .card,
.result-grid:has(.card:hover, .card:focus-visible) .card:not(:hover):not(:focus-visible) {
  opacity: .65;
}

/* The focused card itself. Contract: 1.00 / 1.00.
   Source order is not enough here. The cold-row rule above is (0,4,0), and a
   bare `.card:focus-visible` is only (0,2,0), so without its own grid-scoped
   copy the focused poster in Library and the games search grid was the DIMMEST
   card on the screen (.45 / .93). Each container that dims needs its own
   focused selector at (0,5,0). */
.rows:has(.card:hover, .card:focus-visible) .card:hover,
.rows:has(.card:hover, .card:focus-visible) .card:focus-visible,
.result-grid:has(.card:hover, .card:focus-visible) .card:hover,
.result-grid:has(.card:hover, .card:focus-visible) .card:focus-visible,
.card:hover, .card:focus-visible {
  opacity: 1;
  transform: scale(1);
  border-color: var(--accent);
  box-shadow: 0 20px 40px rgba(255,61,71,.28), 0 8px 22px rgba(0,0,0,.4);
  z-index: 2;
}

/* The one accent bar. §2.6: height 6, width = artwork width, solid accent.
   The card clips its own overflow, so it sits on the bottom edge rather than
   6px below — the same mark in the only place this box can draw it. */
.card::after {
  content: "";
  position: absolute;
  right: 0;
  bottom: 0;
  left: 0;
  height: 6px;
  background: var(--accent);
  opacity: 0;
  transition: opacity var(--focus-ease);
  pointer-events: none;
}
.card:hover::after, .card:focus-visible::after { opacity: 1; }
.card:active { transform: scale(.97); }
.card-image { display: block; width: 100%; height: 100%; object-fit: cover; background: linear-gradient(135deg, #1B1417, #0E0A0C); transition: opacity var(--focus-ease); }
/* .seerr-art joins these: the Requests poster is a real <img> now, like every
   other card in the app, so it needs the same dead-URL mark rather than a
   private one that would drift from this. */
.card.no-image::before, .seerr-art.no-image::before { content: "B"; position: absolute; inset: 0; display: grid; place-items: center; color: rgba(255,255,255,.34); font-size: var(--fs-hero); font-weight: var(--fw-bold); }
.card.no-image .card-image, .seerr-art.no-image .card-image { visibility: hidden; }
.card-label { position: absolute; right: 0; bottom: 0; left: 0; padding: 32px 10px 9px; background: linear-gradient(0deg, rgba(0,0,0,.93), rgba(0,0,0,0)); font-size: var(--fs-caption); font-weight: var(--fw-bold); line-height: 1.25; text-shadow: 0 1px 4px #000; display: -webkit-box; overflow: hidden; -webkit-box-orient: vertical; -webkit-line-clamp: 2; }
.card-source { position: absolute; top: 8px; left: 8px; border-radius: var(--radius-pill); padding: 4px 7px; color: #fff; background: rgba(255,61,71,.92); font-size: var(--fs-caption); font-weight: var(--fw-bold); letter-spacing: .04em; }
.skeleton { background: linear-gradient(100deg, #100C0E 30%, #1B1417 50%, #100C0E 70%); background-size: 200% 100%; animation: shimmer 1.3s infinite linear; box-shadow: none; pointer-events: none; }

/* ── Hero-inside-the-row (DebridStream reference) ───────────
   One band does both jobs: no separate hero banner. The first row with
   content (claimHeroRow, app.js) carries .row-hero; every card already has
   the backdrop/title/meta/synopsis markup (buildCard, app.js) but it stays
   invisible and the card stays plain-poster width everywhere else — only a
   .row-hero ancestor turns the expand-on-focus behaviour on. At rest a
   row-hero row is indistinguishable from any other row, which is the point:
   neighbours stay ordinary portrait posters right up until one is focused.

   ─── OVERRIDDEN 3 Sep 2026, deliberately, and only in one direction ───
   "No separate hero banner" is no longer true: .home-hero (further down this
   file) is a full-bleed moving band above the rows on the home route.

   Markus: "i absolutly love how on the home page the continue watching fills
   the whole screen. all devices should look like this and do this." That is
   the Roku, which now runs its hero as a 1920x1080 Video node at translation
   [0,0]. Five of the six clients had a hero; the web had a .hero-art rule with
   nothing behind it and a home screen that opened on a row of posters.

   EVERY WORD ABOVE STILL HOLDS FOR THE ROWS, and none of it was deleted. The
   argument it makes is against a banner that DUPLICATES the first row's job —
   a static poster-and-blurb slab showing the same title the row below it
   shows. .home-hero does a different job: it is the ONE title you were already
   part-way through, playing, with a resume bar under it. Nothing about
   .row-hero changed: claimHeroRow still runs, every qualifying row still
   expands on focus, and a resting row-hero row is still indistinguishable
   from any other row. If .home-hero ever finds nothing to show it stays
   hidden and the screen is exactly what this comment describes. */
.card-backdrop {
  position: absolute;
  inset: 0;
  z-index: 1;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity var(--focus-ease);
  pointer-events: none;
}
.card-hero-content {
  position: absolute;
  /* Above the trailer, which is now z-index 2 - the title and synopsis have to
     stay readable over moving video. */
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 3;
  padding: 16px 18px 14px;
  background: linear-gradient(0deg, rgba(0,0,0,.95) 10%, rgba(0,0,0,.55) 60%, transparent);
  opacity: 0;
  transform: translateY(6px);
  transition: opacity var(--focus-ease), transform var(--focus-ease);
  pointer-events: none;
}
.card-hero-title { margin: 0; font-size: var(--fs-title); font-weight: var(--fw-bold); letter-spacing: -.02em; line-height: 1.15; }
.card-meta-line { margin: 6px 0 0; color: var(--muted); font-size: var(--fs-caption); font-weight: var(--fw-bold); letter-spacing: .01em; }
.card-synopsis {
  margin: 8px 0 0;
  color: var(--muted);
  font-size: var(--fs-caption);
  line-height: 1.4;
  display: -webkit-box;
  overflow: hidden;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
}
.card-cta { display: inline-block; margin-top: 10px; color: var(--accent); font-size: var(--fs-caption); font-weight: var(--fw-bold); letter-spacing: .02em; }

.row-hero .row-track { align-items: flex-start; }
/* KNOWN DEPARTURE FROM §2.7, recorded rather than quietly fixed: this is the one
   rule in the app that animates `width`, which the contract forbids because it
   relayouts the row every frame. It cannot become a transform without also
   rewriting what `width`/`aspect-ratio` buy here — the portrait-to-landscape
   switch below, and the height cap that stops every row under it from jumping.
   That is a redesign of the hover state, not a retime, so it is out of scope
   for the motion pass. Only the curve moved. */
.row-hero .card {
  transition: width var(--focus-ease), aspect-ratio var(--focus-ease), transform var(--focus-ease), border-color var(--focus-ease), box-shadow var(--focus-ease);
}
.row-hero .card:hover,
.row-hero .card:focus-visible {
  /* The base .card aspect-ratio is 2/3 (portrait) — left alone, widening
     ALONE grows a 432px-wide card to 648px tall, pushing card-hero-content
     off the bottom of the screen entirely. Switching to a landscape ratio
     here is what actually makes this "hero-inside-the-row" instead of just
     "one portrait poster, bigger." */
  /* THE EXPANDED CARD MUST NOT BE TALLER THAN THE RESTING ONE. A resting card
     is 132px wide at 2/3, so 198px tall, and the row is only as tall as its
     cards. While one row did this it did not matter much — the top row grew and
     the page nudged. Now that EVERY row does it, a taller hover state would
     shove every row below it down the page each time the pointer crossed a
     card, and back up when it left.
     At 16/10 the height is width / 1.6, so 316px is the widest that still fits
     inside 198px. The row height is therefore constant and nothing moves except
     the card under the pointer. */
  width: clamp(240px, 24vw, 316px);
  aspect-ratio: 16 / 10;
  transform: none;
  z-index: 5;
}
.row-hero .card:hover .card-backdrop,
.row-hero .card:focus-visible .card-backdrop,
.row-hero .card:hover .card-hero-content,
.row-hero .card:focus-visible .card-hero-content {
  opacity: 1;
  transform: none;
}
.row-hero .card:hover .card-image,
.row-hero .card:focus-visible .card-image,
.row-hero .card:hover .card-label,
.row-hero .card:focus-visible .card-label {
  opacity: 0;
}

@media (max-width: 640px) {
  /* A PHONE GETS THIS TOO. Markus, 2026-08-29: "this should also be on all
     devices". This used to hold the expanded width at the resting 118px, which
     did two things, neither of them good: it switched the treatment off on
     every phone, and — because the 16/10 aspect-ratio above still applied — a
     card that did get focus went 118x74, SHORTER than the 177px it was resting
     at, so the row collapsed around it.
     Narrower, because the screen is narrower, and the same rule as the desktop
     one holds: no taller than a resting card, or the rows below jump. A resting
     card here is 118px at 2/3, so 177px tall; 280px at 16/10 is 175px. 74vw
     keeps it inside the viewport on the smallest phones. */
  .row-hero .card:hover, .row-hero .card:focus-visible { width: min(74vw, 280px); }
}

/* ── Card-hover trailer embed ────────────────────────────── */
/* The trailer preview replaces the poster image on hover.    */
.card-trailer-wrap {
  position: absolute;
  inset: 0;
  /* ABOVE .card-backdrop (z-index 1), BELOW .card-hero-content. At z-index 1 it
     tied with the backdrop and the backdrop won on paint order, so the trailer
     played underneath a still image. */
  z-index: 2;
  opacity: 0;
  transition: opacity 400ms ease;
  pointer-events: none;
  background: #000;
}
.card-trailer-wrap.visible { opacity: 1; }
.card-trailer-wrap video,
.card-trailer-wrap iframe {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border: 0;
}
/* Mute toggle that appears on the hovered card */
.card-mute-btn {
  position: absolute;
  bottom: 36px;
  right: 6px;
  z-index: 3;
  width: 28px;
  height: 28px;
  border: 0;
  border-radius: 50%;
  background: rgba(0,0,0,.7);
  color: #fff;
  font-size: var(--fs-body);
  line-height: 1;
  display: grid;
  place-items: center;
  opacity: 0;
  transition: opacity var(--focus-ease);
  cursor: pointer;
}
.card:hover .card-mute-btn { opacity: 1; }

/* ── Inner pages ─────────────────────────────────────────── */
.page-heading { max-width: 760px; margin: 44px auto 24px; padding: 0 20px; }
/* Admin's one panel. No rule in this file had ever named .admin-panel, so it
   sat unstyled at x=0 under a heading centred on a 760px track: the left edge
   of the panel and of its heading were 360px apart on a desktop. Same track as
   .page-heading's text, on the warm surface with the one hairline. */
.admin-panel { width: min(720px, calc(100% - 40px)); margin: 0 auto 24px; padding: 24px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); }
.admin-panel h2 { margin: 0; font-size: var(--fs-title); font-weight: var(--fw-semibold); }
.admin-panel p { margin: 12px 0 0; color: var(--text-body); font-size: var(--fs-body); line-height: 1.5; }
.admin-panel .muted { color: var(--muted); }
.search-form { display: flex; gap: 10px; max-width: 760px; margin: 0 auto; padding: 0 20px; }
.search-form input { width: 100%; min-height: 46px; border: 1px solid rgba(255,255,255,.12); border-radius: var(--radius); padding: 0 15px; outline: none; color: var(--text); background: var(--surface); transition: border-color var(--settle), box-shadow var(--settle); }
.search-form input::placeholder { color: var(--quiet); }
.search-form input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(255,61,71,.18); }
.search-status { min-height: 24px; max-width: 760px; margin: 16px auto 0; padding: 0 20px; color: var(--muted); font-size: var(--fs-body); }
.search-results, .library-results { margin-top: 24px; }

/* SETTINGS (B32). One column of full-width rows that CYCLE on press, which is
   the shape Fire TV's rebuildPlaybackForm and Roku's SettingsScreen both use.
   It is kept on the web because this same file is what a webOS and a Tizen
   television run, where the only input is a D-pad and there is no place to type
   a number — and because dpad.js moves focus by GEOMETRY, so one column of
   equal-width rows is the layout it navigates correctly. A mouse loses nothing.
   .settings-row stays 48px+ tall for the same tap-target reason .search-form
   input does. */
.settings-host { max-width: 760px; margin: 0 auto; padding: 0 20px 64px; }
.settings-section { margin: 32px 0 12px; color: var(--muted); font-size: 12px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; }
.settings-section:first-child { margin-top: 8px; }
.settings-row { display: block; width: 100%; min-height: 52px; margin-top: 8px; border: 1px solid rgba(255,255,255,.12); border-radius: var(--radius); padding: 13px 16px; color: var(--text); background: var(--surface); font-size: var(--fs-body); font-weight: 700; text-align: left; transition: border-color var(--settle), background var(--settle); }
.settings-row:hover:not(:disabled), .settings-row:focus-visible { border-color: var(--accent); background: var(--surface-focus); outline: none; }
.settings-row:disabled { opacity: .5; cursor: not-allowed; }
.settings-input { width: 100%; min-height: 48px; margin-top: 8px; border: 1px solid rgba(255,255,255,.12); border-radius: var(--radius); padding: 0 15px; outline: none; color: var(--text); background: var(--surface); font-size: var(--fs-body); }
.settings-input::placeholder { color: var(--quiet); }
.settings-input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(255,61,71,.18); }
.settings-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 10px; }
.settings-note { margin: 10px 0 0; color: var(--muted); font-size: var(--fs-body); line-height: 1.45; }
.settings-note[data-tone="error"] { color: #ff8a8f; }
.settings-note[data-tone="success"] { color: #7ee2a8; }
.settings-language { margin-top: 8px; }
.settings-about { display: grid; grid-template-columns: minmax(0, 9rem) minmax(0, 1fr); gap: 6px 16px; margin: 10px 0 0; color: var(--muted); font-size: var(--fs-body); }
.settings-about dt { font-weight: 700; }
.settings-about dd { margin: 0; overflow-wrap: anywhere; }
/* Send feedback (feedback.js), drawn inside .settings-host. */
.feedback-kinds { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
.feedback-kind { min-height: 48px; border: 1px solid rgba(255,255,255,.12); border-radius: 999px; padding: 0 18px; color: var(--text); background: var(--surface); font-size: var(--fs-body); font-weight: 700; transition: border-color var(--settle), background var(--settle); }
.feedback-kind:hover, .feedback-kind:focus-visible { border-color: var(--accent); background: var(--surface-focus); outline: none; }
.feedback-kind[aria-checked="true"] { border-color: var(--accent); background: var(--accent); color: #fff; }
.feedback-label { display: block; margin-top: 18px; color: var(--muted); font-size: var(--fs-body); font-weight: 700; }
.feedback-message { min-height: 128px; padding: 12px 15px; resize: vertical; line-height: 1.45; font-family: inherit; }
.feedback-log { max-height: 260px; overflow: auto; margin: 8px 0 0; border: 1px solid rgba(255,255,255,.12); border-radius: var(--radius); padding: 12px 14px; color: var(--muted); background: var(--surface); font-size: 13px; line-height: 1.5; white-space: pre-wrap; overflow-wrap: anywhere; }

/* STREAM SOURCES (B2). The list on the left and the detail panel on the right,
   which is tvOS's StreamSourcesView layout (a 900pt ScrollView beside an 800pt
   panel) at browser proportions. It collapses to one column at 900px, where the
   panel follows the list instead of sitting beside it.

   .sources-row reuses .settings-row's metrics on purpose — same border, same
   radius, same 52px floor, same hover and focus treatment — because a row here
   and a row on Settings are the same control to a hand and to dpad.js, which
   moves focus by GEOMETRY. It is NOT .settings-row itself: this one lays its
   name and its meta out on one line with the meta pushed right, which is the
   Roku's own `name + Chr(9) + RowMeta` shape.

   READ-ONLY, SO THERE IS NO CONTROL STYLE HERE. No input, no switch, no primary
   button — sources.js draws none, and a style for one would be the first step
   back towards the thing the audit forbade. */
.sources-host { max-width: 1240px; margin: 0 auto; padding: 0 20px 64px; }
.sources-layout { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); gap: 28px; align-items: start; margin-top: 18px; }
.sources-list { min-width: 0; }
.sources-eyebrow { margin: 0; color: var(--quiet); font-size: var(--fs-caption); font-weight: var(--fw-bold); letter-spacing: .13em; text-transform: uppercase; }
.sources-list-heading { margin: 26px 0 4px; }
.sources-card { margin: 0; border: 1px solid rgba(255,255,255,.12); border-radius: var(--radius); padding: 20px; background: var(--surface); }
.sources-card-title { margin: 10px 0 0; font-size: 20px; font-weight: 800; }
.sources-card-base { margin: 6px 0 0; color: var(--muted); font-size: var(--fs-body); overflow-wrap: anywhere; }
.sources-card-copy { margin: 10px 0 0; color: var(--muted); font-size: var(--fs-body); line-height: 1.5; }
.sources-card-meta { margin: 10px 0 0; color: var(--quiet); font-size: var(--fs-body); }
.sources-row { display: flex; align-items: baseline; gap: 14px; width: 100%; min-height: 52px; margin-top: 8px; border: 1px solid rgba(255,255,255,.12); border-radius: var(--radius); padding: 13px 16px; color: var(--muted); background: var(--surface); font-size: var(--fs-body); text-align: left; transition: border-color var(--settle), background var(--settle), color var(--settle); }
.sources-row:hover, .sources-row:focus-visible, .sources-row[aria-pressed="true"] { border-color: var(--accent); color: var(--text); background: var(--surface-focus); outline: none; }
.sources-row-name { flex: 1 1 auto; min-width: 0; overflow: hidden; font-weight: 700; text-overflow: ellipsis; white-space: nowrap; }
.sources-row-meta { flex: 0 0 auto; color: var(--quiet); font-size: var(--fs-caption); white-space: nowrap; }
.sources-detail { position: sticky; top: 96px; border: 1px solid rgba(255,255,255,.12); border-radius: var(--radius); padding: 22px; background: var(--surface); }
.sources-detail-title { margin: 0; font-size: 22px; font-weight: 800; }
.sources-detail-meta { margin: 8px 0 0; color: var(--quiet); font-size: var(--fs-body); }
.sources-detail-copy { margin: 12px 0 0; color: var(--muted); font-size: var(--fs-body); line-height: 1.5; }
.sources-footer { margin-top: 22px; border-top: 1px solid rgba(255,255,255,.10); padding-top: 16px; }
.sources-footer-line { margin: 0 0 10px; color: var(--quiet); font-size: var(--fs-body); line-height: 1.5; }
.sources-footer-line:last-child { margin-bottom: 0; }
.sources-note { margin: 10px 0 0; color: var(--muted); font-size: var(--fs-body); line-height: 1.45; }
.sources-note[data-tone="error"] { color: #ff8a8f; }
@media (max-width: 900px) {
  .sources-layout { grid-template-columns: minmax(0, 1fr); }
  /* Not sticky in one column: it would pin the panel over the list it describes. */
  .sources-detail { position: static; }
}
.result-row { margin-bottom: 29px; }

/* ── B22: the Library's three saved-title sections ────────
   Watchlist, Collection and Watched, each a .result-row with a count in its
   heading, a one-line description under it and a Remove beside every poster.
   The section and grid classes are the ones the old single "Saved titles" grid
   used, so the focus law above applies to these cards unchanged. */
.library-status { margin: 12px var(--gutter) 0; color: var(--muted); font-size: var(--fs-body); }
#library-retry { margin: 12px 0 0 var(--gutter); }
.list-section-copy { margin: -6px var(--gutter) 10px; color: var(--muted); font-size: var(--fs-body); }
/* The flex item is the PAIR now, not the card. The card keeps its own width, so
   the column is exactly as wide as the poster and the Remove sits under it. */
.list-item { display: flex; flex: 0 0 auto; flex-direction: column; gap: 8px; }
.list-actions { display: flex; flex-direction: column; gap: 6px; }
.list-confirm-copy { margin: 0; color: var(--muted); font-size: var(--fs-body); line-height: 1.3; }
.list-keep {
  min-height: 34px;
  padding: 0 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  color: #fff;
  background: var(--surface);
  font: inherit;
  font-size: var(--fs-body);
  font-weight: var(--fw-bold);
  cursor: pointer;
}
.list-keep:hover, .list-keep:focus-visible { background: var(--surface-focus); }
.list-remove {
  min-height: 34px;
  padding: 0 12px;
  border: 1px solid rgba(255,61,71,.42);
  border-radius: var(--radius-pill);
  color: #ff8a8f;
  background: rgba(255,61,71,.12);
  font: inherit;
  font-size: var(--fs-body);
  font-weight: var(--fw-bold);
  cursor: pointer;
  transition: background var(--focus-ease), border-color var(--focus-ease);
}
.list-remove:hover, .list-remove:focus-visible { border-color: rgba(255,61,71,.8); background: rgba(255,61,71,.24); }
.list-remove[disabled] { opacity: .5; cursor: default; }

/* ── Emby library browse ─────────────────────────────────── */
.emby-controls { display: flex; align-items: center; justify-content: space-between; gap: 16px; max-width: 760px; margin: 0 auto; padding: 0 20px; flex-wrap: wrap; }
.emby-tabs { display: flex; gap: 8px; }
.emby-tab {
  min-height: 44px;
  border: 1px solid rgba(255,255,255,.16);
  border-radius: var(--radius-pill);
  padding: 8px 18px;
  color: var(--muted);
  background: rgba(255,255,255,.06);
  font-size: var(--fs-body);
  font-weight: var(--fw-bold);
}
.emby-tab:hover { background: rgba(255,255,255,.11); }
.emby-tab[aria-selected="true"] { color: var(--text); background: var(--accent); border-color: var(--accent); }
#emby-sort {
  min-height: 44px;
  border: 1px solid rgba(255,255,255,.16);
  border-radius: var(--radius);
  padding: 8px 12px;
  color: var(--text);
  background: var(--surface);
  font-size: var(--fs-body);
}
#emby-load-more { display: block; margin: 28px auto 40px; }

/* ── Search (DebridStream reference: "the search context is visually its
   own place") ───────────────────────────────────────────── */
/* A second surface, not a second brand color — the deliberate exception
   DESIGN.md calls out. Tinted teal-blue, not the near-black --bg every
   other screen uses; --accent stays red here too, same as everywhere. */
/* `margin: 0 -20px` used to pull the teal ground out past its parent to fake a
   full-bleed. #app-main is `width: min(1440px, 100%)`, so under 1440px there is
   no gutter for it to pull back into and the section simply hung 20px off both
   edges: measured 1420px of document against a 1400px window (a real horizontal
   scrollbar on desktop), and left:-20 right:410 inside a 390px phone. The ground
   is full-bleed because the section already spans its parent, not because it
   overhangs it. dvh, not vh: on iOS the URL bar makes 100vh taller than the
   screen, which is the difference between "fills" and "scrolls by a bar". */
.search-view { min-height: calc(100vh - 64px); min-height: calc(100dvh - 64px); padding: 28px 20px 40px; background: #0a1620; }
.search-view .search-form { max-width: 640px; margin: 0 auto; align-items: center; }
.search-mic {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  width: 46px;
  height: 46px;
  border: 1px solid rgba(255,255,255,.14);
  border-radius: 50%;
  color: var(--text);
  background: rgba(255,255,255,.05);
  transition: background var(--settle), border-color var(--settle);
}
.search-mic svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.search-mic:hover { background: rgba(255,255,255,.1); }
.search-mic.listening { border-color: var(--accent); color: var(--accent); background: rgba(255,61,71,.14); animation: pulse 1.1s ease-in-out infinite alternate; }
.search-chip-row { max-width: 640px; margin: 12px auto 0; padding: 0 20px; }
.search-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 44px;
  border: 1px solid rgba(255,255,255,.16);
  border-radius: var(--radius-pill);
  padding: 7px 14px;
  color: var(--text);
  background: rgba(255,255,255,.06);
  font-size: var(--fs-body);
  font-weight: var(--fw-bold);
}
.search-chip:hover { background: rgba(255,255,255,.11); }
.search-view .search-status { max-width: 640px; }

.search-body { display: flex; gap: 28px; max-width: 1200px; margin: 20px auto 0; padding: 0 20px; align-items: flex-start; }
.search-facets {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 0 0 190px;
  max-height: 70vh;
  overflow-y: auto;
  padding-right: 6px;
}
.search-facet {
  display: flex;
  align-items: center;
  min-height: 44px;
  padding: 9px 12px;
  border-radius: var(--radius);
  color: var(--muted);
  background: transparent;
  font-size: var(--fs-body);
  font-weight: var(--fw-bold);
  text-align: left;
  transition: background var(--settle), color var(--settle);
}
.search-facet:hover { background: rgba(255,255,255,.06); color: var(--text); }
.search-facet.active { color: #fff; background: rgba(255,61,71,.18); }
.search-view .search-results { flex: 1; display: grid; grid-template-columns: repeat(5, 1fr); gap: 16px; margin-top: 0; }
/* .card is a fixed 132-158px everywhere else (a horizontally-scrolling row
   doesn't care), but a grid TRACK here is usually wider than that — left
   alone the card just sits stranded at the left of its cell instead of
   filling the "5 across" grid the reference shows. */
.search-view .search-results .card { width: 100%; }

@media (max-width: 900px) {
  .search-body { flex-direction: column; }
  .search-facets { flex-direction: row; flex-wrap: wrap; max-height: none; width: 100%; }
  .search-view .search-results { grid-template-columns: repeat(3, 1fr); width: 100%; }
}
@media (max-width: 520px) {
  .search-view .search-results { grid-template-columns: repeat(2, 1fr); }
}
.result-grid { flex-wrap: wrap; overflow: visible; padding-bottom: 0; }
/* A grid page card is NOT a row card. DESIGN.md measures the row at 160x240 and
   the grid at 238x356; the web drew both at the row size, so /movies looked
   like a denser home rather than its own page. Only from 1280 up, so phones
   keep the row size and do not get a two-across grid of very large posters. */
@media (min-width: 1280px) {
  .result-grid .card { width: 238px; }
}
.empty-copy { margin: 20px; color: var(--muted); }
/* DESIGN.md measures a roadmap card at 582x392, three across with a 16px gap
   -- a 1778px block. This was capped at 920px, so the eight franchise cards
   drew at roughly 285x190: half size, and the only screen where the web was
   less than half the Roku's. The cap stays as a max-width so narrow windows
   still collapse instead of overflowing. */
.roadmap-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(0, 582px)); gap: 16px; max-width: 1778px; margin: 0 auto; padding: 0 var(--gutter); justify-content: center; }

/* B25. A franchise card, not a blurb card. The three <article>s this block used
   to dress were a static product pitch; these are buttons over the franchise's
   own TMDB backdrop, which is why 582x392 was the right box all along -- a
   poster does not fill it, a backdrop does. The same left-to-right darkening
   roadmaps.js paints inline keeps the name readable over a light still, exactly
   as .home-hero-scrim and the detail art do. */
.roadmap-card {
  display: flex;
  align-items: flex-end;
  min-height: 392px;
  overflow: hidden;
  padding: 0;
  border: 1px solid rgba(255,255,255,.08);
  border-radius: var(--radius);
  color: var(--text);
  background: var(--surface) center / cover no-repeat;
  text-align: left;
  transition: border-color var(--focus-ease), box-shadow var(--focus-ease), transform var(--focus-ease);
}
.roadmap-card:hover, .roadmap-card:focus-visible { border-color: rgba(255,61,71,.5); box-shadow: 0 8px 24px rgba(255,61,71,.14); outline: none; transform: scale(1.01); }
.roadmap-card.no-art { background-image: linear-gradient(135deg, #1B1417, #0E0A0C); }
.roadmap-card-body { display: flex; flex-direction: column; gap: 6px; width: 100%; padding: 22px; }
.roadmap-card-name { font-size: var(--fs-display); font-weight: var(--fw-bold); letter-spacing: -.03em; line-height: 1.1; }
.roadmap-card-meta { color: var(--accent); font-size: var(--fs-body); font-weight: var(--fw-bold); }
.roadmap-card-copy {
  color: var(--muted); font-size: var(--fs-body); line-height: 1.5;
  display: -webkit-box; overflow: hidden; -webkit-box-orient: vertical; -webkit-line-clamp: 3;
}
.roadmaps-hint { max-width: 760px; margin: 0 auto 16px; padding: 0 20px; color: var(--muted); font-size: var(--fs-body); }

/* ── One franchise ───────────────────────────────────────────
   The backdrop is confined to the header block, the way tvOS's RoadmapHeader
   and the Roku's `one` group both confine it: a full-bleed art behind a wall of
   posters makes every poster harder to read. */
.roadmap-header {
  margin: 0 auto 8px;
  padding: 36px var(--gutter) 28px;
  border-bottom: 1px solid rgba(255,255,255,.1);
  background: transparent center / cover no-repeat;
}
.roadmap-header-body { max-width: 1180px; margin: 0 auto; }
.roadmap-header-body h1 { margin: 16px 0 0; font-size: var(--fs-page-title); letter-spacing: -.03em; }
.roadmap-about { max-width: 900px; margin: 10px 0 0; color: var(--muted); font-size: var(--fs-body); line-height: 1.5; }
.roadmap-header-foot { display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; margin-top: 14px; }
.roadmap-stats { margin: 0; color: var(--muted); font-size: var(--fs-body); font-weight: var(--fw-bold); }
/* The order button. A real control here, unlike tvOS's state indicator, because
   this screen can genuinely re-sort. The :disabled rule is a guard rather than
   a state anything reaches today -- Saga and Release are real on every roadmap,
   so roadmaps.js only ever disables a button that would have one stop, and a
   one-stop order button must LOOK dead rather than look pressable. */
.roadmap-order {
  min-height: 44px;
  border: 1px solid var(--accent);
  border-radius: var(--radius-pill);
  padding: 8px 18px;
  color: var(--text);
  background: rgba(255,61,71,.22);
  font: inherit;
  font-size: var(--fs-body);
  font-weight: var(--fw-bold);
  cursor: pointer;
  transition: background var(--focus-ease), border-color var(--focus-ease);
}
.roadmap-order:hover:not(:disabled), .roadmap-order:focus-visible { background: var(--accent); outline: none; }
.roadmap-order:disabled { border-color: rgba(255,255,255,.16); background: rgba(255,255,255,.06); color: var(--muted); cursor: default; }
#roadmap-back { margin-bottom: 4px; }

.roadmap-chapter { margin: 0 0 29px; }
.roadmap-chapter .row-title { margin: 24px var(--gutter) 0; }
.roadmap-chapter-sub { margin: 4px var(--gutter) 10px; color: var(--muted); font-size: var(--fs-body); }
/* The poster and its caption are one flex item, so the column is exactly as
   wide as the poster -- the same shape .list-item uses in the Library. */
.roadmap-entry, .calendar-entry { display: flex; flex: 0 0 auto; flex-direction: column; gap: 6px; }
/* `width: 0; min-width: 100%` so a long caption contributes NOTHING to the flex
   item's intrinsic width and still fills it. Without it "Ep 12  ·  11:30 pm"
   would make its own column wider than the poster above it, and the whole row
   would stop lining up with every other poster row in the app. */
.roadmap-entry-meta, .calendar-entry-meta { width: 0; min-width: 100%; color: var(--muted); font-size: var(--fs-caption); line-height: 1.3; }
/* The position in the WHOLE watch order, not in this chapter. Top-left, where
   .card-source would sit -- a roadmap entry never carries a source badge, so
   the two can never collide. */
.roadmap-pos {
  position: absolute; top: 8px; left: 8px;
  min-width: 24px;
  border-radius: var(--radius-pill);
  padding: 3px 7px;
  color: #fff;
  background: rgba(10,10,11,.86);
  font-size: var(--fs-caption);
  font-weight: var(--fw-bold);
  text-align: center;
}
/* A `tmdb:` id: visible, ordered, and honestly not openable. 0.45 is the site's
   own number and tvOS's. */
.card.roadmap-unplayable { opacity: .45; cursor: default; }
.card.roadmap-unplayable:hover, .card.roadmap-unplayable:focus-visible { transform: none; box-shadow: 0 8px 22px rgba(0,0,0,.3); }

/* ── Calendar (B12) ──────────────────────────────────────────
   One .result-row per DAY, so the rows are the same rows the Library and the
   search results already draw. A manga chapter is a release note with no
   details page: dimmed like an unplayable roadmap entry, and a press says so
   instead of opening an empty sheet. */
.calendar-days { margin-top: 24px; }
.card.calendar-unopenable { opacity: .55; }

/* ── Education tabs ──────────────────────────────────────── */
.edu-tabs {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  max-width: 760px;
  margin: 0 auto 8px;
  padding: 0 20px;
}
.edu-tab {
  min-height: 44px;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  padding: 9px 16px;
  font-size: var(--fs-body);
  font-weight: var(--fw-bold);
  color: var(--muted);
  background: var(--surface);
  transition: color var(--settle), background var(--settle), border-color var(--settle), box-shadow var(--settle);
}
.edu-tab:hover { color: #fff; background: var(--surface-focus); }
.edu-tab.active {
  color: #fff;
  background: linear-gradient(140deg, var(--accent), var(--accent-strong));
  border-color: var(--accent);
  box-shadow: 0 6px 18px var(--accent-glow);
}
/* Education card variant — slightly lighter background, subtle green border */
/* There is ONE focus colour, and it is the accent. Theme.brs deleted exactly
   this pattern on the Roku by name -- "Games green, Adult magenta, YouTube red"
   -- because a focus ring that changes hue by section teaches the eye that
   focus means different things in different places. The green survives as a
   RESTING tint, which is a category mark, not a focus signal.
   var(--accent) also means the kids shell gets its own gold ring for free. */
.card.edu-card {
  background: var(--surface-soft);
  border-color: rgba(40, 200, 100, .18);
}
.card.edu-card:hover, .card.edu-card:focus-visible {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent-glow), 0 20px 40px rgba(0,0,0,.4);
}
/* EDU badge */
.edu-badge {
  position: absolute;
  top: 8px;
  right: 8px;
  border-radius: var(--radius-pill);
  padding: 3px 7px;
  color: #fff;
  background: rgba(30, 160, 80, .92);
  font-size: var(--fs-caption);
  font-weight: var(--fw-bold);
  letter-spacing: .04em;
}

/* ── Details overlay ─────────────────────────────────────── */
.detail-dialog {
  width: min(900px, calc(100% - 24px));
  max-height: min(760px, calc(100vh - 24px));
  max-height: min(760px, calc(100dvh - 24px));
  overflow: hidden;
  padding: 0;
  border: 1px solid rgba(255,255,255,.1);
  border-radius: var(--radius);
  color: var(--text);
  background: rgba(10,10,10,0.45);
  backdrop-filter: blur(28px) saturate(170%);
  -webkit-backdrop-filter: blur(28px) saturate(170%);
  box-shadow: 0 30px 100px rgba(0,0,0,.85);
}
.detail-dialog::backdrop { background: rgba(0,0,0,.75); backdrop-filter: blur(6px); }
.detail-card { position: relative; min-height: min(560px, calc(100vh - 24px)); min-height: min(560px, calc(100dvh - 24px)); overflow: hidden; }
.detail-art { opacity: .72; background-position: right center; }
.detail-card::after { content: ""; position: absolute; inset: 0; z-index: -1; background: linear-gradient(90deg, rgba(0,0,0,.88) 0%, rgba(0,0,0,.68) 45%, rgba(0,0,0,.3) 100%); }
/* Circular and outlined -- DESIGN.md, "Circular outlined X at the top right".
   .icon-button gives it a square-ish radius, so the shape is set here and only
   for this control. */
.detail-close {
  position: absolute; top: 14px; right: 14px; z-index: 3; color: #fff;
  border-radius: 50%; border: 1px solid rgba(255,255,255,.35);
  background: rgba(10,10,10,.78); transition: background var(--settle);
}
.detail-close:hover { background: rgba(255,61,71,.3); }
/* THE SHEET SCROLLS, so the source list is ONE page.
   Every other box in this dialog is `overflow: hidden` (.detail-dialog and
   .detail-card both), which used to leave `.detail-streams` as the only
   scrollable thing in the sheet — a 250px porthole onto a list 35,000px long.
   Measured before this change, 1440x900, a title with 336 sources: 2 rows
   visible at a time out of 336. Markus, 7 Sep 2026: "i think we should just
   keep the source list onto one page that way your seeing more than 5 sources
   at the detail page."
   Moving the scroll up here makes the whole sheet one continuous page: the
   heading and the actions scroll away and the sources get the full height of
   the dialog, in one scrollbar instead of two nested ones. The max-height
   MATCHES .detail-dialog's own, so the body can use the entire sheet and the
   card's min-height still holds a short title open.
   `overscroll-behavior: contain` so reaching the end of the list does not
   start scrolling the home page behind the dialog. */
.detail-body {
  position: relative; z-index: 0; width: min(550px, 100%); padding: 60px 28px 30px;
  max-height: min(760px, calc(100vh - 24px));
  max-height: min(760px, calc(100dvh - 24px));
  overflow-y: auto;
  overscroll-behavior: contain;
  /* Reserved, so the sheet does not shift sideways between a title with four
     sources and one with 336. */
  scrollbar-gutter: stable;
  /* The same thin dark bar as .season-strip and .episode-rail. The default
     scrollbar here is a bright white column down the middle of a near-black
     sheet — seen in a screenshot the moment this box became scrollable. */
  scrollbar-width: thin;
  scrollbar-color: rgba(255,255,255,.22) transparent;
}
.detail-body::-webkit-scrollbar { width: 6px; }
.detail-body::-webkit-scrollbar-track { background: transparent; }
.detail-body::-webkit-scrollbar-thumb { border-radius: var(--radius-pill); background: rgba(255,255,255,.22); }
.detail-body h2 { margin: 0; font-size: clamp(27px, 5vw, 43px); line-height: 1.04; letter-spacing: -.05em; }
.detail-copy { margin: 14px 0 0; color: #dddde0; font-size: var(--fs-body); line-height: 1.55; }
#detail-copy.is-collapsed { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden; }
.detail-copy-toggle { min-width: 88px; min-height: 44px; padding: 8px 0; border: 0; background: transparent; color: #f1f1f3; font: inherit; font-size: var(--fs-body); text-decoration: underline; text-underline-offset: 4px; cursor: pointer; }
.detail-copy-toggle:focus-visible { outline: 2px solid var(--accent, #ff3d47); outline-offset: 3px; }
.source-pill { display: inline-flex; margin: 0 0 12px; border: 1px solid rgba(255,61,71,.5); border-radius: var(--radius-pill); padding: 5px 9px; color: #fff; background: rgba(255,61,71,.16); font-size: var(--fs-caption); font-weight: var(--fw-bold); }
/* ── Directed by, and the filmography behind it ──────────── */
/* A name here is a BUTTON, not a label, so it carries the same 44px touch
   target and the same focus ring as every other control in the dialog —
   dpad.js walks real focusable elements, and a styled <span> is invisible to
   it. */
.detail-crew { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-top: 20px; }
.detail-crew-label { color: var(--muted); font-size: var(--fs-body); }
.detail-crew-name {
  min-height: 44px;
  padding: 8px 16px;
  border: 1px solid rgba(255,255,255,.16);
  border-radius: var(--radius-pill);
  background: rgba(255,255,255,.06);
  color: #fff;
  font-size: var(--fs-body);
  cursor: pointer;
}
.detail-crew-name:hover, .detail-crew-name:focus-visible { border-color: var(--accent); color: var(--accent); }
/* The wrapper personCard() adds so a tmdb: id can be resolved on the click.
   It must not change the grid, so it lays out as the card it holds. */
.person-card-slot { display: contents; }

/* ── B26: cast, companies, reviews, more like this ───────── */
/* One heading style for all four, and for the Movies/Series headings inside the
   company dialog. On the Roku every one of these labels is the same HEAD 40 at
   0xFFFFFF99 (DetailsScreen.xml:332 and its neighbours); the web equivalent is
   one rule rather than four. */
.detail-section { margin-top: 28px; }
.detail-section-label {
  margin: 0 0 12px;
  color: var(--muted);
  font-size: var(--fs-body);
  font-weight: var(--fw-bold);
  letter-spacing: .02em;
}
/* Horizontal strips, the same shape as the rows they mirror on a television.
   They scroll on their own rather than wrapping, so a 12-name cast never pushes
   the reviews below it off the bottom of the sheet. */
.detail-people, .detail-companies-strip {
  display: flex;
  gap: 14px;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  padding-bottom: 8px;
  scrollbar-width: thin;
  scrollbar-color: rgba(255,255,255,.22) transparent;
}
.detail-people::-webkit-scrollbar, .detail-companies-strip::-webkit-scrollbar { height: 6px; }
.detail-people::-webkit-scrollbar-track, .detail-companies-strip::-webkit-scrollbar-track { background: transparent; }
.detail-people::-webkit-scrollbar-thumb, .detail-companies-strip::-webkit-scrollbar-thumb { border-radius: var(--radius-pill); background: rgba(255,255,255,.22); }

/* A cast member is a BUTTON, like the director chip above it and for the same
   reason: dpad.js walks real focusable elements, so a styled <div> is
   unreachable from a remote. */
.detail-person {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  width: 96px;
  padding: 8px 4px;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: transparent;
  color: var(--text);
  font: inherit;
  text-align: center;
  cursor: pointer;
}
.detail-person:hover, .detail-person:focus-visible { border-color: var(--accent); background: rgba(255,255,255,.06); }
/* Both the <img> and the <span> renderCast falls back to. `display: block` is
   for the span half — a bare inline element ignores width and height. */
.detail-person-photo { display: block; width: 72px; height: 72px; border-radius: 50%; object-fit: cover; background: rgba(255,255,255,.08); }
/* A headshot TMDB does not have leaves a filled circle, NOT a hole. `visibility:
   hidden` was the first shape of this rule and it reads as a missing tile with a
   name floating under it. */
.detail-person.no-image .detail-person-photo { background: rgba(255,255,255,.12); }
.detail-person-name { font-size: var(--fs-caption); line-height: 1.25; }
.detail-person-role { color: var(--muted); font-size: var(--fs-caption); line-height: 1.2; }

/* A company is focusable because it leads to a full catalogue, exactly as Fire
   TV's companyCard says of itself (DetailActivity.kt:1011). */
.detail-company {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 148px;
  min-height: 96px;
  padding: 12px;
  border: 1px solid rgba(255,255,255,.16);
  border-radius: var(--radius);
  background: rgba(255,255,255,.06);
  color: var(--text);
  font: inherit;
  text-align: center;
  cursor: pointer;
}
.detail-company:hover, .detail-company:focus-visible { border-color: var(--accent); }
.detail-company-logo { max-width: 112px; max-height: 40px; object-fit: contain; }
.detail-company-name { font-size: var(--fs-caption); line-height: 1.25; }

/* Reviews stack instead of sitting side by side: the sheet's text column is
   550px wide, and the Roku's two 852px cards do not fit in it. Two is still the
   count — buildReviews' `if shown > 2`. */
.detail-reviews-list { display: flex; flex-direction: column; gap: 12px; }
.detail-review { padding: 14px 16px; border-left: 3px solid var(--accent, #ff3d47); border-radius: var(--radius); background: rgba(255,255,255,.06); }
.detail-review-head { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 8px; margin: 0; font-size: var(--fs-caption); }
.detail-review-author { color: #fff; font-weight: var(--fw-bold); }
.detail-review-score { color: var(--accent, #ff3d47); }
/* Three lines, like the Roku card. The full text is 1200 characters — the fleet
   already truncates it — and a whole blog post inside a detail sheet buries the
   source list under it. */
.detail-review-text {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  overflow: hidden;
  margin: 8px 0 0;
  color: #dddde0;
  font-size: var(--fs-body);
  line-height: 1.5;
}

/* The company dialog's two blocks. */
.company-section + .company-section { margin-top: 28px; }

.detail-status { min-height: 22px; margin: 16px 0 0; color: var(--muted); font-size: var(--fs-body); line-height: 1.4; }

/* ── Trailer embed in detail art ─────────────────────────── */
.detail-trailer {
  position: absolute;
  inset: 0;
  z-index: 0;
  opacity: 0;
  transition: opacity 600ms ease;
  pointer-events: none;
  background: #000;
}
.detail-trailer.loaded { opacity: 1; }
.detail-trailer iframe,
.detail-trailer video {
  width: 100%;
  height: 100%;
  border: 0;
  object-fit: cover;
}
/* Mute toggle button in detail overlay */
.detail-mute-btn {
  position: absolute;
  bottom: 16px;
  right: 16px;
  z-index: 4;
  width: 36px;
  height: 36px;
  border: 1px solid rgba(255,255,255,.2);
  border-radius: 50%;
  background: rgba(0,0,0,.7);
  color: #fff;
  font-size: var(--fs-title);
  display: grid;
  place-items: center;
  cursor: pointer;
  transition: background var(--settle), border-color var(--settle);
}
.detail-mute-btn:hover { background: rgba(255,61,71,.4); border-color: var(--accent); }

/* ── Quality selector ────────────────────────────────────── */
.quality-select {
  height: 42px;
  min-width: 130px;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  padding: 0 14px;
  color: #fff;
  background: var(--surface);
  font-size: var(--fs-body);
  font-weight: var(--fw-bold);
  cursor: pointer;
  appearance: none;
  -webkit-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23fff' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 12px center;
  padding-right: 32px;
  transition: border-color var(--settle), box-shadow var(--settle);
}
.quality-select:hover, .quality-select:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(255,61,71,.18); outline: none; }
.quality-select option { background: var(--surface-focus); color: #fff; }

/* ── Season strip and episode rail ────────────────────────
   Replaces a single <select> that held EVERY episode of EVERY season: measured
   on the deployed app, The Simpsons drew 876 <option>s in it, Grey's Anatomy
   476, Breaking Bad 67, with no grouping of any kind.

   Both controls here are plain buttons, and that is the point. dpad.js walks
   focusable elements by geometry, so it can reach a button and cannot open or
   walk a native <select> popup — a dropdown is unusable from a TV remote. The
   .episode-jump select survives beside them as the mouse's quick jump, scoped
   to one season, which is what takes it from 876 options to about twenty.

   The chips borrow .edu-tab's language on purpose: pill, muted at rest, accent
   gradient when active. One focus colour everywhere, and it is the accent. */
.episode-picker {
  display: grid;
  gap: 12px;
  width: 100%;
  margin-top: 14px;
}
.season-strip {
  display: flex;
  flex-wrap: nowrap;
  gap: 8px;
  /* A show with twenty-eight seasons cannot wrap into eight rows above the
     episodes. It scrolls sideways, and the scrollbar is left visible rather
     than hidden — a hidden one is how a mouse user learns there is nothing
     more to the right. */
  overflow-x: auto;
  padding-bottom: 4px;
  scroll-snap-type: x proximity;
}
.season-chip {
  flex: 0 0 auto;
  min-height: 40px;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  padding: 8px 16px;
  color: var(--muted);
  background: var(--surface);
  font-size: var(--fs-body);
  font-weight: var(--fw-bold);
  white-space: nowrap;
  scroll-snap-align: start;
  transition: color var(--settle), background var(--settle), border-color var(--settle), box-shadow var(--settle);
}
.season-chip:hover { color: #fff; background: var(--surface-focus); }
.season-chip:focus-visible { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-glow); }
.season-chip.active {
  color: #fff;
  background: linear-gradient(140deg, var(--accent), var(--accent-strong));
  border-color: var(--accent);
  box-shadow: 0 6px 18px var(--accent-glow);
}
/* Not a season, so it never wears the "this is the open season" gradient. It is
   an action sitting at the end of a row of states, and it says so by being
   outlined rather than filled. */
.season-chip-jump {
  margin-left: 4px;
  border-style: dashed;
  border-color: rgba(255,255,255,.28);
}

.episode-rail {
  display: flex;
  gap: 8px;
  overflow-x: auto;
  padding-bottom: 4px;
  scroll-snap-type: x proximity;
}
.episode-chip {
  flex: 0 0 auto;
  display: grid;
  gap: 2px;
  align-content: center;
  justify-items: start;
  width: 190px;
  min-height: 56px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 8px 12px;
  text-align: left;
  background: var(--surface);
  scroll-snap-align: start;
  transition: background var(--settle), border-color var(--settle), box-shadow var(--settle);
}
.episode-chip:hover { background: var(--surface-focus); }
.episode-chip:focus-visible { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-glow); }
.episode-chip.active { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent) inset; }
.episode-chip-number { color: var(--accent); font-size: var(--fs-caption); font-weight: var(--fw-bold); letter-spacing: .04em; }
.episode-chip-title {
  /* Two lines, then an ellipsis. A one-line clamp cuts most episode titles mid
     word; letting them run makes every card in the rail a different height. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  color: #fff;
  font-size: var(--fs-body);
  font-weight: var(--fw-bold);
  line-height: 1.25;
}
.episode-jump { justify-self: start; max-width: 100%; }

/* Both strips scroll, so both need a visible track — but two default macOS
   scrollbars stacked under a chip row read as chrome, not as content. Thin,
   rounded and low-contrast: still findable, no longer the loudest thing in the
   dialog. */
.season-strip, .episode-rail { scrollbar-width: thin; scrollbar-color: rgba(255,255,255,.22) transparent; }
.season-strip::-webkit-scrollbar, .episode-rail::-webkit-scrollbar { height: 6px; }
.season-strip::-webkit-scrollbar-track, .episode-rail::-webkit-scrollbar-track { background: transparent; }
.season-strip::-webkit-scrollbar-thumb, .episode-rail::-webkit-scrollbar-thumb { border-radius: var(--radius-pill); background: rgba(255,255,255,.22); }

@media (max-width: 640px) {
  .episode-chip { width: 158px; }
}

/* A remote cannot drag a scrollbar, so the app has to move both strips itself
   (renderEpisodeControls scrolls the open season and the playing episode into
   view). Smooth everywhere except where the viewer asked for no motion. */
@media (prefers-reduced-motion: no-preference) {
  .season-strip, .episode-rail { scroll-behavior: smooth; }
}

/* ── Player ──────────────────────────────────────────────── */
.player { position: fixed; inset: 0; z-index: 60; display: flex; flex-direction: column; background: #000; }
.player-bar { position: absolute; top: 0; right: 0; left: 0; z-index: 2; display: flex; align-items: center; gap: 12px; padding: calc(10px + var(--safe-top)) 16px 26px; pointer-events: none; background: linear-gradient(180deg, rgba(0,0,0,.8), transparent); }
.player-close { pointer-events: auto; display: inline-flex; align-items: center; gap: 4px; border: 0; border-radius: var(--radius-pill); padding: 8px 12px 8px 6px; color: #fff; background: var(--surface); font-size: var(--fs-body); font-weight: var(--fw-bold); transition: background var(--settle); }
.player-close:hover { background: rgba(255,61,71,.3); }
.player-title { overflow: hidden; color: #fff; font-size: var(--fs-body); font-weight: var(--fw-bold); text-overflow: ellipsis; white-space: nowrap; text-shadow: 0 1px 4px #000; }
.player-bar { flex-wrap: wrap; }
.stream-preferences, .stream-track-controls { display: flex; flex-wrap: wrap; align-items: end; gap: 12px; }
.stream-preferences { margin: 18px 0; }
.stream-preference-field { display: grid; gap: 6px; min-width: 145px; max-width: 100%; color: var(--muted); font-size: var(--fs-body); }
.stream-preference-select, .stream-audio-track, .stream-subtitle-track { min-height: 44px; width: 100%; max-width: 260px; border: 1px solid rgba(255,255,255,.25); border-radius: var(--radius); padding: 9px 30px 9px 12px; background: var(--surface-focus); color: #fff; font: inherit; }
.stream-preference-select:focus-visible, .stream-audio-track:focus-visible, .stream-subtitle-track:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; }
.stream-track-controls { margin-left: auto; pointer-events: auto; }
.stream-track-controls .stream-preference-field { color: #eee; }
.stream-track-notice, .stream-check-note { color: var(--muted); font-size: var(--fs-caption); line-height: 1.5; }
.stream-check-status { margin: 12px 0 8px; color: var(--text); font-size: var(--fs-body); }
.stream-check-coverage { display: flex; flex-wrap: wrap; gap: 8px; }
.stream-check-coverage span { border: 1px solid rgba(255,255,255,.18); border-radius: var(--radius); padding: 6px 9px; font-size: var(--fs-caption); color: var(--muted); }
.stream-sample-proof { margin: 8px 0 0; color: #c9d8c6; font-size: var(--fs-caption); line-height: 1.5; }
/* #book-search-host BELONGS IN THIS RULE, not beside it. It is the third child
   of #media-view and it was left out, so while the heading above it and the
   public-domain shelf below it were both centred in a 1320px column with a
   gutter, the real book search ran the FULL WIDTH OF THE SCREEN starting at
   x=0 — outside the page's own margin, and on a television outside the
   title-safe area, where a TV overscans the first rows clean off the edge.
   Measured at 1920×1080: heading at x=340, every book row at x=0. */
#media-view > .page-heading, #book-search-host, #media-library-host { width: 100%; max-width: 1320px; margin-left: auto; margin-right: auto; padding-left: clamp(20px, 4vw, 48px); padding-right: clamp(20px, 4vw, 48px); }
.media-library-tabs { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 24px; }
.media-library button, .media-audio-player button, .media-book-reader button { min-height: 44px; padding: 10px 16px; border: 1px solid rgba(255,255,255,.14); border-radius: var(--radius); background: var(--surface-soft); color: #f3f3f5; font: inherit; cursor: pointer; }
.media-library-tabs button[aria-current="page"] { color: #111; background: #f4f4f5; border-color: #f4f4f5; }
.media-library-search { display: flex; align-items: end; gap: 12px; max-width: 760px; margin-bottom: 20px; }
.media-library-search label { display: grid; flex: 1; gap: 8px; min-width: 0; color: var(--muted); font-size: var(--fs-body); }
.media-library-search input { width: 100%; min-width: 0; min-height: 48px; padding: 12px 14px; border: 1px solid rgba(255,255,255,.14); border-radius: var(--radius); background: var(--surface); color: #fff; font: inherit; }
.media-library-status { margin: 16px 0; color: var(--muted); line-height: 1.6; }
.media-library-status button { margin-left: 12px; }
.media-search-section { margin-top: 36px; }
.media-search-section h2 { font-size: var(--fs-display); }
.media-search-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(135px, 1fr)); gap: 18px; }
.media-search-card { display: flex; flex-direction: column; gap: 12px; min-width: 0; min-height: 44px; padding: 0; border: 0; background: transparent; color: #eee; text-align: left; font: inherit; cursor: pointer; }
.media-search-card img { width: 100%; aspect-ratio: 2 / 3; object-fit: cover; border-radius: var(--radius); }
.media-search-card span { font-size: var(--fs-body); line-height: 1.45; overflow-wrap: anywhere; }
.media-library-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: 28px 20px; align-items: start; }
.media-library .media-library-card { display: flex; flex-direction: column; width: 100%; height: auto; min-width: 0; aspect-ratio: auto; padding: 0; border: 0; border-radius: 0; background: transparent; text-align: left; box-shadow: none; overflow: visible; }
.media-library-card .card-image { display: block; position: static; width: 100%; height: auto; aspect-ratio: 2 / 3; object-fit: cover; border-radius: var(--radius); background: var(--surface); }
.media-library-card.no-image .card-image { visibility: hidden; }
#media-view[data-kind="music"] .card-image, #media-view[data-kind="podcasts"] .card-image { aspect-ratio: 1; }
.media-library-card .card-label { position: static; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; width: auto; margin: 12px 0 0; padding: 0; background: none; font-size: var(--fs-body); font-weight: var(--fw-semibold); line-height: 1.4; text-shadow: none; white-space: normal; overflow: hidden; }
.media-card-creator, .media-reading-position { display: block; margin-top: 5px; color: var(--muted); font-size: var(--fs-body); line-height: 1.5; }
.media-reading-position { color: #dfc6ad; }
.media-library-detail:empty { display: none; }
.media-library-detail { margin: 24px 0 32px; padding-bottom: 24px; border-bottom: 1px solid var(--border); }
.media-library-detail h2 { max-width: 780px; font-size: clamp(22px, 3vw, 32px); line-height: 1.2; }
.media-track-list { padding: 0; list-style: none; }
.media-track-row { display: flex; align-items: center; gap: 14px; padding: 12px 0; border-bottom: 1px solid var(--border); }
.media-track-row button { max-width: 100%; text-align: left; overflow-wrap: anywhere; }
.media-book-reader { width: min(860px, calc(100vw - 32px)); height: 88vh; height: min(88dvh, 1100px); max-height: 1100px; margin: auto; padding: 0; border: 0; border-radius: var(--radius); background: #f6f0e6; color: #2e2922; }
.media-book-reader[open] { display: flex; flex-direction: column; }
.media-book-reader::backdrop { background: rgba(0,0,0,.85); }
.media-book-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; padding: 16px 24px; border-bottom: 1px solid #d5cabc; }
.media-book-toolbar h2 { flex: 1; margin: 0; min-width: 160px; font-size: var(--fs-title); line-height: 1.3; }
.media-book-reader button { color: #302a22; border-color: #b9aa96; background: #f6f0e6; }
.media-book-progress { font-size: var(--fs-body); color: #625645; }
.media-book-text { flex: 1; min-height: 0; padding: 28px clamp(22px, 5vw, 70px) 50px; margin: 0; overflow-y: auto; overscroll-behavior: contain; white-space: pre-wrap; overflow-wrap: anywhere; font: var(--fs-title)/1.8 Georgia, 'Times New Roman', serif; }
.media-audio-player { position: fixed; z-index: 900; bottom: 0; left: 0; right: 0; display: grid; grid-template-columns: minmax(150px, 1fr) minmax(300px, 1.4fr) auto; gap: 8px 20px; align-items: center; padding: 16px max(20px, calc((100vw - 1360px) / 2)); border-top: 1px solid rgba(255,255,255,.14); background: #151518; box-shadow: 0 -12px 36px rgba(0,0,0,.3); }
.media-audio-info { display: grid; gap: 4px; min-width: 0; }
.media-audio-title, .media-audio-creator { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.media-audio-creator, .media-audio-time { color: var(--muted); font-size: var(--fs-caption); }
.media-audio-controls { display: flex; justify-content: center; gap: 8px; }
.media-audio-controls button { font-size: var(--fs-body); white-space: nowrap; }
.media-audio-player button:disabled { opacity: .42; cursor: default; }
.media-audio-player .primary-button, .media-library .primary-button { background: var(--accent); border-color: var(--accent); color: #fff; }
.media-audio-seek { grid-column: 2; grid-row: 2; width: 100%; min-width: 0; height: 44px; margin: -6px 0; accent-color: var(--accent); }
.media-audio-time { grid-column: 1; grid-row: 2; }
.media-audio-status { grid-column: 1 / -1; margin: 0; color: #d4c5b5; font-size: var(--fs-body); }
.media-audio-status:empty { display: none; }
.media-audio-queue { grid-column: 3; grid-row: 1 / 3; }
.media-audio-queue summary { display: flex; align-items: center; min-height: 44px; cursor: pointer; font-size: var(--fs-body); }
.media-audio-queue ol { position: absolute; right: 20px; bottom: 100%; width: min(440px, calc(100vw - 40px)); max-height: 40vh; overflow-y: auto; margin: 0; padding: 12px; border: 1px solid rgba(255,255,255,.14); border-radius: var(--radius) var(--radius) 0 0; background: var(--surface-soft); list-style: none; }
.media-audio-queue li button { width: 100%; text-align: left; border: 0; background: transparent; }
.media-audio-queue [aria-current="true"] { color: #ff9096; }
.has-audio-player #app-main { padding-bottom: calc(var(--media-player-height, 260px) + 40px); }
@media (max-width: 1180px) { .topnav { display: none; } }
@media (max-width: 680px) {
  .media-library-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px 16px; }
  .media-library-card .card-label { font-size: var(--fs-body); }
  .media-audio-player { grid-template-columns: 1fr auto; gap: 6px 12px; padding: 12px 16px max(12px, env(safe-area-inset-bottom)); }
  .media-audio-info { grid-column: 1; }
  .media-audio-controls { grid-column: 1 / -1; grid-row: 2; display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; }
  .media-audio-controls button { padding: 8px 5px; font-size: var(--fs-caption); }
  .media-audio-seek { grid-column: 1 / -1; grid-row: 3; }
  .media-audio-time { grid-column: 1; grid-row: 4; }
  .media-audio-queue { grid-column: 2; grid-row: 1; }
  .media-audio-status { grid-row: 5; }
  .media-book-toolbar { padding: 12px 16px; }
  .media-book-text { font-size: var(--fs-title); padding: 20px 24px 40px; }
}
@media (max-width: 600px) {
  .stream-preference-field { flex: 1 1 130px; min-width: 0; }
  .stream-track-controls { flex: 1 1 100%; margin-left: 0; }
  .player-title { flex: 1; min-width: 0; }
}
.player-stage { position: relative; display: grid; flex: 1; place-items: center; }
.video { width: 100%; height: 100%; background: #000; opacity: 0; transition: opacity .2s; }
.video.ready { opacity: 1; }
.player-msg { position: absolute; width: min(430px, calc(100% - 40px)); margin: 0; color: var(--muted); text-align: center; line-height: 1.45; }
.spinner { width: 34px; height: 34px; border: 3px solid rgba(255,255,255,.15); border-top-color: var(--accent); border-radius: 50%; animation: spin .8s linear infinite; }
.spinner.big { position: absolute; width: 48px; height: 48px; border-width: 4px; }

/* ── Toast ───────────────────────────────────────────────── */
/* app.js moves this single host into the open <dialog> when
   there is one, because showModal() puts that dialog in the
   top layer and a body-level element cannot draw above it. */
.toast-host {
  position: fixed;
  z-index: 80;
  bottom: calc(18px + var(--safe-bottom));
  left: 50%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  width: min(430px, calc(100% - 28px));
  transform: translateX(-50%);
  pointer-events: none;
}
.toast-host-inline { position: absolute; bottom: 18px; }
.toast {
  max-width: 100%;
  margin: 0;
  border: 1px solid rgba(255,255,255,.08);
  border-radius: var(--radius);
  padding: 12px 16px;
  color: var(--text);
  /* Was also blur(18px) saturate(140%). Behind a 92%-opaque panel there is
     almost nothing left to blur: the cost was real and the effect was not. */
  background: var(--surface);
  box-shadow: 0 14px 40px rgba(0,0,0,.4);
  font-size: var(--fs-body);
  font-weight: var(--fw-bold);
  line-height: 1.4;
  text-align: center;
  animation: toast-in .18s ease-out;
}
.toast-error { border-color: rgba(255,61,71,.5); background: rgba(255,61,71,.18); }
.toast-out { opacity: 0; transform: translateY(8px); transition: opacity .2s ease-in, transform .2s ease-in; }

/* ── Streams list ────────────────────────────────────────── */
/* ONE PAGE, NO PORTHOLE, NO PAGER.
   `max-height: 250px; overflow-y: auto;` stood here and it is the whole of the
   "only about 5 sources" complaint: the list was never paginated, all 336 rows
   were always in the DOM, they were just being shown through a 250px window
   inside a sheet that could not scroll. The scroll now belongs to .detail-body
   (see the note there) and this box simply grows, so every source is reached by
   scrolling the sheet — no pager, no "show more", nothing hidden.
   No windowing: the rows are already all built up front (loadStreams appends
   every one), so letting them lay out costs height, not work. 336 rows is
   about 25,000px of ordinary flex children and it scrolls at 60fps.
   Shared with #manga-chapters-list, which has the same dialog skeleton and the
   same problem on a title with hundreds of chapters. */
.detail-streams {
  margin-top: 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.stream-row {
  background: var(--surface);
  padding: 0.5rem 0.75rem;
  border-radius: var(--radius);
  cursor: pointer;
  display: flex;
  justify-content: space-between;
  align-items: center;
  border: 1px solid transparent;
  user-select: none;
  transition: border-color var(--settle), background var(--settle), box-shadow var(--settle);
}
.stream-row:hover {
  border-color: var(--accent);
  background: rgba(255,61,71,.08);
  box-shadow: 0 4px 16px rgba(255,61,71,.15);
}
.stream-row.dead { opacity: 0.4; }
/* The line that says what the capability probe removed (buildFilterNote,
   app.js). Quiet on purpose — it is an explanation for a count that changed,
   not a warning. Without it a viewer who counted 400 sources yesterday and 260
   today has no way to tell a working filter from a broken backend. */
.stream-note {
  padding: 6px 2px 2px;
  color: var(--quiet);
  font-size: var(--fs-caption);
  font-weight: var(--fw-semibold);
  line-height: 1.4;
}
.stream-info { display: flex; flex-direction: column; gap: 0.25rem; }
.stream-title { font-size: var(--fs-body); color: var(--text); }
.stream-tags { font-size: var(--fs-caption); color: var(--muted); }
.stream-q { font-weight: var(--fw-semibold); color: var(--accent); font-size: var(--fs-body); }
.badge { display: inline-block; padding: 2px 6px; border-radius: var(--radius-pill); font-weight: var(--fw-bold); font-size: var(--fs-caption); margin-right: 6px; }
/* Was #8a2be2, a purple. The contract allows ONE accent and every other
   client badges 4K in it. */
.badge-4k { background: var(--accent); color: #fff; }
/* Were #0066ee (a blue), #1a9e4a (a second green) and #555 (a grey): three
   colours the palette does not have, on every stream row. Only 4K earns the
   accent; the rest sit on the warm surface with the one hairline, and the
   label says which is which. */
.badge-1080, .badge-720, .badge-sd { background: var(--surface); box-shadow: inset 0 0 0 1px var(--border); color: var(--text); }
.stream-meta { font-size: var(--fs-caption); color: var(--muted); margin-top: 4px; display: flex; gap: 8px; }

/* ── Progress bar ────────────────────────────────────────── */
.progress-bar { position: absolute; bottom: 0; left: 0; right: 0; height: 4px; background: rgba(255,255,255,0.2); }
.progress-fill { height: 100%; background: var(--accent); }

/* ── Keyframes ───────────────────────────────────────────── */
@keyframes toast-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
@keyframes pulse { from { background-position: 0 0; } to { background-position: 100% 0; } }

/* ── Responsive ──────────────────────────────────────────── */
@media (min-width: 760px) {
  .hero { margin-right: 18px; margin-left: 18px; }
  .hero-content { padding: 70px 54px 45px; }
  /* The gutter is a token (:root, near the top). Setting it here rather than
     re-stating .row-title/.row-track means the >=1600 rule that widens it to
     the contract's 90 is not silently overridden by this block, which is
     exactly what happened the first time. */
  :root { --gutter: 26px; }
  /* 160, the measured poster width. aspect-ratio 2/3 gives the 240 height. */
  .card { width: 160px; }
  .detail-body { padding: 74px 48px 46px; }
}

/* The contract's gutter. DESIGN.md was measured on a 1920 desktop whose
   content track is 1740 wide -- 90 each side -- and Theme.brs uses the same 90.
   This MUST sit after the 760px block above: equal specificity, so source order
   is what decides, and when it sat earlier the 26 won at every width. */
@media (min-width: 1600px) { :root { --gutter: 90px; } }

/* The nav used to survive down to 700px, then to 900px. Neither was far enough.
   .topnav is overflow-x:auto with the scrollbar hidden, so there is no
   affordance saying the rest is off to the right — an item that does not fit
   just reads as a word cut in half ("Roadmaps" rendering as "Rc" at 768 is what
   moved this from 700 to 900).

   900 WAS STILL TOO LOW, and it was too low BEFORE the canonical bar landed.
   MEASURED in headless Comet 151.0.7922.247 on the real page, profile connected
   and Watch Party mounted, because .topbar's width is not just this nav —
   profile.js and watch-party.js both insertBefore(#search-button), so the bar
   also carries a profile chip and "Party" at runtime:

     nav needs           first width that does not clip
     499px  old bar      909px   (so 901-908 clipped, on a 7-item bar)
     506px  canonical    916px   (so 901-915 clipped)

   And the profile chip is VARIABLE WIDTH - it prints the profile's name. Tuning
   the breakpoint to one name is the silent-failure shape this whole comment
   exists to stop, and it happened: a first attempt measured 57px for "Mark" and
   91px for "Alexandra", called 91px the worst case, and set 960px. IT MISSED THE
   ACTUAL WORST CASE, which is the LOGGED-OUT default - the button reads "Connect
   profile" at 126px, wider than any of the names measured, and it is what every
   first-time visitor sees. 960 shipped a 961-1060px clipped band, and "Search"
   read as a half-word across most laptop widths.

   Fixed by removing the variable instead of chasing it. #profile-connect-button
   above now has max-width:126px with an ellipsis, so the topbar's requirement is
   a FIXED number, and the 1200px rule below buys 68px back from chip padding
   rather than dropping the whole bar early.

   RE-MEASURED after both, in headless Comet 151.0.7922.247 on the served page,
   logged out (the real worst case), stepping every width from 900 to 1400:

     clipped widths: NONE
     992 and below   nav hidden, drawer takes over
     993 .. 1199     nav 438px, every chip whole   (tightened padding)
     1200 and up     nav 506px, every chip whole

   So: 992px. There is no clipped band at any width, which is the property that
   matters - not the number. The drawer carries all 15 destinations including the
   two the bar does not (Home and Search), so handing over loses nothing.

   IF YOU ADD OR WIDEN A CHIP, RE-MEASURE. Do not reason about it: the last two
   attempts were both arithmetic, and both were wrong. The script is
   scratchpad/measure-topbar.mjs - it bisects for the first clean width and
   prints every chip that overflows its nav. */
/* Just above the handover, buy width back from the chips rather than dropping the
   whole bar. 7 chips x 8px of padding + 6 gaps = 68px, which is the difference
   between losing the nav at ~1060 and losing it at ~990. The drawer keeps its own
   padding because this selector is .topnav only - the shared
   `.topnav button, .drawer-nav button` rule above is deliberately not touched. */
@media (max-width: 1200px) {
  .topnav { gap: 0; }
  .topnav button { padding-left: 6px; padding-right: 6px; }
}

@media (max-width: 992px) {
  .topnav { display: none; }
}

@media (max-width: 700px) {
  .hero { min-height: 440px; }
  .hero-art { opacity: .58; background-position: 64% top; }
  .hero-content { padding-top: 170px; }
  .roadmap-grid { grid-template-columns: 1fr; }
  .roadmap-card { min-height: 260px; }
}

@media (max-width: 420px) {
  .topbar { width: calc(100% - 16px); }
  .brand { padding-right: 3px; }
  .brand > span:last-child { display: none; }
  .card { width: 118px; }
  /* Stacking the whole form left the 46px round mic floating on its own line,
     centred above the field, attached to nothing. Wrapping instead keeps the
     mic beside the input it dictates into, and drops only the submit button to
     its own full-width line. */
  .search-form { flex-wrap: wrap; align-items: center; }
  /* flex-basis 0, not auto: the base rule gives this input `width: 100%`, and
     an auto basis honours that, so the input claimed the whole line and pushed
     the mic onto a line of its own anyway. */
  .search-form input { flex: 1 1 0; width: auto; min-width: 0; }
  .search-form .primary-button { flex: 1 0 100%; width: 100%; }
  .emby-controls { justify-content: flex-start; }
  #emby-sort { flex: 1 0 100%; }
  .detail-dialog { width: calc(100% - 16px); }
  .detail-body { padding: 55px 20px 26px; }
  .edu-tabs { gap: 6px; }
  .edu-tab { padding: 7px 12px; font-size: var(--fs-body); }
}

/* ── Card hover trailer preview ───────────────────────────────
   The poster stays in the DOM underneath: the video fades in over it, so a
   trailer that fails to load leaves the card looking exactly as before rather
   than as a black hole. */
.card-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  object-fit: cover;
  border-radius: inherit;
  opacity: 0;
  animation: card-preview-in 420ms ease forwards;
  pointer-events: none;
}
@keyframes card-preview-in { to { opacity: 1; } }
.card-previewing { transform: scale(1.04); z-index: 3; }
/* NOT INSIDE AN EXPANDING ROW. The card is already at its hover width there, so
   an extra 4% on top pushes it past the row's height and the page starts moving
   again - exactly what the width cap was set to prevent. */
.row-hero .card-previewing { transform: none; }
.card-previewing .card-label { opacity: 0; }
@media (prefers-reduced-motion: reduce) {
  .card-video { animation: none; opacity: 1; }
  .card-previewing { transform: none; }
}

/* ── Detail rating chips ─────────────────────────────────────── */
.detail-chips { display: flex; flex-wrap: wrap; gap: 8px; margin: 6px 0 10px; }
/* Small OUTLINED pills, per DESIGN.md's detail sheet ("2008 - IMDb 8.9 - Share",
   small outlined pills). They were filled 6px rectangles in a cold grey, which
   is why the sheet read like a form and not like the TVs. */
.detail-chip {
  border-radius: var(--radius-pill); padding: 3px 10px;
  background: transparent; border: 1px solid rgba(255,255,255,.25);
  color: var(--text-body);
  font-size: var(--fs-caption); font-weight: var(--fw-bold); letter-spacing: .02em;
}
.detail-chip-cert { border: 1px solid rgba(255,255,255,.45); background: transparent; color: #fff; }
.detail-chip-star { background: rgba(255,61,71,.22); color: #fff; }

/* ── Seerr request desk ────────────────────────────────────────────────────
   A request card is deliberately NOT a catalog card: it is not something you
   can watch, and dressing it up like a poster row invites a click that leads
   nowhere. It states what the server has, and offers a button only when there
   is something the button can actually do. */
.search-results { display: flex; flex-wrap: wrap; gap: 16px; }
.seerr-card {
  width: 200px; display: flex; flex-direction: column; gap: 10px;
  background: rgba(255, 255, 255, 0.04); border-radius: var(--radius); padding: 10px;
}
.seerr-art {
  aspect-ratio: 2 / 3; border-radius: var(--radius); background-size: cover;
  background-position: center; background-color: rgba(255, 255, 255, 0.06);
  /* Both needed by the <img> this now holds: `relative` anchors the .no-image
     mark's `inset: 0`, and `hidden` keeps the poster inside the rounded corner
     the background-image used to follow for free. */
  position: relative; overflow: hidden;
}
.seerr-body { display: flex; flex-direction: column; gap: 8px; }
.seerr-title { font-size: var(--fs-body); line-height: 1.3; margin: 0; }
.seerr-status {
  align-self: flex-start; font-size: var(--fs-caption); padding: 3px 8px; border-radius: var(--radius-pill);
  background: rgba(255, 255, 255, 0.1);
}
/* The status colours carry meaning, so they are keyed off Seerr's own enum
   rather than off the label text — a re-worded label must not silently change
   what a card looks like. */
.seerr-status[data-status="2"] { background: #a16207; color: #fff; }
.seerr-status[data-status="3"] { background: #1d4ed8; color: #fff; }
.seerr-status[data-status="4"] { background: #0369a1; color: #fff; }
.seerr-status[data-status="5"] { background: #15803d; color: #fff; }
.seerr-card .primary-button { font-size: var(--fs-body); padding: 6px 10px; }

/* ── Comic reader ──────────────────────────────────────────────────────────
   Full-bleed, because a comic page is the content. `object-fit: contain` keeps
   a tall page whole instead of cropping the top and bottom off it on a 16:9 TV. */
.comic-reader {
  position: fixed; inset: 0; z-index: 60; background: #000;
  display: flex; flex-direction: column;
}
.comic-reader[hidden] { display: none; }
.comic-bar {
  display: flex; align-items: center; gap: 16px; padding: 10px 18px;
  background: rgba(0, 0, 0, 0.85); color: #fff; font-size: var(--fs-body);
}
.comic-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.comic-counter { opacity: 0.7; font-variant-numeric: tabular-nums; }
.comic-close {
  background: rgba(255, 255, 255, 0.14); color: #fff; border: 0;
  border-radius: var(--radius-pill); padding: 6px 14px; cursor: pointer;
}
.comic-close:focus-visible { outline: 3px solid var(--accent, #FF3D47); }
.comic-page { flex: 1; min-height: 0; width: 100%; object-fit: contain; }

/* ── Continuous top-to-bottom reading (the long-strip / webtoon mode) ───────
   Every page of the chapter in ONE scrolling column. The images are full
   width up to a comfortable measure and centred, so a phone gets edge to edge
   and a 4K television does not stretch a 900px scan across two metres.

   `scroll-behavior` IS DELIBERATELY `auto` HERE. tv-reading-mode.js drives the
   scroll itself, a frame at a time, so that a D-pad press eases instead of
   jumping. The browser's own smooth scrolling would be a SECOND thing setting
   scrollTop on the same element and the two chase each other into a stutter.
   There is exactly one scroller and it is the rAF loop. */
.comic-strip {
  flex: 1; min-height: 0; width: 100%;
  overflow-y: auto; overflow-x: hidden;
  scroll-behavior: auto;
  background: #000;
  /* Momentum scrolling on a touch device still works; the D-pad path is the
     rAF loop and the two never run at once. */
  -webkit-overflow-scrolling: touch;
}
.comic-strip:focus-visible { outline: 3px solid var(--accent, #FF3D47); outline-offset: -3px; }
.comic-strip-page {
  display: block; width: 100%; max-width: 1100px; height: auto;
  margin: 0 auto; background: #0a0a0b;
  /* The reserved box for a page that has not arrived yet. Without it a column
     of lazy images is zero pixels tall and there is nothing to scroll. */
  aspect-ratio: 3 / 4;
}
.comic-strip-page.is-loaded { aspect-ratio: auto; }
/* A page turn is meaningless in a strip and a mode button is meaningless in a
   text book, so both are hidden outright rather than disabled. An overlay that
   sets display on its children must not out-argue that. */
.comic-reader [hidden] { display: none !important; }

/* ── Book reader ───────────────────────────────────────────────────────────
   A book is TEXT, so the surface is a reflowing box and not an image. Three
   numbers do the work and all three are ten-foot numbers:

   --book-text-size  is set by tv-book-reader.js from the viewer's own choice,
                     22px to 52px, and the SAME choice decides how many
                     characters the server puts on a page. Bigger type really
                     does mean fewer words, not an overflowing box.
   46ch              a comfortable measure — roughly 55 to 60 real characters,
                     the width a book is set to. Prose run across a whole 4K
                     screen is unreadable at ANY size, because the eye loses the
                     line coming back. It is in `ch` on purpose: the measure is
                     counted in characters, so it grows with the type and stays
                     right at every one of the five sizes.
   1.7               line height. Generous, because a television is read at
                     three metres and by people who are not leaning in. */
.book-reader { background: #14120f; }
.book-page {
  flex: 1; min-height: 0; overflow-y: auto;
  width: 100%;
  /* content-box, against the sheet's global `* { box-sizing: border-box }`.
     The measure is the TEXT width; with border-box the padding would eat into
     46ch and the line would come out short by a third at the large sizes. */
  box-sizing: content-box;
  max-width: 46ch;
  margin: 0 auto;
  padding: clamp(18px, 4vh, 56px) clamp(18px, 4vw, 64px);
  color: #ece6da;
  font-size: var(--book-text-size, 34px);
  line-height: 1.7;
  letter-spacing: 0.005em;
  white-space: pre-wrap;
  overflow-wrap: break-word;
  hyphens: auto;
  text-align: left;
}
.book-page:focus-visible { outline: 3px solid var(--accent, #FF3D47); outline-offset: -3px; }
.book-reader .comic-bar { background: rgba(0, 0, 0, 0.9); }
.book-status {
  margin: 0; padding: 14px 18px 20px; text-align: center;
  color: #f4efe6; font-size: var(--fs-title); display: flex; gap: 12px;
  align-items: center; justify-content: center; flex-wrap: wrap;
}
.book-status[hidden] { display: none !important; }

/* ── The books row (Books & Audio) ─────────────────────────────────────────
   One row per result. A row that cannot be opened carries a SENTENCE where the
   Read button would be — never a disabled button, which reads as "try again"
   and answers nothing. */
.book-search { margin: 0 0 34px; }
/* NOT .row-title: that carries `margin-left: var(--gutter)` for the poster
   rails and would indent this heading past the search box under it. */
.book-search-title { margin: 0; font-size: var(--fs-title); font-weight: var(--fw-bold); letter-spacing: -.03em; }
.book-search-blurb { margin: 6px 0 16px; color: var(--muted, rgba(255,255,255,.72)); max-width: 62ch; }
.book-search-status { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; min-height: 24px; }
.book-results { display: flex; flex-direction: column; gap: 10px; margin-top: 14px; }
.book-row {
  display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
  background: var(--surface, var(--surface-focus)); border: 1px solid rgba(255,255,255,.12);
  border-radius: var(--radius); padding: 14px 16px;
}
.book-row-head { flex: 1; min-width: 240px; }
.book-row-title { display: block; line-height: 1.4; overflow-wrap: anywhere; }
.book-row-meta { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }
.book-chip {
  font-size: var(--fs-caption); line-height: 1.6; padding: 2px 9px; border-radius: var(--radius-pill);
  background: rgba(255,255,255,.09); color: rgba(255,255,255,.82);
}
.book-row-why { margin: 0; flex: 1 1 260px; color: rgba(255,255,255,.68); font-size: var(--fs-body); line-height: 1.5; }
.book-open { flex-shrink: 0; min-height: 44px; }
.book-open:focus-visible, .book-reader button:focus-visible, .comic-reader .comic-mode:focus-visible {
  outline: 3px solid var(--accent, #FF3D47); outline-offset: 3px;
}
@media (max-width: 640px) {
  .book-row { align-items: flex-start; }
  .book-open { width: 100%; }
}

/* ── Game screenshots (Games / FLT-2) ────────────────────── */
.game-shots { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 6px; }
.game-shots img { width: 150px; aspect-ratio: 16 / 9; object-fit: cover; border-radius: var(--radius); border: 1px solid rgba(255,255,255,.1); }

[data-theme="kids_warm"] {
  --bg: #1a1510;
  --surface: #261f1a;
  --text: #fffdfa;
  --accent: #FFD700;
  /* --accent-strong was MISSING here, and it is not decoration: every primary
     button, the profile picker's old avatar tile and the pill gradients are
     `linear-gradient(var(--accent), var(--accent-strong))`. Without this line
     the kids shell painted gold fading into Blazing red — two brands in one
     button. The value is the same relationship #E11D2B has to #FF3D47: the
     accent taken down about a fifth. */
  --accent-strong: #C9A400;
  --accent-glow: rgba(255, 215, 0, 0.4);
}

/* DELETED, 4 Sep 2026: 191 lines of BrightMinds (.bm-*) and "Phase 4 Delight"
   rules that styled brightminds.js and delight.js -- two files index.html
   stopped loading, and which are now deleted from the repo. Nothing in any
   shipped script referenced a single one of these classes. */

/* Deleted: a second `.hero::after` ("Volumetric Fog Hero") and a second
   `.hero-art` transition used to sit here, 880 lines below the originals.
   Being last, the duplicate won — and it declared no `z-index: -1`, so unlike
   the real scrim at the top of this file it painted the gradient OVER
   .hero-content instead of behind it, greying out the hero title and buttons
   it was supposed to be making legible. One scrim, declared once, next to the
   rest of the .hero rules. */
/* Anime & Manga: one quiet room, with the same player and readers. */
.anime-room-header, .anime-room, #manga-view, #comics-view { padding-left: var(--gutter); padding-right: var(--gutter); }
.anime-room-shelf .row-title, .manga-history .row-title { margin-left: 0; }
.anime-room-header { padding: 30px 0 0; margin-bottom: 24px; }
.anime-room-header { padding-left: var(--gutter); padding-right: var(--gutter); }
.anime-room-intro { display: flex; justify-content: space-between; align-items: end; gap: 36px; }
.anime-room-intro h1 { font-size: var(--fs-page-title); line-height: 1.12; letter-spacing: -.035em; margin: 6px 0 12px; }
.anime-room-intro p { color: rgba(255,255,255,.72); margin: 0; line-height: 1.6; }
.anime-room-intro .eyebrow { color: rgba(255,255,255,.65); }
#anime-room-search { width: min(420px, 40%); flex-shrink: 0; }
#anime-room-search label { display: block; font-size: var(--fs-body); color: rgba(255,255,255,.72); margin-bottom: 9px; }
#anime-room-search > div { display: flex; gap: 8px; min-width: 0; }
#anime-room-search button { flex: 0 0 auto; }
#anime-room-query { width: 100%; min-width: 0; min-height: 46px; border: 1px solid rgba(255,255,255,.2); border-radius: var(--radius); background: var(--surface); color: var(--text); padding: 10px 14px; font: inherit; }
.anime-room-tabs { display: flex; gap: 28px; margin-top: 28px; border-bottom: 1px solid rgba(255,255,255,.12); }
.anime-room-tabs button { border: 0; border-bottom: 3px solid transparent; border-radius: 0; background: transparent; min-height: 50px; padding: 10px 0; color: rgba(255,255,255,.72); font: inherit; font-weight: var(--fw-semibold); cursor: pointer; }
.anime-room-tabs button[aria-current="page"] { border-bottom-color: var(--accent); color: var(--text); }
.anime-room-filters { display: flex; gap: 8px; overflow-x: auto; padding: 3px 2px 12px; scrollbar-width: none; }
.anime-room-filters::-webkit-scrollbar { display: none; }
.anime-room-filters button { white-space: nowrap; min-height: 44px; padding: 9px 16px; border-radius: var(--radius-pill); border: 1px solid rgba(255,255,255,.18); background: transparent; color: rgba(255,255,255,.78); font: inherit; font-size: var(--fs-body); cursor: pointer; }
.anime-room-filters button[aria-pressed="true"] { background: #f7f7f8; color: #0a0a0b; border-color: #f7f7f8; }
#anime-room-feature { position: relative; isolation: isolate; min-height: 380px; display: flex; align-items: center; overflow: hidden; border-radius: var(--radius); background: var(--surface); margin: 0 0 36px; }
#anime-room-feature[hidden] { display: none; }
.anime-feature-art { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: center 25%; z-index: -2; }
.anime-feature-cover { position: absolute; right: 6%; top: 8%; height: 84%; max-width: 32%; object-fit: contain; border-radius: var(--radius); z-index: -2; }
.anime-feature-poster::after { opacity: .35; }
#anime-room-feature::after { content: ''; position: absolute; inset: 0; background: linear-gradient(90deg,rgba(10,10,11,.97),rgba(10,10,11,.9) 35%,rgba(10,10,11,.15) 100%); z-index: -1; }
.anime-feature-body { padding: clamp(24px, 4vw, 54px); width: min(64%, 680px); }
.anime-feature-body h2 { font-size: clamp(27px, 3vw, 42px); letter-spacing: -.025em; line-height: 1.1; margin: 12px 0; }
.anime-feature-meta { font-size: var(--fs-body); color: rgba(255,255,255,.8); }
.anime-feature-description { font-size: var(--fs-body); line-height: 1.7; color: rgba(255,255,255,.82); display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
.anime-feature-actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 24px; }
.anime-feature-actions .primary-button { background: #f7f7f8; color: #0a0a0b; }
.anime-room-shelf, .manga-history { margin: 0 0 36px; }
.anime-room-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(145px, 1fr)); gap: 20px 16px; }
.anime-room-grid .card { width: 100%; min-width: 0; }
.anime-room-grid .card-label { white-space: normal; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
.manga-history-grid { display: grid; grid-template-columns: repeat(auto-fit,minmax(230px,1fr)); gap: 12px; }
.manga-history-item { display: flex; align-items: center; text-align: left; gap: 16px; background: var(--surface); color: var(--text); border: 1px solid rgba(255,255,255,.12); border-radius: var(--radius); padding: 12px; min-width: 0; cursor: pointer; font: inherit; }
.manga-history-item img { width: 54px; height: 76px; object-fit: cover; border-radius: var(--radius); background: var(--surface-soft); flex-shrink: 0; }
.manga-history-item > span { min-width: 0; }
.manga-history-item strong { display: block; line-height: 1.4; }
.manga-history-item span span { display: block; font-size: var(--fs-caption); line-height: 1.6; color: rgba(255,255,255,.72); margin-top: 6px; }
.comic-reader .comic-bar { flex-wrap: wrap; gap: 10px; }
.comic-reader .comic-bar button { min-height: 44px; border: 1px solid rgba(255,255,255,.3); background: var(--surface-focus); color: #fff; border-radius: var(--radius-pill); padding: 8px 15px; font: inherit; cursor: pointer; }
.comic-reader .comic-bar button:disabled { opacity: .45; cursor: default; }
.anime-room-header button:focus-visible, .anime-room button:focus-visible, .manga-history-item:focus-visible, #manga-reader button:focus-visible, #anime-room-query:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; }
#manga-view > .page-heading, #comics-view > .page-heading { margin-top: 0; }
#manga-view > .search-form { display: none; }
#manga-view .row-title { margin-left: 0; }
#manga-view .row-track { padding-left: 0; padding-right: 0; }
@media (max-width: 768px) {
  .anime-room-intro { display: block; }
  #anime-room-search { width: 100%; margin-top: 22px; }
  .anime-room-tabs { margin-top: 20px; }
  .anime-feature-body { width: 85%; }
  #anime-room-feature { min-height: 360px; }
}
@media (max-width: 480px) {
  .anime-room-header { padding-top: 14px; }
  .anime-room-intro p { font-size: var(--fs-body); }
  .anime-room-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px 12px; }
  #anime-room-feature { align-items: end; min-height: 390px; }
  .anime-feature-body { width: 100%; padding: 24px; }
  #anime-room-feature::after { background: linear-gradient(0deg,rgba(10,10,11,.98),rgba(10,10,11,.85) 55%,rgba(10,10,11,.12)); }
  .anime-feature-actions { gap: 8px; }
  .anime-feature-actions button { font-size: var(--fs-body); padding: 10px 15px; }
  .comic-reader .comic-label { flex-basis: 100%; }
  .comic-reader .comic-counter { margin-right: auto; }
}

/* ── YouTube ────────────────────────────────────────────────
   A YouTube tile is 16:9 and it is WIDE. Every other card in this app is the
   2:3 poster in .card above, and reusing that class here was tried first: a
   hqdefault thumbnail object-fit: cover'd into a portrait box crops both
   sides off the frame, and a YouTube frame is almost always centre-composed,
   so what survives is the middle third of somebody's face. The Roku hit the
   same wall from the other side — YouTubeItem was rebuilt from a 336x300 tile
   to 2.8's WIDE slot for exactly this reason.

   ONE ACCENT, AND IT IS THE APP'S. Theme.brs deleted "Games green, Adult
   magenta, YouTube red" by name, because a focus ring that changes hue by
   section teaches the eye that focus means different things in different
   places. There is no YouTube red anywhere below; focus is --accent, the same
   ring .card gets, so a d-pad user reads the same signal in every room. */
.yt-sections {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  max-width: 1100px;
  margin: 16px auto 4px;
  padding: 0 var(--gutter);
}
.yt-section {
  min-height: 44px;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  padding: 9px 16px;
  font-size: var(--fs-body);
  font-weight: var(--fw-bold);
  color: var(--muted);
  background: var(--surface);
  transition: color var(--settle), background var(--settle), border-color var(--settle), box-shadow var(--settle);
}
.yt-section:hover { color: #fff; background: var(--surface-focus); }
.yt-section.active {
  color: #fff;
  background: linear-gradient(140deg, var(--accent), var(--accent-strong));
  border-color: var(--accent);
  box-shadow: 0 6px 18px var(--accent-glow);
}
.yt-section:focus-visible, .yt-chip:focus-visible, .yt-card:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 3px;
}

.yt-rows { margin-top: 18px; }
.yt-row .row-title { font-size: var(--fs-title); }

/* The tile. 232px wide keeps four across a 1024 tablet and eight across a
   1920 television, which is the density YouTubeScreen's WIDE slot lands at. */
.yt-card {
  position: relative;
  flex: 0 0 auto;
  width: 232px;
  padding: 0;
  border: 0;
  color: var(--text);
  background: none;
  text-align: left;
  scroll-snap-align: start;
  /* Same easing and the same two properties as .card. §2.6 is one law for the
     whole product, not one law for posters — and this row is a top-level nav
     destination, so it is one of the most-seen rows in the app. */
  transition: transform var(--focus-ease), opacity var(--focus-ease);
}

/* THE FOCUS LAW, ON THE YOUTUBE ROWS TOO — §2.6, the same three tiers and the
   same numbers as .card above.
   These tiles are 16:9 rather than 2:3 and they are built by youtube.js rather
   than buildCard(), which is the whole reason they were missed: they are a
   second card type with a second set of rules, and the first pass only brought
   .card onto the contract. A row that lights every tile equally sits next to
   rows that do not, and that inconsistency reads as worse than either. */
.yt-rows:has(.yt-card:hover, .yt-card:focus-visible) .yt-card {
  opacity: .45;
  transform: scale(.93);
}
.yt-row:has(.yt-card:hover, .yt-card:focus-visible) .yt-card {
  opacity: .65;
}
.yt-rows:has(.yt-card:hover, .yt-card:focus-visible) .yt-card:hover,
.yt-rows:has(.yt-card:hover, .yt-card:focus-visible) .yt-card:focus-visible,
.yt-card:hover, .yt-card:focus-visible {
  opacity: 1;
  transform: scale(1);
  z-index: 2;
}
.yt-card:active { transform: scale(.98); }

/* The one accent bar, exactly as .card::after draws it: height 6, the width of
   the artwork, solid accent. It replaces the 2px ring that used to sit on
   .yt-thumb — §2.6's chrome list is exhaustive and ends "Nothing else. No
   rings, no white halos", and a ring on the YouTube row is still a ring. */
.yt-thumb::after {
  content: "";
  position: absolute;
  right: 0;
  bottom: 0;
  left: 0;
  height: 6px;
  background: var(--accent);
  opacity: 0;
  transition: opacity var(--focus-ease);
  pointer-events: none;
}
.yt-card:hover .yt-thumb::after,
.yt-card:focus-visible .yt-thumb::after { opacity: 1; }
.yt-thumb {
  position: relative;
  display: block;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border: 1px solid rgba(255,255,255,.07);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: 0 8px 22px rgba(0,0,0,.3);
  transition: border-color var(--focus-ease), box-shadow var(--focus-ease);
}
.yt-card:hover .yt-thumb, .yt-card:focus-visible .yt-thumb {
  border-color: var(--accent);
  /* The ring that used to lead this list is gone — see .yt-thumb::after above.
     The two lifts stay: they are depth, not chrome, and .card keeps the same
     pair. */
  box-shadow: 0 20px 40px rgba(255,61,71,.28), 0 8px 22px rgba(0,0,0,.4);
}
.yt-thumb-img { display: block; width: 100%; height: 100%; object-fit: cover; background: linear-gradient(135deg, #1B1417, #0E0A0C); }
/* Same rule as .card.no-image: a dead thumbnail has to be MARKED, because an
   empty rectangle reads exactly like a tile that is still loading. */
.yt-thumb.no-image::before { content: "B"; position: absolute; inset: 0; display: grid; place-items: center; color: rgba(255,255,255,.34); font-size: var(--fs-hero); font-weight: var(--fw-bold); }
.yt-thumb.no-image .yt-thumb-img { visibility: hidden; }
.yt-duration {
  position: absolute;
  right: 6px;
  bottom: 6px;
  border-radius: var(--radius-pill);
  padding: 2px 6px;
  color: #fff;
  background: rgba(0,0,0,.82);
  font-size: var(--fs-caption);
  font-weight: var(--fw-bold);
  font-variant-numeric: tabular-nums;
}
.yt-card-title {
  display: -webkit-box;
  overflow: hidden;
  margin-top: 8px;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  font-size: var(--fs-body);
  font-weight: var(--fw-bold);
  line-height: 1.3;
}
.yt-card-meta {
  display: block;
  overflow: hidden;
  margin-top: 3px;
  color: var(--muted);
  font-size: var(--fs-caption);
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* Channel chips. These are the ONLY way into a channel page, and that is
   deliberate: a channel link on every card would double the DOWN presses
   between rows on a remote, because dpad.js moves by geometry. */
.yt-chips { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin: 0 0 10px var(--gutter); }
.yt-chips-label { color: var(--quiet); font-size: var(--fs-caption); font-weight: var(--fw-bold); letter-spacing: .06em; text-transform: uppercase; }
.yt-chip {
  min-height: 34px;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  padding: 6px 13px;
  color: var(--muted);
  background: var(--surface);
  font-size: var(--fs-body);
  font-weight: var(--fw-bold);
  transition: color var(--settle), background var(--settle), border-color var(--settle);
}
.yt-chip:hover { color: #fff; border-color: var(--accent); background: var(--surface-focus); }

.yt-channel {
  display: flex;
  gap: 16px;
  align-items: center;
  max-width: 1100px;
  margin: 18px auto 0;
  padding: 16px var(--gutter);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}
.yt-channel-art { flex: 0 0 auto; width: 72px; height: 72px; overflow: hidden; border-radius: var(--radius-pill); background: var(--surface-focus); }
.yt-channel-art.no-image::before { content: "B"; display: grid; place-items: center; width: 100%; height: 100%; color: rgba(255,255,255,.34); font-size: var(--fs-display); font-weight: var(--fw-bold); }
.yt-channel-avatar { display: block; width: 100%; height: 100%; object-fit: cover; }
.yt-channel-text { flex: 1 1 auto; min-width: 0; }
.yt-channel-name { margin: 0; font-size: var(--fs-display); font-weight: var(--fw-bold); letter-spacing: -.02em; }
.yt-channel-meta { margin: 4px 0 0; color: var(--muted); font-size: var(--fs-body); }
.yt-playlist-form { margin-bottom: 18px; }

@media (max-width: 640px) {
  .yt-card { width: 188px; }
  .yt-channel { flex-wrap: wrap; }
  .yt-channel-art { width: 56px; height: 56px; }
  /* Measured at 390x844: the fleet sends fifteen shelves, so the wrapped chip
     rail took EIGHT lines and pushed the first row of videos below the fold —
     the phone opened on a wall of buttons and no content. One scrolling line
     instead. It stays wrapped on a television, where the width is there and a
     d-pad reads a grid more easily than a scroller. */
  .yt-sections {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
  }
  .yt-sections::-webkit-scrollbar { display: none; }
  .yt-section { flex: 0 0 auto; }
}

/* ── Live TV — DESIGN-V2 §2.14, the binged layout ───────────────────────────
   A left rail (Channels · Guide · Sports · Teams · Favorites · Search) beside
   one content track, the frame the Apple TV and the Roku draw. Every size
   below is the contract's number on the 1920 canvas times --lt, which is 1px
   on a 1920 screen (a television, or a full-width desktop) and shrinks with
   the window down to 0.6px; a phone gets its own column further down. So the
   LOGO card is 372 wide on a TV, as on every other client, and still four
   across in a 1280 window.

   Colours are the §2.5 tokens only: --surface plates, --surface-focus
   (surfaceHi) for the raised and the selected, --focus-fill for a focused
   row, --accent for focus, live and progress, --good for "live now" and
   followed teams. Nothing here is grey. */
#livetv-view,
.lt-overlay {
  --lt: clamp(0.6px, calc(100vw / 1920), 1px);
  --lt-head: calc(40 * var(--lt));
  --lt-body: calc(26 * var(--lt));
  --lt-meta: calc(22 * var(--lt));
  --lt-whisper: calc(18 * var(--lt));
  --lt-trail: calc(90 * var(--lt));
  /* §2.14.3: the guide's channel column, x 176 … 600. */
  --lt-glead: calc(424 * var(--lt));
}
#livetv-view { --lt-top: 96px; }
#livetv-view:not([hidden]) {
  display: grid;
  grid-template-columns: calc(112 * var(--lt)) minmax(0, 1fr);
  column-gap: calc(64 * var(--lt));
  align-items: start;
  min-height: calc(100vh - var(--lt-top));
  padding-top: 12px;
}

/* ── The rail (§2.14.1) ── */
.lt-rail {
  position: sticky;
  top: var(--lt-top);
  z-index: 15;
  height: calc(100vh - var(--lt-top) - 12px);
}
.lt-rail-panel {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: calc(112 * var(--lt));
  overflow: hidden;
  border-radius: 0 calc(16 * var(--lt)) calc(16 * var(--lt)) 0;
  background: var(--surface);
  transition: width var(--settle);
}
/* It slides OVER the content; the content never moves. A 96 px t^1.6 ramp
   (0 -> 60%) sits on its right edge while it is open. */
.lt-rail::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc(400 * var(--lt));
  width: 96px;
  background: linear-gradient(90deg, rgba(0,0,0,.6), rgba(0,0,0,.42) 20%, rgba(0,0,0,.27) 40%, rgba(0,0,0,.14) 60%, rgba(0,0,0,.05) 80%, rgba(0,0,0,0));
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--settle);
}
.lt-rail.is-open .lt-rail-panel, .lt-rail:hover .lt-rail-panel { width: calc(400 * var(--lt)); }
.lt-rail.is-open::after, .lt-rail:hover::after { opacity: 1; }
.lt-rail-items { display: flex; flex-direction: column; padding-top: calc(16 * var(--lt)); }
.lt-rail-item {
  display: flex;
  align-items: center;
  width: calc(80 * var(--lt));
  height: calc(80 * var(--lt));
  margin: 0 0 calc(16 * var(--lt)) calc(16 * var(--lt));
  padding: 0;
  border: 0;
  border-radius: calc(16 * var(--lt));
  background: transparent;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  transition: background var(--settle), color var(--settle), width var(--settle);
}
.lt-rail.is-open .lt-rail-item, .lt-rail:hover .lt-rail-item { width: calc(368 * var(--lt)); }
.lt-icon {
  flex: 0 0 auto;
  width: calc(40 * var(--lt));
  height: calc(40 * var(--lt));
  margin-left: calc(20 * var(--lt));
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.lt-rail-label {
  margin-left: calc(28 * var(--lt));
  font-size: var(--lt-body);
  opacity: 0;
  transition: opacity var(--settle);
}
.lt-rail.is-open .lt-rail-label, .lt-rail:hover .lt-rail-label { opacity: 1; }
.lt-rail-item[aria-current="page"] { background: var(--surface-focus); color: #fff; }
.lt-rail-item:hover, .lt-rail-item:focus, .lt-rail-item:focus-visible { outline: none; background: var(--focus-fill); color: #fff; }
.lt-rail-live {
  position: absolute;
  left: calc(50 * var(--lt));
  bottom: calc(40 * var(--lt));
  display: flex;
  align-items: center;
  margin: 0;
  color: var(--muted);
  font-size: var(--lt-meta);
  white-space: nowrap;
}
.lt-rail-live .lt-rail-label { margin-left: calc(16 * var(--lt)); font-size: var(--lt-meta); }

.lt-dot {
  display: inline-block;
  flex: 0 0 auto;
  width: calc(12 * var(--lt));
  height: calc(12 * var(--lt));
  min-width: 7px;
  min-height: 7px;
  border-radius: 50%;
  background: var(--muted);
}
.lt-dot[data-dot="accent"] { background: var(--accent); }
.lt-dot[data-dot="good"] { background: var(--good); }

/* ── The content track ── */
.lt-main { min-width: 0; }
.lt-status { margin: 0 var(--lt-trail) calc(24 * var(--lt)) 0; color: var(--muted); font-size: var(--lt-body); }
.lt-pane { min-width: 0; padding-bottom: calc(64 * var(--lt)); }
.lt-row { margin-bottom: calc(64 * var(--lt)); scroll-margin-top: var(--lt-top); }
.lt-row-head { display: flex; align-items: center; gap: calc(16 * var(--lt)); margin-right: var(--lt-trail); }
.lt-row-title { margin: 0; color: #fff; font-size: var(--lt-head); font-weight: var(--fw-semibold); line-height: 1.15; letter-spacing: -.01em; }
.lt-row-count { color: var(--muted); font-size: var(--lt-meta); }
.lt-track {
  display: flex;
  overflow-x: auto;
  overflow-y: hidden;
  margin-top: calc(24 * var(--lt));
  padding: calc(8 * var(--lt)) var(--lt-trail) calc(8 * var(--lt)) 0;
  scrollbar-width: none;
}
.lt-track::-webkit-scrollbar, .lt-chips::-webkit-scrollbar { display: none; }

/* ── The LOGO card (§2.14.2): slot 372, art 360 × 203 inset 6 ── */
.lt-card {
  position: relative;
  display: flex;
  flex: 0 0 auto;
  flex-direction: column;
  width: calc(372 * var(--lt));
  margin: 0 calc(48 * var(--lt)) 0 0;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  text-align: left;
}
.lt-card:focus, .lt-card:focus-visible { outline: none; }
.lt-slot {
  display: block;
  padding: calc(6 * var(--lt));
  border-radius: calc(22 * var(--lt));
  transition: transform var(--focus-ease), box-shadow var(--focus-ease);
}
.lt-plate {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: calc(360 * var(--lt));
  height: calc(203 * var(--lt));
  overflow: hidden;
  border-radius: calc(16 * var(--lt));
  background: var(--surface-focus);
  transition: opacity var(--focus-ease);
}
/* `light: true` from the fleet: a dark logo on our near-black would vanish. */
.lt-plate.is-light { background: #EDEDED; }
.lt-logo { display: block; width: auto; height: auto; max-width: calc(252 * var(--lt)); max-height: calc(114 * var(--lt)); object-fit: contain; }
.lt-plate-name {
  display: -webkit-box;
  overflow: hidden;
  padding: 0 calc(24 * var(--lt));
  color: var(--text-body);
  font-size: var(--lt-head);
  font-weight: var(--fw-semibold);
  line-height: 1.2;
  text-align: center;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
}
.lt-plate.is-light .lt-plate-name { color: #111; }
.lt-progress { position: absolute; right: calc(12 * var(--lt)); bottom: calc(12 * var(--lt)); left: calc(12 * var(--lt)); height: calc(4 * var(--lt)); min-height: 3px; }
.lt-progress-fill { display: block; height: 100%; border-radius: 2px; background: var(--accent); }
.lt-bar {
  display: block;
  height: calc(6 * var(--lt));
  min-height: 3px;
  margin: calc(4 * var(--lt)) calc(6 * var(--lt)) 0;
  border-radius: calc(3 * var(--lt));
  background: var(--accent);
  opacity: 0;
  transition: opacity var(--focus-ease);
}
.lt-cap1 {
  display: flex;
  align-items: center;
  gap: calc(10 * var(--lt));
  min-width: 0;
  margin: calc(10 * var(--lt)) calc(6 * var(--lt)) 0;
  color: var(--text-body);
  font-size: var(--lt-body);
  white-space: nowrap;
}
.lt-cap1-text { overflow: hidden; text-overflow: ellipsis; }
.lt-heart { flex: 0 0 auto; width: calc(24 * var(--lt)); height: calc(24 * var(--lt)); min-width: 12px; min-height: 12px; fill: var(--accent); stroke: none; }
.lt-cap2 {
  display: block;
  min-height: 1.3em;
  margin: calc(4 * var(--lt)) calc(6 * var(--lt)) 0;
  overflow: hidden;
  color: var(--muted);
  font-size: var(--lt-meta);
  text-overflow: ellipsis;
  white-space: nowrap;
}
.lt-cap2[data-live="true"] { color: var(--accent); }

/* §2.6, the focus law. Engagement is a class livetv.js sets while focus is
   in the pane (a television always has something focused; a pointer often
   does not, and a page must never sit dimmed with nothing to un-dim it). No
   :has() — the webOS and Tizen engines this ships in do not all have it.
     focused 1.00 / 1.00 + halo + bar · sibling in the row 0.65 / 0.93 · cold 0.45 / 0.93 */
.lt-pane.lt-engaged .lt-slot { transform: scale(.93); }
.lt-pane.lt-engaged .lt-plate { opacity: .45; }
.lt-pane.lt-engaged .lt-hot .lt-plate { opacity: .65; }
.lt-pane .lt-card:focus .lt-slot, .lt-pane .lt-card:hover .lt-slot {
  transform: scale(1);
  box-shadow: inset 0 0 0 calc(6 * var(--lt)) var(--accent);
}
.lt-pane .lt-card:focus .lt-slot .lt-plate, .lt-pane .lt-card:hover .lt-slot .lt-plate { opacity: 1; }
.lt-card:focus .lt-bar, .lt-card:hover .lt-bar { opacity: 1; }
.lt-card:focus .lt-cap1, .lt-card:hover .lt-cap1 { color: #fff; }

/* A Games Today card: away crest — score — home crest. */
.lt-face { display: flex; align-items: center; justify-content: space-between; width: 100%; padding: 0 calc(28 * var(--lt)); }
.lt-face-mid { display: flex; flex-direction: column; align-items: center; gap: calc(8 * var(--lt)); }
.lt-face-score { color: #fff; font-size: var(--lt-head); font-weight: var(--fw-semibold); white-space: nowrap; }
.lt-face-when { color: var(--text-body); font-size: var(--lt-body); text-align: center; }
.lt-crest { display: inline-flex; flex: 0 0 auto; align-items: center; justify-content: center; }
.lt-crest img { display: block; max-width: 100%; max-height: 100%; object-fit: contain; }
.lt-crest-big { width: calc(96 * var(--lt)); height: calc(96 * var(--lt)); }
.lt-crest-80 { width: calc(80 * var(--lt)); height: calc(80 * var(--lt)); }
.lt-crest-40 { width: calc(40 * var(--lt)); height: calc(40 * var(--lt)); min-width: 20px; min-height: 20px; }
.lt-crest-32 { width: calc(32 * var(--lt)); height: calc(32 * var(--lt)); min-width: 18px; min-height: 18px; }
.lt-crest-abbr { color: var(--text-body); font-size: var(--lt-meta); font-weight: var(--fw-bold); }
.lt-pill {
  display: inline-flex;
  align-items: center;
  height: calc(34 * var(--lt));
  padding: 0 calc(12 * var(--lt));
  border-radius: var(--radius-pill);
  background: var(--accent);
  color: #fff;
  font-size: var(--lt-meta);
  font-weight: var(--fw-semibold);
  letter-spacing: .02em;
}
.lt-pill[data-tone="dim"] { background: var(--surface-focus); color: var(--muted); }

.lt-viewall .lt-plate { flex-direction: column; gap: calc(8 * var(--lt)); background: var(--surface); box-shadow: inset 0 0 0 2px var(--border); }
.lt-viewall-label { color: #fff; font-size: var(--lt-head); font-weight: var(--fw-semibold); }
.lt-viewall-count { color: var(--muted); font-size: var(--lt-meta); }

/* View All and Search: the same cards, 4 across. */
.lt-grid {
  display: grid;
  grid-template-columns: repeat(4, calc(372 * var(--lt)));
  column-gap: calc(48 * var(--lt));
  row-gap: calc(40 * var(--lt));
  margin-top: calc(24 * var(--lt));
  padding-top: calc(8 * var(--lt));
}
.lt-grid .lt-card { margin: 0; }

/* ── Chips (§2.14.4): META pills, r16 ── */
.lt-chips {
  display: flex;
  gap: calc(16 * var(--lt));
  overflow-x: auto;
  margin-bottom: calc(40 * var(--lt));
  padding: calc(4 * var(--lt)) var(--lt-trail) calc(4 * var(--lt)) 0;
  scrollbar-width: none;
}
.lt-chip {
  flex: 0 0 auto;
  height: calc(56 * var(--lt));
  min-height: 32px;
  padding: 0 calc(24 * var(--lt));
  border: 0;
  border-radius: calc(16 * var(--lt));
  background: var(--surface);
  color: var(--text-body);
  font-size: var(--lt-meta);
  white-space: nowrap;
  transition: background var(--settle), color var(--settle);
}
.lt-chip.is-selected { background: var(--surface-focus); color: #fff; }
.lt-chip:hover, .lt-chip:focus, .lt-chip:focus-visible { outline: none; background: var(--focus-fill); color: #fff; }
.lt-row-head .lt-back { margin-left: auto; }
.lt-more { margin-top: calc(32 * var(--lt)); }

/* ── Sports: SCORE cards 520 × 232, 3 across, and the schedule plates ── */
.lt-scores {
  display: grid;
  grid-template-columns: repeat(3, calc(520 * var(--lt)));
  gap: calc(24 * var(--lt));
  margin-top: calc(24 * var(--lt));
}
.lt-track-scores .lt-score { flex: 0 0 auto; margin-right: calc(24 * var(--lt)); }
.lt-score {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  width: calc(520 * var(--lt));
  height: calc(232 * var(--lt));
  padding: calc(20 * var(--lt)) calc(24 * var(--lt));
  border: 0;
  border-radius: calc(16 * var(--lt));
  background: var(--surface);
  color: inherit;
  text-align: left;
  transition: background var(--settle), box-shadow var(--focus-ease);
}
.lt-score:hover, .lt-score:focus, .lt-score:focus-visible {
  outline: none;
  background: var(--surface-focus);
  box-shadow: inset 0 0 0 calc(6 * var(--lt)) var(--accent);
}
.lt-score-top { display: flex; align-items: center; gap: calc(14 * var(--lt)); color: var(--muted); font-size: var(--lt-meta); }
.lt-score-when { color: var(--text-body); }
.lt-score-clock { margin-left: auto; color: var(--text-body); }
.lt-score-team { display: flex; align-items: center; gap: calc(16 * var(--lt)); color: var(--text-body); font-size: var(--lt-body); }
.lt-score-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lt-score-pts { color: #fff; font-size: var(--lt-head); font-weight: var(--fw-semibold); }
.lt-score-foot { min-height: 1.2em; overflow: hidden; color: var(--muted); font-size: var(--lt-meta); text-overflow: ellipsis; white-space: nowrap; }
/* A game with no stream yet is dimmed to textDim (§2.14.4). */
.lt-score.is-off .lt-score-team, .lt-score.is-off .lt-score-when, .lt-sched.is-off .lt-sched-team { color: var(--muted); }
.lt-score.is-off .lt-crest, .lt-sched.is-off .lt-crest { opacity: .6; }

.lt-sched-plate {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  column-gap: calc(32 * var(--lt));
  margin: calc(24 * var(--lt)) var(--lt-trail) 0 0;
  padding: calc(8 * var(--lt)) calc(24 * var(--lt));
  border-radius: calc(16 * var(--lt));
  background: var(--surface);
}
.lt-sched {
  display: grid;
  grid-template-columns: calc(150 * var(--lt)) minmax(0, 1fr) auto;
  align-items: center;
  gap: calc(16 * var(--lt));
  min-height: calc(106 * var(--lt));
  padding: calc(12 * var(--lt));
  border: 0;
  border-bottom: 2px solid var(--border);
  border-radius: calc(8 * var(--lt));
  background: transparent;
  color: inherit;
  text-align: left;
  transition: background var(--settle);
}
.lt-sched:nth-last-child(-n+2) { border-bottom-color: transparent; }
.lt-sched:hover, .lt-sched:focus, .lt-sched:focus-visible { outline: none; background: var(--focus-fill); }
.lt-sched-when { display: flex; flex-direction: column; gap: calc(4 * var(--lt)); }
.lt-sched-day { color: var(--muted); font-size: var(--lt-meta); }
.lt-sched-time { color: #fff; font-size: var(--lt-body); }
.lt-sched-teams { display: flex; flex-direction: column; gap: calc(8 * var(--lt)); min-width: 0; }
.lt-sched-team { display: flex; align-items: center; gap: calc(12 * var(--lt)); overflow: hidden; color: var(--text-body); font-size: var(--lt-body); white-space: nowrap; }
.lt-sched-net { display: flex; align-items: center; justify-content: flex-end; gap: calc(10 * var(--lt)); max-width: calc(260 * var(--lt)); color: var(--muted); font-size: var(--lt-meta); text-align: right; }
.lt-sched:focus .lt-sched-day, .lt-sched:focus .lt-sched-net, .lt-sched:focus .lt-sched-team { color: #fff; }

/* ── Teams (§2.14.5): 120 × 120 crest plates, 8 across ── */
.lt-crests {
  display: grid;
  grid-template-columns: repeat(8, calc(180 * var(--lt)));
  column-gap: calc(30 * var(--lt));
  row-gap: calc(32 * var(--lt));
  margin-top: calc(24 * var(--lt));
}
.lt-crest-tile { display: flex; flex-direction: column; align-items: center; min-width: 0; padding: 0; border: 0; background: none; color: inherit; }
.lt-crest-tile:focus, .lt-crest-tile:focus-visible { outline: none; }
.lt-crest-plate {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: calc(120 * var(--lt));
  height: calc(120 * var(--lt));
  border-radius: calc(16 * var(--lt));
  background: var(--surface);
  transition: box-shadow var(--focus-ease), background var(--settle);
}
.lt-crest-tile.is-followed .lt-crest-plate { box-shadow: inset 0 0 0 calc(6 * var(--lt)) var(--good); }
.lt-crest-tile:hover .lt-crest-plate, .lt-crest-tile:focus .lt-crest-plate { background: var(--surface-focus); box-shadow: inset 0 0 0 calc(6 * var(--lt)) var(--accent); }
.lt-check {
  position: absolute;
  top: calc(-8 * var(--lt));
  right: calc(-8 * var(--lt));
  display: grid;
  place-items: center;
  width: calc(32 * var(--lt));
  height: calc(32 * var(--lt));
  min-width: 18px;
  min-height: 18px;
  border-radius: 50%;
  background: var(--good);
  color: var(--bg);
  font-size: var(--lt-meta);
  font-weight: var(--fw-bold);
}
.lt-crest-name { max-width: 100%; margin-top: calc(12 * var(--lt)); overflow: hidden; color: var(--text-body); font-size: var(--lt-body); text-overflow: ellipsis; white-space: nowrap; }
.lt-crest-abbr-line { color: var(--muted); font-size: var(--lt-meta); }
.lt-crest-tile:focus .lt-crest-name { color: #fff; }

/* ── Messages, Search, the toast ── */
.lt-message { max-width: calc(1300 * var(--lt)); padding-top: calc(16 * var(--lt)); }
.lt-message-title { margin: 0 0 calc(16 * var(--lt)); color: #fff; font-size: var(--lt-head); font-weight: var(--fw-semibold); }
.lt-message-body { margin: 0; color: var(--muted); font-size: var(--lt-body); line-height: 1.45; }
.lt-search { display: flex; align-items: center; gap: calc(24 * var(--lt)); margin: 0 var(--lt-trail) calc(16 * var(--lt)) 0; }
.lt-search-input {
  width: calc(900 * var(--lt));
  max-width: 100%;
  height: calc(80 * var(--lt));
  min-height: 40px;
  padding: 0 calc(28 * var(--lt));
  border: 2px solid var(--border);
  border-radius: calc(16 * var(--lt));
  background: var(--surface);
  color: #fff;
  font-size: var(--lt-body);
}
.lt-search-input:focus, .lt-search-input:focus-visible { outline: none; border-color: var(--accent); background: var(--surface-focus); }
.lt-search-status { color: var(--muted); font-size: var(--lt-meta); }
.lt-toast {
  position: fixed;
  bottom: calc(48px + var(--safe-bottom));
  left: 50%;
  z-index: 70;
  max-width: calc(100vw - 32px);
  margin: 0;
  padding: calc(14 * var(--lt)) calc(28 * var(--lt));
  overflow: hidden;
  border-radius: var(--radius-pill);
  background: var(--surface-focus);
  box-shadow: inset 0 0 0 2px var(--border), 0 12px 32px rgba(0,0,0,.5);
  color: #fff;
  font-size: var(--lt-body);
  text-overflow: ellipsis;
  white-space: nowrap;
  transform: translateX(-50%);
}

/* ── Guide (§2.14.3): preview on top, grid below ── */
.lt-guide { display: flex; flex-direction: column; height: calc(100vh - var(--lt-top) - 24px); min-height: calc(640 * var(--lt)); margin-right: var(--lt-trail); }
.lt-guide-top { display: flex; flex: 0 0 auto; height: calc(360 * var(--lt)); }
.lt-preview {
  position: relative;
  display: flex;
  flex: 0 0 auto;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: calc(640 * var(--lt));
  height: calc(360 * var(--lt));
  overflow: hidden;
  border-radius: calc(16 * var(--lt));
  background: #000;
  box-shadow: inset 0 0 0 2px var(--border);
}
.lt-preview-art { display: flex; align-items: center; justify-content: center; width: calc(320 * var(--lt)); height: calc(120 * var(--lt)); }
.lt-preview-art img { max-width: 100%; max-height: 100%; object-fit: contain; }
.lt-preview-hint { margin: calc(28 * var(--lt)) 0 0; color: var(--muted); font-size: var(--lt-whisper); }
.lt-preview-video { position: absolute; inset: 0; width: 100%; height: 100%; background: #000; object-fit: contain; }
.lt-details { display: flex; flex: 1; flex-direction: column; min-width: 0; margin-left: calc(64 * var(--lt)); }
.lt-details-title {
  display: -webkit-box;
  margin: calc(4 * var(--lt)) 0 calc(12 * var(--lt));
  overflow: hidden;
  color: #fff;
  font-size: var(--lt-head);
  font-weight: var(--fw-semibold);
  line-height: 1.2;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
}
.lt-details-meta { display: flex; flex-wrap: wrap; gap: calc(14 * var(--lt)); margin: 0 0 calc(14 * var(--lt)); color: var(--text-body); font-size: var(--lt-meta); }
.lt-live-word { color: var(--accent); font-weight: var(--fw-semibold); }
.lt-details-desc {
  display: -webkit-box;
  margin: 0;
  overflow: hidden;
  color: var(--text-body);
  font-size: var(--lt-body);
  line-height: 1.4;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
}
.lt-details-foot { display: flex; gap: calc(18 * var(--lt)); margin: auto 0 0; color: var(--muted); font-size: var(--lt-meta); }
.lt-details-name { color: #fff; }
.lt-guide-clock { position: relative; flex: 0 0 auto; height: calc(56 * var(--lt)); margin-top: calc(28 * var(--lt)); }
.lt-guide-now-label { position: absolute; bottom: calc(12 * var(--lt)); left: 0; color: var(--accent); font-size: var(--lt-meta); font-weight: var(--fw-semibold); }
.lt-guide-mark { position: absolute; bottom: calc(12 * var(--lt)); color: var(--muted); font-size: var(--lt-meta); white-space: nowrap; }
.lt-guide-now-dot { position: absolute; bottom: calc(-6 * var(--lt)); z-index: 3; width: calc(12 * var(--lt)); height: calc(12 * var(--lt)); min-width: 7px; min-height: 7px; margin-left: calc(-6 * var(--lt)); border-radius: 50%; background: var(--accent); }
.lt-guide-body { position: relative; flex: 1; min-height: 0; }
.lt-guide-rows { position: absolute; inset: 0; overflow-x: hidden; overflow-y: auto; padding-top: calc(8 * var(--lt)); scrollbar-width: none; }
.lt-guide-rows::-webkit-scrollbar { display: none; }
.lt-guide-now-line { position: absolute; top: 0; bottom: 0; z-index: 3; width: 2px; margin-left: -1px; background: var(--accent); pointer-events: none; }
.lt-grow { display: flex; height: calc(92 * var(--lt)); min-height: 44px; margin-bottom: calc(8 * var(--lt)); }
.lt-gch {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  gap: calc(16 * var(--lt));
  width: calc(408 * var(--lt));
  min-width: 0;
  margin-right: calc(16 * var(--lt));
  padding: 0 calc(16 * var(--lt));
  border-radius: calc(8 * var(--lt));
  background: var(--surface);
}
.lt-gch-num { flex: 0 0 auto; width: calc(36 * var(--lt)); color: var(--muted); font-size: var(--lt-meta); }
.lt-gch-logo { display: flex; flex: 0 0 auto; align-items: center; justify-content: center; width: calc(120 * var(--lt)); height: calc(48 * var(--lt)); }
.lt-gch-logo img { max-width: 100%; max-height: 100%; object-fit: contain; }
.lt-gch-name { min-width: 0; overflow: hidden; color: #fff; font-size: var(--lt-body); text-overflow: ellipsis; white-space: nowrap; }
.lt-gcells { position: relative; flex: 0 0 auto; width: calc(1224 * var(--lt)); height: 100%; }
.lt-gcell {
  position: absolute;
  top: 0;
  display: flex;
  align-items: center;
  height: 100%;
  padding: 0 calc(20 * var(--lt));
  overflow: hidden;
  border: 0;
  border-radius: calc(8 * var(--lt));
  background: var(--surface);
  color: var(--text-body);
  font-size: var(--lt-body);
  text-align: left;
  white-space: nowrap;
  transition: background var(--settle), color var(--settle);
}
.lt-gcell-title { overflow: hidden; text-overflow: ellipsis; }
.lt-gcell.is-now { background: var(--surface-focus); color: #fff; }
.lt-gcell.is-now::before { content: ""; flex: 0 0 auto; width: calc(10 * var(--lt)); height: calc(10 * var(--lt)); min-width: 6px; min-height: 6px; margin-right: calc(12 * var(--lt)); border-radius: 50%; background: var(--accent); }
.lt-gcell[data-empty="true"] { color: var(--muted); }
.lt-gcell:hover, .lt-gcell:focus, .lt-gcell:focus-visible { outline: none; background: var(--focus-fill); color: #fff; }
.lt-gcell.is-now:focus::before, .lt-gcell.is-now:hover::before { background: #fff; }

/* ── The live player overlay (§2.14.7), inside #player ── */
.player.lt-live .player-title { visibility: hidden; }
.lt-overlay { position: absolute; inset: 0; z-index: 3; pointer-events: none; }
.lt-overlay::before {
  content: "";
  position: absolute;
  right: 0;
  bottom: 0;
  left: 0;
  height: calc(380 * var(--lt));
  /* t^1.6 from 0 to 85%, sampled every tenth. */
  background: linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,.021) 10%, rgba(0,0,0,.065) 20%, rgba(0,0,0,.124) 30%, rgba(0,0,0,.196) 40%, rgba(0,0,0,.28) 50%, rgba(0,0,0,.376) 60%, rgba(0,0,0,.48) 70%, rgba(0,0,0,.595) 80%, rgba(0,0,0,.718) 90%, rgba(0,0,0,.85) 100%);
}
.lt-ov-logo {
  position: absolute;
  top: calc(72 * var(--lt));
  left: calc(72 * var(--lt));
  display: flex;
  align-items: center;
  justify-content: center;
  width: calc(136 * var(--lt));
  height: calc(80 * var(--lt));
  border-radius: calc(16 * var(--lt));
  background: var(--surface);
}
.lt-ov-logo.is-light { background: #EDEDED; }
.lt-ov-logo img { max-width: calc(104 * var(--lt)); max-height: calc(56 * var(--lt)); object-fit: contain; }
.lt-ov-logo-name { padding: 0 8px; color: #fff; font-size: var(--lt-meta); text-align: center; }
.lt-ov-panel { position: absolute; right: calc(72 * var(--lt)); bottom: calc(70 * var(--lt)); left: calc(72 * var(--lt)); display: flex; flex-direction: column; gap: calc(14 * var(--lt)); }
.lt-ov-line1 { display: flex; align-items: center; gap: calc(16 * var(--lt)); min-width: 0; }
.lt-ov-line1 .lt-pill { height: calc(40 * var(--lt)); padding: 0 calc(14 * var(--lt)); }
.lt-ov-name { overflow: hidden; color: #fff; font-size: var(--lt-head); font-weight: var(--fw-semibold); text-overflow: ellipsis; white-space: nowrap; }
.lt-ov-line2 { margin: 0; overflow: hidden; color: var(--text-body); font-size: var(--lt-body); text-overflow: ellipsis; white-space: nowrap; }
.lt-ov-progress { display: flex; align-items: center; gap: calc(20 * var(--lt)); }
.lt-ov-bar { position: relative; width: calc(600 * var(--lt)); height: calc(6 * var(--lt)); min-height: 3px; overflow: hidden; border-radius: 3px; background: rgba(255,255,255,.2); }
.lt-ov-fill { display: block; height: 100%; background: var(--accent); }
.lt-ov-until { color: var(--muted); font-size: var(--lt-meta); }
.lt-ov-chips { display: flex; flex-wrap: wrap; align-items: center; gap: calc(12 * var(--lt)); }
.lt-ov-chip { display: inline-flex; align-items: center; height: calc(40 * var(--lt)); padding: 0 calc(16 * var(--lt)); border-radius: calc(16 * var(--lt)); background: var(--surface); color: #fff; font-size: var(--lt-meta); }
.lt-ov-hint { margin-left: calc(8 * var(--lt)); color: var(--muted); font-size: var(--lt-whisper); }
.lt-ov-note { position: absolute; top: calc(70 * var(--lt)); left: 50%; max-width: 80%; margin: 0; padding: calc(14 * var(--lt)) calc(28 * var(--lt)); border-radius: var(--radius-pill); background: var(--surface-focus); color: #fff; font-size: var(--lt-body); transform: translateX(-50%); }

/* ── A phone: the rail becomes a strip of tabs above one column ── */
@media (max-width: 760px) {
  #livetv-view, .lt-overlay {
    --lt: 0.5px;
    --lt-head: 20px;
    --lt-body: 15px;
    --lt-meta: 13px;
    --lt-whisper: 12px;
    --lt-trail: 12px;
    --lt-glead: 132px;
  }
  #livetv-view:not([hidden]) { display: block; padding-left: 12px; }
  .lt-rail { position: static; height: auto; margin-bottom: 16px; }
  .lt-rail::after, .lt-rail-live { display: none; }
  .lt-rail-panel, .lt-rail.is-open .lt-rail-panel, .lt-rail:hover .lt-rail-panel { position: static; width: auto; overflow-x: auto; background: none; scrollbar-width: none; }
  .lt-rail-items { flex-direction: row; gap: 8px; padding: 0 12px 0 0; }
  .lt-rail-item, .lt-rail.is-open .lt-rail-item, .lt-rail:hover .lt-rail-item { flex: 0 0 auto; width: auto; height: 40px; margin: 0; padding: 0 14px 0 0; background: var(--surface); }
  .lt-icon { width: 20px; height: 20px; margin-left: 10px; }
  .lt-rail-label { margin-left: 8px; opacity: 1; }
  .lt-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 12px; margin-right: 12px; }
  .lt-grid .lt-card, .lt-grid .lt-plate { width: 100%; }
  .lt-scores { grid-template-columns: minmax(0, 1fr); margin-right: 12px; }
  .lt-scores .lt-score { width: 100%; }
  .lt-score { width: 280px; height: auto; min-height: 132px; gap: 8px; }
  .lt-sched-plate { grid-template-columns: minmax(0, 1fr); }
  .lt-sched:nth-last-child(2) { border-bottom-color: var(--border); }
  .lt-crests { grid-template-columns: repeat(4, minmax(0, 1fr)); column-gap: 8px; margin-right: 12px; }
  .lt-guide { height: auto; margin-right: 12px; }
  .lt-guide-top { flex-direction: column; height: auto; }
  .lt-preview { width: 100%; height: auto; aspect-ratio: 16 / 9; }
  .lt-details { margin: 12px 0 0; }
  .lt-details-foot { margin-top: 12px; }
  .lt-guide-mark, .lt-guide-now-dot { display: none; }
  .lt-guide-body { min-height: 60vh; overflow-x: auto; }
  .lt-guide-rows { position: relative; inset: auto; width: max-content; overflow: visible; }
  .lt-gch { width: 120px; margin-right: 12px; padding: 0 8px; }
  .lt-gch-logo { display: none; }
}

/* ============================================================
   GAMES HUB — the one browser that replaces the eight nookie apps.

   Every colour here is a token off :root. DESIGN-V2 §2.5 names twelve and
   deletes the rest by hex; the greys this file used to be full of (#141416
   above all) are the ones Markus banned on 24 Aug. Nothing below introduces
   a literal that is not already in the ladder.
   ============================================================ */

.games-panel { margin-top: 20px; }

/* The chip rows. DESIGN-V2 §2.6: a chip is NOT the card law — it is SETTLE,
   140ms outQuad (--settle), which is the token .primary-button already uses.
   Retiming these to the 250ms card curve is the specific mistake §2.6 calls
   out by name. */
/* 760px, the same track .search-form uses, and it is deliberate rather than
   inherited. LOOKED AT, 2026-09-23: at 980 the chips started 110px left of
   the search box they filter, and #games-hub-results inherited the global
   `.search-results { display: flex }` with no max-width at all, so every
   release row ran the full 1440 while the control above it stopped at 760.
   One track for the whole screen. */
.games-chip-row {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
  max-width: 760px; margin: 14px auto 0; padding: 0 20px;
}
.games-chip {
  min-height: 38px; padding: 0 14px;
  border: 1px solid var(--border); border-radius: var(--radius-pill);
  color: var(--muted); background: var(--surface);
  font: inherit; font-size: var(--fs-caption); font-weight: var(--fw-bold);
  letter-spacing: .02em; cursor: pointer;
  transition: color var(--settle), background var(--settle), border-color var(--settle);
}
.games-chip:hover { color: #fff; background: var(--surface-focus); }
.games-chip[aria-pressed="true"] {
  color: #fff; background: var(--focus-fill); border-color: var(--accent);
}
.games-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.games-select-label { color: var(--muted); font-size: var(--fs-caption); font-weight: var(--fw-bold); }
.games-select {
  min-height: 38px; padding: 0 10px;
  border: 1px solid var(--border); border-radius: var(--radius);
  color: var(--text); background: var(--surface); font: inherit; font-size: var(--fs-caption);
}

#games-hub-results {
  display: grid; gap: 10px;
  max-width: 760px; margin: 20px auto 0; padding: 0 20px;
}
.games-warnings { max-width: 760px; margin: 8px auto 0; padding: 0 20px; }
/* A failed source is WARN, and warn is a toast colour in DESIGN-V2 2.5 — never
   a chip fill. This is a line of text, which is what it is for. */
.games-warning { margin: 4px 0 0; color: #FFB84D; font-size: var(--fs-caption); }
/* One line per part of a split game, under the dialog's status: what each
   press got, so a missing part is named. Warn colour only on a part that did
   not come. */
.games-part-list { margin: 8px 0 0; padding: 0 0 0 22px; color: var(--muted); font-size: var(--fs-caption); line-height: 1.6; }
.games-part-list li[data-state="failed"] { color: #FFB84D; }
.games-part-list a { color: #f1f1f3; text-decoration: underline; text-underline-offset: 3px; }

.games-source-error { margin-top: 6px; color: #FFB84D; font-size: var(--fs-caption); }
.games-sources-gap { max-width: 760px; margin: 12px auto 0; padding: 0 20px; color: var(--quiet); font-size: var(--fs-caption); }

/* A REAL SWITCH. This used to be a state badge, and the comment here used to
   say why one was impossible: the registry's only write was POST /games/sources
   and it upserts a whole declaration including a live search() function, which
   a browser cannot round-trip. PATCH /games/sources/<id> { enabled } now exists
   and writes a two-key overlay instead, so the badge became the button it
   always wanted to be. Green when on, quiet when off, and the same pill shape
   as before so the row did not move. */
.games-source-toggle {
  min-width: 92px; padding: 5px 12px; border-radius: var(--radius-pill);
  border: 1px solid var(--border); background: var(--surface); color: var(--muted);
  font: inherit; text-align: center; cursor: pointer;
  font-size: var(--fs-caption); font-weight: var(--fw-bold); letter-spacing: .04em;
  transition: color var(--settle), background var(--settle), border-color var(--settle);
}
.games-source-toggle[data-enabled="true"] { color: #4CD48A; border-color: rgba(76,212,138,.4); }
.games-source-toggle[data-enabled="false"] { color: var(--quiet); }
.games-source-toggle:hover:not(:disabled) { background: var(--surface-focus); }
.games-source-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* The in-flight press. It is disabled between the PATCH and its reply so a
   double press cannot send two contradictory writes at the row's expense. */
.games-source-toggle:disabled { opacity: .55; cursor: progress; }

.games-lane-state-row { display: flex; align-items: center; gap: 10px; margin-top: 10px; }
.games-accounts-where code {
  padding: 2px 6px; border-radius: 6px;
  background: var(--bg); color: var(--text); font-size: .92em;
}

/* A hub result is a WIDE row, not a poster. A release carries a file name, a
   size and a lane list, and none of those fit under a 2:3 tile — the eight
   apps this replaces all used a list for exactly that reason. */
.games-row {
  display: grid; grid-template-columns: 96px 1fr auto; gap: 14px; align-items: center;
  width: 100%; padding: 12px; text-align: left;
  border: 1px solid var(--border); border-radius: var(--radius);
  color: var(--text); background: var(--surface); font: inherit; cursor: pointer;
  transition: background var(--settle), border-color var(--settle);
}
.games-row:hover, .games-row:focus-visible { background: var(--surface-focus); border-color: var(--accent); }
.games-row:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.games-row-art {
  width: 96px; aspect-ratio: 3 / 4; object-fit: cover;
  border-radius: 12px; background: linear-gradient(135deg, #1B1417, #0E0A0C);
}
.games-row-art.is-blank { display: grid; place-items: center; color: var(--quiet); font-size: 22px; font-weight: var(--fw-bold); }
.games-row-main { min-width: 0; }
.games-row-title { display: block; font-size: var(--fs-body); font-weight: var(--fw-bold); line-height: 1.3; }
.games-row-meta { display: block; margin-top: 4px; color: var(--muted); font-size: var(--fs-caption); }
.games-row-lanes { display: flex; flex-wrap: wrap; gap: 6px; justify-content: flex-end; }

/* A lane badge is information, never a button. `good` is the one green in
   §2.5 (#4CD48A); the direct lane gets the accent because it is the lane
   that needs no account and is therefore never the degraded one. */
.games-lane-badge {
  padding: 3px 9px; border-radius: var(--radius-pill);
  font-size: var(--fs-caption); font-weight: var(--fw-bold); letter-spacing: .04em;
  border: 1px solid var(--border); color: var(--muted);
}
.games-lane-badge[data-lane="realdebrid"] { color: #4CD48A; border-color: rgba(76,212,138,.4); }
.games-lane-badge[data-lane="torbox"] { color: #4CD48A; border-color: rgba(76,212,138,.4); }
/* `http`, not `direct`. That is the token lib/game-sources.js uses in LANES,
   in lanesFor() and in the `lane` field of every row; a selector written for
   `direct` matched nothing and the one lane that always works was drawn as the
   degraded one. It gets the accent because it is never the degraded one. */
.games-lane-badge[data-lane="http"] { color: var(--accent); border-color: var(--accent); }
.games-lane-badge[data-armed="false"] { color: var(--quiet); border-color: rgba(255,255,255,.12); }

.games-lane-head { margin: 18px 0 0; font-size: var(--fs-body); font-weight: var(--fw-bold); }
.games-lane-row { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 10px; }
/* A lane with no key must READ disabled, not just refuse the click. LOOKED AT,
   2026-09-23: .secondary-button had no :disabled rule, so "Real-Debrid - add a
   key first" sat at the same weight as the live "Send to PS5" beside it and
   the only thing saying it was off was the sentence inside the button. */
.games-lane-row button:disabled {
  opacity: .45; cursor: not-allowed;
  border-color: rgba(255,255,255,.10); background: var(--surface);
}
.games-lane-row button:disabled:hover { background: var(--surface); }
.games-lane-state { margin: 14px 0 0; color: var(--muted); font-size: var(--fs-caption); }

/* The registry. One row per source: name, what it can fetch, and the switch. */
.games-source-list { max-width: 760px; margin: 0 auto; padding: 0 20px; list-style: none; display: grid; gap: 10px; }
.games-source-item {
  display: grid; grid-template-columns: 1fr auto; gap: 12px; align-items: center;
  padding: 14px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface);
}
.games-source-item[data-enabled="false"] { opacity: .58; }
.games-source-name { font-size: var(--fs-body); font-weight: var(--fw-bold); }
.games-source-sub { margin-top: 4px; color: var(--muted); font-size: var(--fs-caption); }

.games-add-form { max-width: 760px; margin: 26px auto 0; padding: 20px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); }
.games-add-title { margin: 0 0 4px; font-size: var(--fs-body); font-weight: var(--fw-bold); }
.games-field { display: block; margin-top: 12px; }
.games-field > span { display: block; margin-bottom: 6px; color: var(--muted); font-size: var(--fs-caption); font-weight: var(--fw-bold); }
.games-field input, .games-field select {
  width: 100%; min-height: 44px; padding: 0 12px;
  border: 1px solid var(--border); border-radius: var(--radius);
  color: var(--text); background: var(--bg); font: inherit;
  transition: border-color var(--settle);
}
.games-field input:focus, .games-field select:focus { outline: none; border-color: var(--accent); }
.games-add-form .primary-button { margin-top: 16px; }

@media (max-width: 640px) {
  .games-row { grid-template-columns: 72px 1fr; }
  .games-row-art { width: 72px; }
  .games-row-lanes { grid-column: 1 / -1; justify-content: flex-start; }
}

/* The kind label is the first thing on a source's second line, so it is the
   word the eye lands on. A Prowlarr indexer and one of our own scraper modules
   fail for completely different reasons and get fixed in different repos.

   `kind`, not `origin`. There is no `origin` field on the wire — the add-on's
   registry list() emits `kind`, one of 'module' or 'indexer', and this selector
   was written against a field name nothing has ever sent. */
.games-source-item[data-kind="indexer"] .games-source-sub::first-line { color: var(--text-body); }

/* ── In-browser emulator (emulator.js) ───────────────────────────────────────
   A full-screen player for one retro ROM: fixed inset:0, z-index 60 (the
   .player's layer), a thin bar on top and the game filling the rest.
   ON THE LIVE TOKENS, per DESIGN-V2.md §2.5 rather than the older comic-reader
   look it was first copied from: the ground is `bg`, the one page black (a
   resting #000 is the scrim colour, and the scrim is "never a resting colour");
   the Close pill rests on `surface` like .player-close, not on a white-alpha
   grey; the one hairline is `line`, the accent at 20%, 2px. They track the
   active theme (red by default, gold under kids_warm). Mobile-first: 16px side
   gutter, and the overlay clips its own overflow so nothing scrolls sideways. */
.emulator-overlay {
  position: fixed; inset: 0; z-index: 60; background: var(--bg);
  display: flex; flex-direction: column; overflow: hidden;
}
.emulator-overlay[hidden] { display: none; }
/* Lock the page behind the player so a phone does not scroll the app under it. */
html.emulator-open, html.emulator-open body { overflow: hidden; }

.emulator-bar {
  flex: 0 0 auto; display: flex; align-items: center; gap: 16px;
  padding: calc(12px + var(--safe-top, 0px)) 16px 12px;
  background: var(--bg); border-bottom: 2px solid var(--border);
  color: var(--text); font-size: var(--fs-body);
}
.emulator-title {
  flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; font-weight: var(--fw-bold);
}
.emulator-close {
  flex: 0 0 auto; border: 0; border-radius: var(--radius-pill);
  padding: 8px 16px; min-height: 44px; cursor: pointer;
  color: var(--text); background: var(--surface);
  font: inherit; font-weight: var(--fw-bold);
  transition: background var(--settle);
}
.emulator-close:hover { background: rgba(255, 61, 71, .3); }
.emulator-close:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; }

/* The disc-loading line: textDim on the page black, one hairline under it. It
   leaves the moment the game starts, and the stage takes the room back. */
.emulator-hint {
  flex: 0 0 auto; margin: 0; padding: 8px 16px;
  color: var(--muted); font-size: var(--fs-body); line-height: 1.5;
  background: var(--bg); border-bottom: 2px solid var(--border);
}
.emulator-hint[hidden] { display: none; }
.emulator-stage { position: relative; flex: 1; min-height: 0; width: 100%; background: var(--bg); }
.emulator-frame { display: block; width: 100%; height: 100%; border: 0; background: var(--bg); }

/* The setup panel overlays the stage: the system picker, the local-ROM chooser,
   a refusal that says why a system cannot start here, and the watchdog hint
   all live here, centred and scrollable, never wider than the screen. */
.emulator-setup {
  position: absolute; inset: 0; overflow-y: auto; overflow-x: hidden;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 16px; padding: 24px 16px calc(24px + var(--safe-bottom, 0px));
  background: var(--bg); color: var(--text);
}
.emulator-setup[hidden] { display: none; }
.emulator-status { max-width: 640px; margin: 0; text-align: center; color: var(--muted); line-height: 1.55; }
.emulator-setup-label { margin: 0; font-size: var(--fs-title); font-weight: var(--fw-semibold); }
.emulator-picker {
  display: flex; flex-wrap: wrap; gap: 8px; justify-content: center;
  width: 100%; max-width: 760px;
}
.emulator-system { min-height: 44px; }
.emulator-system[aria-pressed="true"] { outline: 2px solid var(--accent); outline-offset: 2px; }
.emulator-file {
  display: flex; flex-direction: column; gap: 8px; align-items: center;
  width: 100%; max-width: 480px; margin-top: 4px;
  padding-top: 16px; border-top: 2px solid var(--border);
}
.emulator-file-label { color: var(--muted); font-size: var(--fs-body); }
.emulator-file input[type="file"] { max-width: 100%; color: var(--text); font: inherit; }

@media (max-width: 640px) {
  .emulator-picker { max-width: 100%; }
  .emulator-system { flex: 1 1 140px; }
}
