:root {
  /* Cool grey neutrals under the amber accent. The ground stays near-black for contrast, while the
     surfaces and the navbar sit visibly lighter: an all-ink stack read as too heavy on the eyes. */
  --bg: #111318;
  --bg-elev: #23272b;
  --bg-elev-2: #2d3035;
  /* A surface that must read as RAISED above --bg-elev (the airing guide block). Dark lifts by
     going lighter (= --bg-elev-2); light ALSO lifts by going lighter, where --bg-elev-2 goes darker. */
  --bg-lift: #2d3035;
  --bg-deep: #16191c;
  /* rgb triplets of the grounds, for the translucent panels over video and artwork: Chrome 79
     has no color-mix(), so an alpha variant has to be rgba(var(--bg-rgb), a). Keep in step. */
  --bg-rgb: 17, 19, 24;
  --bg-deep-rgb: 22, 25, 28;
  --bg-elev-rgb: 35, 39, 43;

  /* Dark surfaces — navbar */
  --nav-bg: #343a40;
  --nav-bg-2: #23272b;
  --nav-bg-hover: rgba(255,255,255,0.08);

  /* Text. Kept soft on purpose: brighter greys read as harsh for long viewing on a dark-room TV.
     --fg-dim is ~2.5:1 on panels, so it is for secondary text, never a label you must read to act. */
  --fg: #c8c9ca;
  --fg-muted: #8d959d;
  --fg-dim: #5d6368;
  /* Reserved for controls that really are disabled, so "off" never shares a grey with live text. */
  --fg-disabled: #50565c;
  /* Inactive navbar tabs. The active tab is amber + its underline, so "selected" stays obvious. */
  --fg-tab: #8d959d;

  /* Text on dark surfaces (same palette) */
  --fg-on-dark: #c8c9ca;
  --fg-on-dark-muted: #6c757d;

  /* Accent ramp (amber). Every accent in the app — tints, glows, the logo chevrons, the "Nu" of the
     wordmark — derives from these tokens, so a palette change is a change here and nowhere else.
     The -rgb triplets exist because webOS ships Chrome 79, which has no color-mix(): an alpha
     variant has to be rgba(var(--accent-rgb), a). */
  --accent: #d97706;
  --accent-bright: #f59e0b;
  /* The light end, used by the third logo chevron. */
  --accent-light: #fbbf24;
  --accent-rgb: 217, 119, 6;
  --accent-bright-rgb: 245, 158, 11;
  --accent-dim: rgba(var(--accent-rgb), 0.12);
  /* The dark end of the accent ramp. Existed only as a raw #b45309 inside two hand-written
     gradients (.btn.primary and .play-btn) that had drifted into being written out twice. */
  --accent-deep: #b45309;
  /* THE filled-accent surface. One recipe, so a "primary" control cannot be a gradient in one place
     and a flat fill in another. */
  --accent-gradient: linear-gradient(135deg, var(--accent) 0%, var(--accent-deep) 100%);

  --star: #f59e0b;
  --success: #198754;
  --danger: #dc3545;
  /* STATUS colours on dark surfaces: an account that is fine / about to lapse / failed, a live or
     failed probe. The -rgb twins exist for tints: rgba(var(--status-ok-rgb), .14) works on webOS's
     Chrome 79, where color-mix() does not. */
  --status-ok: #4ade80;           --status-ok-rgb: 74, 222, 128;
  --status-warn: #fbbf24;         --status-warn-rgb: 251, 191, 36;
  --status-err: #f87171;          --status-err-rgb: 248, 113, 113;
  --status-err-strong: #ef4444;   --status-err-strong-rgb: 239, 68, 68;

  /* Card/banner surface. Was referenced by .sync-ready-banner but never DEFINED, which made its
     color-mix() invalid and silently dropped the banner's background on every platform. The
     color-mix() is gone too (Chrome 111+; webOS ships 79), so this is a plain surface now. */
  --bg-card: #1b1f24;
  /* The tinted variant that color-mix() was trying to produce: --accent at 15% over --bg-card. */
  --bg-card-accent: #2c2716;

  /* Borders */
  --border: #3b4046;
  --border-bright: #4b5158;
  /* The resting edge of a pressable control (buttons, icon buttons, pills, dropdown triggers). At
     --border it barely separated from --bg-elev-2, which is half of why controls read as disabled. */
  --border-control: #555c64;

  /* Scrollbar — ONE definition for every scroll region (see components/ScrollArea.tsx).
     The thumb is deliberately brighter than --border-bright: at that value it read as
     "barely there" against a dark panel on the TV, which is what made the player list look like
     it had no scrollbar at all. */
  /* Second pass, from the TV: 6px at 40% white was STILL "so subtle it feels like nothing is
     there", especially in the player's right panel over its translucent black. Widened and
     brightened, then walked back from 10px because that read as a heavy slab against the main
     grid's wide surface, and then from 7px too. Back to the original 6px WIDTH — width was never
     the problem, contrast was. What makes it legible now is the brighter thumb (60% vs the
     original 40%) plus the faint always-on rail behind it, so there is something to see even where
     the thumb is short. One token drives every scroller, so these move together everywhere. These are the single source for every scroller in the
     app (ScrollArea + the native fallback), so the grids, the player panel and the sidebars stay
     identical by construction — which is what was asked for. */
  --scrollbar-size: 6px;
  /* The TV edge inset: how far the few marks drawn AT a side edge (the scroll rails on the right)
     sit in on a TV. 0 here; TV builds raise it
     (:root.is-tv below). Measured with an edge test pattern (marks every 2px from each edge): the
     OLED's Screen Shift slides the whole picture ~2px sideways over time — one day the right edge
     lost 2px, the next the left — so BOTH side edges need it. Bottom stays at 0, and the bars'
     colour still runs to the edge; only the marks move.
     --tv-edge-top: a small breathing gap above the navbar's controls on the TV, chosen by eye (the
     top edge measured no crop). The navbar grows by it, so the bar's colour still meets the top
     edge and the controls sit 4px lower; the overlay hanging from the bar moves with it. */
  --tv-edge: 0px;
  --tv-edge-top: 0px;
  --scrollbar-thumb: rgba(255, 255, 255, 0.6);
  --scrollbar-thumb-hover: rgba(255, 255, 255, 0.75);
  --scrollbar-track: rgba(255, 255, 255, 0.05);

  /* THE row highlight, defined once. Grid rows, the EPG guide, the series episode list and the
     player's right panel all draw the same thing, so it cannot be four near-identical fills that
     drift apart again — they were 0.26-gradient / 0.06 / 0.05 / 0.06 flat, and the panels read as
     "subtle" next to the grid. --accent-bright, not --accent: the darker shade made the border a
     brownish edge next to a yellow label. */
  /* EVERY DROPDOWN LIST (tv.css, .dd-panel): one opaque raised panel, a hairline edge, a rule under
     its heading — a step above --bg-elev so it reads as lifted off whatever it opens over. */
  --dd-bg: #1c2026;
  --dd-edge: #31363d;
  --dd-rule: #2a2f35;
  --row-highlight: linear-gradient(90deg,
    rgba(var(--accent-bright-rgb), 0.26) 0%,
    rgba(var(--accent-bright-rgb), 0.10) 50%,
    rgba(var(--accent-bright-rgb), 0.02) 100%);
  /* A CAM print's lit state (frontend lib/cam): the same highlight, in red instead of amber, so a
     cinema recording never reads as one more copy of the film. */
  --cam: #ef4444;
  --cam-rgb: 239, 68, 68;
  --cam-highlight: linear-gradient(90deg,
    rgba(var(--cam-rgb), 0.30) 0%,
    rgba(var(--cam-rgb), 0.12) 50%,
    rgba(var(--cam-rgb), 0.03) 100%);

  /* TV overscan inset — 0 everywhere; a per-TV localStorage override can set it (lib/platform.ts). See that file for why this insets instead of
     scaling, and for the TV-side "Just Scan" setting that removes the need for it entirely. */
  --overscan: 0px;

  --radius-s: 4px;
  --radius-m: 6px;
  --radius-l: 8px;
  --radius-pill: 999px;

  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;
  --space-7: 48px;
  --space-8: 64px;

  /* Layout dimensions */
  --navbar-h: 57px;
  --filter-w: 260px;

  /* Font sizes — bumped +1px from previous.
     --font-sm and --font-xxs were added after an audit found 13px written out 36 times (the de
     facto "supporting text" size) and 11px 24 times (the de facto badge size) with no token to
     name either — the two commonest sizes in the app were the two with no token. */
  --font-xxs: 11px;
  --font-sm: 13px;
  --font-xs: 17px;
  --font-s:  18px;
  --font-m:  20px;
  --font-l:  23px;
  --font-xl: 27px;
  --font-xxl: 35px;

  /* The label column in every label/value pair — the detail panels' meta grid, the info dialog's,
     and the preview band's Director/Cast rows. One width, so the values line up at the same place
     on every surface that shows them. */
  /* The label column in the detail panels' metadata table, and in the band's credit rows. 72px: every
     label in it is "Director" or "Cast" (~56px at --font-sm plus the cell's 8px gutter), and a wider
     column leaves a tab-stop's worth of empty space between the word and its value. Both users of this token carry the same two
     words, so they tighten together and stay aligned with each other. Sized to "Director" plus the
     cell's gutter and nothing more — the labels stay left-aligned in one column, so this width is
     the whole of what separates a label from its value. */
  --meta-label-w: 60px;

  --ctrl-h: 40px;
  /* The small square control: icon buttons, close buttons, the compact .btn variants. */
  --ctrl-h-sm: 32px;

  --shadow-sm: 0 1px 3px rgba(0,0,0,0.25);
  --shadow: 0 2px 8px rgba(0,0,0,0.30);
  --shadow-lg: 0 8px 24px rgba(0,0,0,0.40);

  /* ONE ring. An audit found seven competing amber ring formulas (four outline variants at two
     widths and three box-shadow alphas) for the same idea. --focus-ring is the outer form;
     --focus-ring-inset is for controls that must ring INSIDE their own box, where an outer ring
     would overlap a neighbour or spill out of a clipped container (.pc-mode, the corner "Read more" chip).
     NOT for a small square control sitting in a strip of them: 2px inside a 30px box reads as a
     drawn border rather than as focus, which is what took it off .view-switch-opt — those follow
     the strip's own language, a 1px recolour identical to their hover. */
  --focus-ring: 0 0 0 2px var(--accent), 0 0 0 4px rgba(var(--accent-rgb), 0.25);
  --focus-ring-inset: inset 0 0 0 2px var(--accent-bright);
  /* CONTROLS DRAWN ON MEDIA — the player's buttons, its ✕, the ▶/ⓘ marks on artwork. One look
     everywhere: nothing loud at rest, and lit = white GLASS with an amber glyph (a solid amber fill
     was too dominant). --media-plate is the dark scrim a control keeps under the glass on video;
     --media-frost is the denser, theme-dark plate under BLURRED artwork for the big ▶ marks, where
     a plain plate let a busy poster show through. Dense (0.80) because it is see-through: a lighter
     plate takes the brightness of its picture, and a ▶ on a bright TV still reads lighter than the
     same mark on a dark poster. */
  --media-glass: rgba(255,255,255,0.14);
  --media-glass-pressed: rgba(255,255,255,0.22);
  --media-plate: rgba(0,0,0,0.72);
  --media-frost: rgba(17,19,24,0.80);
  /* Text inputs and select triggers: a soft glow rather than an outline, which would fight the
     caret and double up with the border they already change on focus. */
  --ring-field: 0 0 0 3px rgba(var(--accent-rgb), 0.15);
  --easing: cubic-bezier(0.4, 0, 0.2, 1);
  /* Faint chrome lines and fills that must follow the theme: a white hairline/hover vanishes on the
     light ground, and a black one does on the dark. Over video or artwork, keep the literal white. */
  --hairline: rgba(255,255,255,0.07);
  --hover-tint: rgba(255,255,255,0.04);
  /* A name chip on a surface (the band's cast): readable as a shape, quieter than a control. */
  --chip-bg: rgba(255,255,255,0.10);
  /* The hero title (preview band + info card): white lifted off the blurred-poster wash by a shadow. */
  --fg-strong: #ffffff;
  --hero-title-shadow: 0 1px 3px rgba(0,0,0,0.55);
}

* { box-sizing: border-box; }
html, body, #root { height: 100%; margin: 0; padding: 0; }
body {
  background: var(--bg);
  color: var(--fg);
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
  font-size: var(--font-s);
  font-weight: 400;
  line-height: 1.5;
  overflow: hidden;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
button { font: inherit; color: inherit; font-weight: 500; }
input { font: inherit; color: inherit; }
img { display: block; max-width: 100%; }

h1, h2, h3, h4 { font-weight: 700; line-height: 1.3; margin: 0; }

/* Split, never merged. webOS ships Chrome 79 and :focus-visible landed in Chrome 86; an unknown
   pseudo-class makes the parser discard the WHOLE selector list, so while these shared a list the
   focus ring was silently desktop-only — .is-focused, which is the JS-driven fallback the TV
   actually relies on, never got it there. Row-shaped elements opt back out in tv.css (a ring
   around a full-width row reads as a border on its top edge, not a ring). */
[data-focusable="true"]:focus-visible {
  outline: none; box-shadow: var(--focus-ring); z-index: 5;
}
[data-focusable="true"].is-focused {
  outline: none; box-shadow: var(--focus-ring); z-index: 5;
}

/* Native scrollbars stay styled as a fallback for anything not yet wrapped in <ScrollArea>, and
   for the platforms that DO honor these rules. Matched to the ScrollArea thumb so the two can
   never look like different scrollbars. */
::-webkit-scrollbar { width: var(--scrollbar-size); height: var(--scrollbar-size); }
::-webkit-scrollbar-track { background: var(--scrollbar-track); }
::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover); }

/* ── Shared scroll container ────────────────────────────────────────────────────────────────
   The scroller and the thumb are SIBLINGS: as a descendant the thumb would scroll away with the
   content it tracks. Geometry is written from JS (useScrollThumb). */
.scroll-wrap {
  /* min-height:0 — a flex child defaults to min-height:auto, which sizes it to its content
     instead of the parent's flex-basis, so it silently grows past the container and never
     clips/scrolls (this exact trap hid several scrollbars before). */
  flex: 1; min-height: 0; position: relative; display: flex;
}
.scroll-viewport {
  flex: 1; min-height: 0; overflow-y: auto;
  /* overflow-x: hidden STATED. Setting only overflow-y makes the other axis compute to `auto` (a
     box cannot scroll on one axis and be `visible` on the other), so every scroller in the app was
     silently a HORIZONTAL scroller too — and a wall of cards overflows its box by a pixel or two
     routinely. Nothing here ever wants to scroll sideways: the guide pans by writing a custom
     property, and the season tabs own their own overflow-x outside this component.
     What it cost: focusing a card calls scrollIntoView({ inline: 'nearest' }) (useSpatialNav) and
     the browser does the same on focus() by itself, so a card the slide-over was covering would
     shove the whole wall sideways to "reveal" it — the grid appearing to slide as the panel came
     in. It never did it on the FIRST open, because until the panel has been closed once nothing has
     put focus on a card; closing calls findRowById(id).focus(). */
  overflow-x: hidden;
  -webkit-overflow-scrolling: touch;
  /* Hide the PLATFORM scrollbar here — this element draws its own thumb (.scroll-thumb) and the
     global ::-webkit-scrollbar rule below would otherwise paint a second bar right next to it
     (reported as "a bigger one overlapping with a smaller one"). The global rule still applies to
     anything not wrapped in <ScrollArea>. */
  scrollbar-width: none;
}
.scroll-viewport::-webkit-scrollbar { width: 0; height: 0; display: none; }
/* Block-flow variant for containers sized from the OUTSIDE (the detail sidebars: fixed width +
   align-self:stretch). webOS 4's WebKit shrinks flex-column children to fit the scroll container
   instead of scrolling them — that's what made the series plot and episode list overlap — so
   these panels must stay block flow. The wrapper keeps the panel's box; the viewport fills it. */
.scroll-wrap--block { display: block; position: relative; flex: none; }
.scroll-wrap--block > .scroll-viewport { display: block; height: 100%; box-sizing: border-box; }
/* The detail sidebars' padding moved off the panel box onto the scroller, so content scrolls
   clear of the edges instead of being clipped by the wrapper's padding. */
.scroll-viewport--sidebar { padding: var(--space-4); }
.scroll-track {
  position: absolute; top: 0; bottom: 0; right: calc(2px + var(--tv-edge));
  width: var(--scrollbar-size);
  /* A faint always-on rail. Without it the only thing on screen was the thumb, so a short thumb on
     a long list looked like a stray mark rather than a scroll position. */
  background: var(--scrollbar-track);
  border-radius: calc(var(--scrollbar-size) / 2);
  pointer-events: none; /* display-only — d-pad/wheel drive the scroll, not a drag */
}
:root.is-tv { --tv-edge: 2px; }
:root.is-tv { --tv-edge-top: 4px; }
.scroll-thumb {
  position: absolute; top: 0; right: 0;
  width: var(--scrollbar-size); height: 0;
  border-radius: calc(var(--scrollbar-size) / 2);
  background: var(--scrollbar-thumb);
  display: none; /* JS shows it only when the content actually overflows */
}
/* The rail is meaningless when nothing overflows — useScrollThumb hides the thumb in that case,
   so mirror it here rather than leaving an empty rail down every non-scrolling panel. */
.scroll-wrap:not(.has-overflow) > .scroll-track { background: transparent; }
