/* ============================================================
   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.
   Transitions: 300ms ease throughout
   ============================================================ */

: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);
  --radius: 16px;
  --transition: 300ms ease;
  /* 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 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 + page titles         -> Roku 40 */
  --fs-hero: 42px;      /* the hero title, and only it  -> Roku 78 */

  /* 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; }

.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(--transition);
}

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

.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(--transition);
}
.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(--transition), background var(--transition);
}
/* 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; }

/* ── 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: clamp(28px, 5vw, 52px); 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%;
  width: min(620px, 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; }
.home-hero-synopsis {
  margin: 10px 0 0;
  max-width: 560px;
  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; }
}

/* 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; }
}

/* ── 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(--transition), background var(--transition), box-shadow var(--transition);
}
/* 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 rgba(255,255,255,.12); color: #fff; background: rgba(28,28,31,.9); }
.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; }
.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. */
  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. Listed last so it wins. */
.rows:has(.card:hover, .card:focus-visible) .card:hover,
.rows: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(--transition); }
/* .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(--transition);
  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(--transition), transform var(--transition);
  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; }
.row-hero .card {
  transition: width var(--transition), aspect-ratio var(--transition), transform var(--transition), border-color var(--transition), box-shadow var(--transition);
}
.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(--transition);
  cursor: pointer;
}
.card:hover .card-mute-btn { opacity: 1; }

/* ── Inner pages ─────────────────────────────────────────── */
.page-heading { max-width: 760px; margin: 44px auto 24px; padding: 0 20px; }
.page-heading h1 { font-size: clamp(30px, 6vw, 48px); }
.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(--transition), box-shadow var(--transition); }
.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(--transition), background var(--transition); }
.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; }

/* 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(--transition), background var(--transition), color var(--transition); }
.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 rgba(255,255,255,.18);
  border-radius: var(--radius-pill);
  color: #fff;
  background: rgba(28,28,31,.9);
  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(--transition), border-color var(--transition);
}
.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(--transition), color var(--transition);
}
.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: clamp(30px, 6vw, 48px); 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 rgba(255,255,255,.12);
  border-radius: var(--radius-pill);
  padding: 9px 16px;
  font-size: var(--fs-body);
  font-weight: var(--fw-bold);
  color: var(--muted);
  background: rgba(28,28,31,.8);
  transition: color var(--transition), background var(--transition), border-color var(--transition), box-shadow var(--transition);
}
.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(--transition);
}
.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(--transition), border-color var(--transition);
}
.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 rgba(255,255,255,.18);
  border-radius: var(--radius-pill);
  padding: 0 14px;
  color: #fff;
  background: rgba(28,28,31,.9);
  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(--transition), box-shadow var(--transition);
}
.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 rgba(255,255,255,.12);
  border-radius: var(--radius-pill);
  padding: 8px 16px;
  color: var(--muted);
  background: rgba(28,28,31,.8);
  font-size: var(--fs-body);
  font-weight: var(--fw-bold);
  white-space: nowrap;
  scroll-snap-align: start;
  transition: color var(--transition), background var(--transition), border-color var(--transition), box-shadow var(--transition);
}
.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 rgba(255,255,255,.12);
  border-radius: var(--radius);
  padding: 8px 12px;
  text-align: left;
  background: rgba(28,28,31,.8);
  scroll-snap-align: start;
  transition: background var(--transition), border-color var(--transition), box-shadow var(--transition);
}
.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: rgba(28,28,31,.78); font-size: var(--fs-body); font-weight: var(--fw-bold); transition: background var(--transition); }
.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: #202024; }
.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 #303034; }
.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 #29292d; }
.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: rgba(14,14,16,.96);
  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: rgba(20,20,22,.6);
  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(--transition), background var(--transition), box-shadow var(--transition);
}
.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; }
.badge-1080 { background: #0066ee; color: #fff; }
.badge-720 { background: #1a9e4a; color: #fff; }
.badge-sd { background: #555; color: #fff; }
.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); }

/* 10-foot UI D-pad magnetic focus engine */
@media (pointer: coarse), (hover: none) {
  .card:focus-visible, .primary-button:focus-visible, .secondary-button:focus-visible, button:focus-visible {
    transform: scale(1.08);
    box-shadow: 0 0 15px var(--accent-glow);
    outline: 3px solid var(--accent);
    outline-offset: 4px;
    z-index: 10;
    transition: transform 0.2s ease, box-shadow 0.2s ease;
  }
}

[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: clamp(28px, 3.5vw, 44px); 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 rgba(255,255,255,.12);
  border-radius: var(--radius-pill);
  padding: 9px 16px;
  font-size: var(--fs-body);
  font-weight: var(--fw-bold);
  color: var(--muted);
  background: rgba(28,28,31,.8);
  transition: color var(--transition), background var(--transition), border-color var(--transition), box-shadow var(--transition);
}
.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(--transition), box-shadow var(--transition);
}
.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 rgba(255,255,255,.12);
  border-radius: var(--radius-pill);
  padding: 6px 13px;
  color: var(--muted);
  background: rgba(28,28,31,.8);
  font-size: var(--fs-body);
  font-weight: var(--fw-bold);
  transition: color var(--transition), background var(--transition), border-color var(--transition);
}
.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 ────────────────────────────────────────────────────────────────
   A channel is not a poster. Every other grid in this app is built around
   2:3 artwork; a live channel has a wide, transparent, often square logo and
   a name that has to be readable because the logos are inconsistent and many
   are missing entirely (and every http one is dropped as mixed content — see
   livetv.js safeHttpsUrl). So these tiles are wide, the art box is fixed and
   letterboxed with `object-fit: contain`, and the NAME is the primary label
   rather than a caption under an image. */
.livetv-groups {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  max-width: 1100px;
  margin: 16px auto 4px;
  padding: 0 var(--gutter);
}
.livetv-chip {
  min-height: 44px;
  border: 1px solid rgba(255,255,255,.12);
  border-radius: var(--radius-pill);
  padding: 9px 16px;
  font-size: var(--fs-body);
  font-weight: var(--fw-bold);
  color: var(--muted);
  background: rgba(28,28,31,.8);
  transition: color var(--transition), background var(--transition), border-color var(--transition), box-shadow var(--transition);
}
.livetv-chip:hover { color: #fff; background: var(--surface-focus); }
/* aria-pressed IS the state, so the styling reads the same attribute a screen
   reader does — there is no second `.active` class to keep in step with it. */
.livetv-chip[aria-pressed="true"] {
  color: #fff;
  background: linear-gradient(140deg, var(--accent), var(--accent-strong));
  border-color: var(--accent);
  box-shadow: 0 6px 18px var(--accent-glow);
}
.livetv-chip:focus-visible, .livetv-card:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 3px;
}

.livetv-results {
  display: grid;
  /* 230px was sized for a tile whose second line said "News · Guide". B33 put
     "10:31 AM Premier League Darts" there instead, and a programme nobody can
     read is the same as no programme. */
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 14px;
  max-width: 1100px;
  margin: 20px auto 0;
  padding: 0 var(--gutter);
}
.livetv-card {
  /* `.card` is a 132px-wide 2:3 POSTER box, and `.livetv-card` never said
     otherwise — so despite everything the comment above promises, every Live TV
     tile has been drawn as a poster with its text column squeezed to about
     60px. That was survivable while the second line said "News · Guide"; it is
     not survivable now that the line says what is ON, because "10:28 AM
     Premier League Darts" arrived as "10:28 A…". The grid track decides the
     width of a channel tile. */
  width: auto;
  aspect-ratio: auto;
  display: grid;
  grid-template-columns: 64px minmax(0, 1fr);
  /* Three rows in the right column now: name, the on-now line, the bar. */
  grid-template-rows: auto auto auto;
  column-gap: 12px;
  align-items: center;
  /* 44px is the floor every control in this app meets; three rows beside a
     64px logo clears it comfortably. Grown from 76px when the on-now line and
     its progress bar became the third row (B33). */
  min-height: 88px;
  padding: 10px 12px;
  text-align: left;
  border: 1px solid rgba(255,255,255,.10);
  border-radius: var(--radius);
  background: rgba(28,28,31,.72);
  transition: background var(--transition), border-color var(--transition), transform var(--transition);
}
.livetv-card:hover { background: var(--surface-focus); border-color: rgba(255,255,255,.2); transform: translateY(-2px); }
.livetv-card-art {
  grid-row: 1 / 4;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 64px;
  height: 48px;
  border-radius: var(--radius);
  background: rgba(0,0,0,.45);
  font-size: var(--fs-body);
  font-weight: var(--fw-bold);
  color: var(--muted);
  overflow: hidden;
}
/* `contain`, never `cover`: these logos are wordmarks on transparent
   backgrounds and cropping one cuts the name in half. */
.livetv-card-art img { width: 100%; height: 100%; object-fit: contain; }
.livetv-card-name {
  align-self: end;
  font-size: var(--fs-body);
  font-weight: var(--fw-bold);
  color: #fff;
  /* Provider names are long and full of separators. Two lines, then ellipsis —
     one line hid the part that distinguishes "CA - CNN HD" from "CHL: CNN HD". */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
/* B33. The "on now" line. It is ONE line whatever it holds — the programme when
   the fleet has a listing, the country and group when it does not — and the
   progress bar below it always takes its 4px whether or not a fill is drawn.
   Reserving both is what stops the whole grid jumping under the pointer when a
   guide answer lands for one card in fifty; tvOS reserves the same two boxes
   (LiveView.swift:597-614) for the same reason. */
.livetv-card-meta {
  align-self: center;
  font-size: var(--fs-caption);
  font-weight: var(--fw-semibold);
  color: var(--muted);
  min-height: 1.3em;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
/* A real listing is brighter than a fallback, so the eye finds what is ON. */
.livetv-card-meta[data-live="true"] { color: #fff; }
.livetv-card-progress {
  grid-column: 2;
  align-self: start;
  height: 4px;
  margin-top: 6px;
  border-radius: 2px;
  background: rgba(255,255,255,.12);
  overflow: hidden;
}
.livetv-card-progress-fill {
  display: block;
  width: 0;
  height: 100%;
  border-radius: 2px;
  background: linear-gradient(90deg, var(--accent), var(--accent-strong));
}

/* ── B23. The grid guide ────────────────────────────────────────────────────
   Three hours at 8px a minute, which is Fire TV's GuideActivity number. The
   channel column and the time ruler are pinned with position:sticky inside the
   ONE scroller, so horizontal and vertical alignment cannot drift — there is
   nothing to keep in step. */
.livetv-guide-bar {
  display: flex;
  align-items: center;
  gap: 14px;
  max-width: 1100px;
  margin: 0 auto;
  padding: 0 var(--gutter);
}
.livetv-guide-bar .search-status { margin: 0; padding: 0; }
.livetv-guide-grid {
  max-width: 1100px;
  margin: 16px auto 0;
  padding: 0 var(--gutter);
  overflow: auto;
  max-height: 68vh;
  border-radius: var(--radius);
}
.livetv-guide-inner {
  display: block;
  width: max-content;
  min-width: 100%;
}
.livetv-guide-head {
  position: sticky;
  top: 0;
  z-index: 3;
  display: flex;
  height: 36px;
  background: var(--bg);
  border-bottom: 1px solid rgba(255,255,255,.12);
}
.livetv-guide-corner {
  position: sticky;
  left: 0;
  z-index: 4;
  flex: 0 0 190px;
  width: 190px;
  display: flex;
  align-items: center;
  padding: 0 12px;
  font-size: var(--fs-caption);
  font-weight: var(--fw-bold);
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--muted);
  background: var(--bg);
}
.livetv-guide-times { display: flex; width: var(--livetv-timeline-width); }
.livetv-guide-time {
  display: flex;
  align-items: center;
  flex: 0 0 auto;
  padding: 0 8px;
  font-size: var(--fs-caption);
  font-weight: var(--fw-semibold);
  color: var(--muted);
  border-left: 1px solid rgba(255,255,255,.08);
}
.livetv-guide-row { display: flex; height: 56px; }
.livetv-guide-name {
  position: sticky;
  left: 0;
  z-index: 2;
  flex: 0 0 190px;
  width: 190px;
  padding: 0 12px;
  font-size: var(--fs-caption);
  font-weight: var(--fw-bold);
  color: #fff;
  background: var(--bg);
  border-top: 1px solid rgba(255,255,255,.06);
  /* Provider names are long. Two lines then ellipsis, as the cards do. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  align-content: center;
}
.livetv-guide-track { display: flex; width: var(--livetv-timeline-width); }
.livetv-guide-block {
  flex: 0 0 auto;
  min-width: 0;
  height: 100%;
  overflow: hidden;
  padding: 6px 8px;
  text-align: left;
  font-size: var(--fs-caption);
  font-weight: var(--fw-semibold);
  color: #fff;
  background: rgba(28,28,31,.82);
  border: 1px solid rgba(255,255,255,.08);
  border-radius: 4px;
  transition: background var(--transition), border-color var(--transition);
}
.livetv-guide-block:hover { background: var(--surface-focus); border-color: rgba(255,255,255,.22); }
.livetv-guide-block:focus-visible { outline: 3px solid var(--accent); outline-offset: -3px; }
/* A filled gap is still pressable — it tunes the channel, which is the whole
   point of a guide row with no listings in it — but it must not read as a
   programme. */
.livetv-guide-block[data-empty="true"] { color: var(--quiet); background: rgba(255,255,255,.03); }

@media (max-width: 640px) {
  .livetv-results { grid-template-columns: 1fr; }
  .livetv-guide-corner, .livetv-guide-name { flex-basis: 132px; width: 132px; }
  .livetv-guide-grid { max-height: 60vh; }
  .livetv-groups {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
  }
  .livetv-groups::-webkit-scrollbar { display: none; }
  .livetv-chip { flex: 0 0 auto; }
}
