:root {
  --bg: #000;
  --fg: #f5f5f5;
  --accent: #ff6b6b;
  --accent-2: #4ecdc4;
  /* Dark card/chip tone matches the search-overlay and up-next cards
     further down (#1e1e28), which were already dark -- this just makes
     the whole kid UI consistently dark instead of introducing a second
     dark shade. */
  --chip-bg: #1e1e28;
  --chip-bg-active: #ff6b6b;
  --chip-fg-active: #ffffff;
  --card-bg: #1e1e28;
  /* A black shadow reads as elevation because cards/chips (#1e1e28) are
     still lighter than the page's pure black -- but needed a higher
     opacity than the light-theme original to actually show up. */
  --shadow: 0 4px 14px rgba(0, 0, 0, 0.6);

  /* Text/control sizing: these are the laptop/browser-window defaults.
     The TV runs as a real ~1920px-wide app with no browser chrome and is
     viewed from across a room, so it gets meaningfully larger everything
     via the media query below -- a laptop browser window is essentially
     never that wide, so this cleanly tells the two apart without any
     device-sniffing. Sized to what this UI originally shipped with,
     before the TV-specific pass. */
  --fs-h1: 28px;
  --fs-btn: 16px;
  --btn-pad: 10px 20px 10px 16px;
  --fs-chip: 16px;
  --chip-h: 48px;
  --chip-pad: 0 18px 0 8px;
  --chip-img: 32px;
  --fs-sort: 15px;
  --sort-h: 34px;
  --sort-pad: 0 20px;
  --fs-badge: 13px;
  --badge-pad: 2px 7px;
  --fs-card-title: 16px;
  --card-title-pad: 12px 14px 16px;
  --card-title-lh: 21px;
  /* See .card-title's comment below for why this isn't just "2 lines +
     padding". */
  --card-title-max-h: 53px;
  --fs-empty: 20px;
  --fs-search-input: 30px;
  --fs-upnext-h2: 24px;
  --close-btn-size: 56px;
  --fs-close-btn: 26px;
  --search-close-btn-size: 48px;
  --fs-player-time: 16px;
  --player-play-btn-size: 40px;
  --player-progress-h: 6px;
}

@media (min-width: 1800px) {
  :root {
    --fs-h1: 46px;
    --fs-btn: 26px;
    --btn-pad: 12px 24px 12px 18px;
    --fs-chip: 26px;
    --chip-h: 64px;
    --chip-pad: 0 24px 0 8px;
    --chip-img: 44px;
    --fs-sort: 24px;
    --sort-h: 50px;
    --sort-pad: 0 26px;
    --fs-badge: 17px;
    --badge-pad: 3px 9px;
    --fs-card-title: 26px;
    --card-title-pad: 14px 16px 18px;
    --card-title-lh: 34px;
    --card-title-max-h: 86px;
    --fs-empty: 28px;
    --fs-search-input: 40px;
    --fs-upnext-h2: 34px;
    --close-btn-size: 72px;
    --fs-close-btn: 34px;
    --search-close-btn-size: 62px;
    /* The whole point of this bar existing: YouTube's own native time
       text was confirmed too small to read from a couch, on the real TV. */
    --fs-player-time: 30px;
    --player-play-btn-size: 56px;
    --player-progress-h: 10px;
  }
}

header.top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 18px 24px;
}

header.top h1 {
  margin: 0;
  font-size: var(--fs-h1);
  letter-spacing: -0.5px;
}

header.top h1 .dot {
  color: var(--accent);
}

.search-btn {
  display: flex;
  align-items: center;
  gap: 8px;
  background: var(--chip-bg);
  border: none;
  border-radius: 999px;
  padding: var(--btn-pad);
  font-size: var(--fs-btn);
  font-weight: 600;
  color: var(--fg);
  box-shadow: var(--shadow);
}

.search-btn:hover, .search-btn:focus-visible {
  background: var(--accent-2);
  color: #fff;
}

.channels {
  display: flex;
  gap: 10px;
  overflow-x: auto;
  /* Top/bottom only -- left/right clearance (for horizontal scrolling and
     the focus glow, see the ::before/::after spacers below) is handled
     entirely by real flex items instead of padding now, not stacked on
     top of it: padding-left/right here used to be 36px in addition to
     those spacers, which pushed the whole row ~70px further right than
     every other row on the page (header, sort tabs, grid all use 24px),
     visibly misaligned. */
  padding: 20px 0 40px;
  scrollbar-width: none; /* Firefox; Chromium (incl. the TV) needs ::-webkit-scrollbar below instead */
  scroll-behavior: smooth;
  /* scrollIntoView (used when a chip is focused, see kid.js) otherwise
     stops the instant the target is minimally visible -- flush against
     the edge, zero clearance -- since that's the literal definition of
     "nearest". This tells it (and would tell scroll-snap, if this ever
     uses that again) to treat 36px at each edge as off-limits, so it
     stops with the same clearance the ::before/::after spacers below
     exist to make room for. */
  scroll-padding-left: 36px;
  scroll-padding-right: 36px;
  /* Fades the trailing edge as a hint that there's more to scroll to,
     without needing a hard cutoff line. Mirrors on the leading edge once
     scrolled away from the start (see the [data-scrolled] rule below).
     Needs the -webkit- prefix: unprefixed mask-image only landed in
     Chromium ~120, well after the TV's ~87. */
  -webkit-mask-image: linear-gradient(to right, black calc(100% - 48px), transparent);
  mask-image: linear-gradient(to right, black calc(100% - 48px), transparent);
}

/* scrollbar-width: none above only hides it in Firefox -- Chromium
   (including the TV) needs its own vendor-prefixed pseudo-element. D-pad
   is the only real input method here, so a scrollbar is dead weight, not
   an affordance: nothing can click-drag it, and the mask-image edge fade
   already does the "there's more to scroll to" hinting job. */
.channels::-webkit-scrollbar {
  display: none;
}

.channels[data-scrolled] {
  -webkit-mask-image: linear-gradient(to right, transparent, black 48px, black calc(100% - 48px), transparent);
  mask-image: linear-gradient(to right, transparent, black 48px, black calc(100% - 48px), transparent);
}

/* Confirmed on the real TV: padding on a scrollable flex container isn't
   reliably honored at either end once actual scrolling is involved --
   scrolling to the end clipped the last chip against the trailing edge
   (padding-right effectively ignored there), and turned out to clip the
   very first chip against the leading edge too, contradicting the
   leading-edge-is-fine assumption desktop testing suggested. Rather than
   keep chasing which edge is affected on which engine, both ends now use
   an actual flex item instead of container padding -- unlike padding, a
   real flex item always counts toward scrollable width, regardless of
   any given engine's specific padding-in-overflow quirks. */
.channels::before,
.channels::after {
  content: "";
  flex: 0 0 36px;
}

.chip {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  /* Fixed height, not padding-derived: content-driven height depends on
     the text span's line-height, which can compute slightly differently
     across browser engines (seen between this and the TV's older
     Chromium) and threw chips with vs. without a thumbnail out of visual
     alignment even though their padding matched. A fixed height is
     immune to that regardless of root cause. */
  height: var(--chip-h);
  background: var(--chip-bg);
  border: none;
  border-radius: 999px;
  padding: var(--chip-pad);
  font-size: var(--fs-chip);
  font-weight: 600;
  box-shadow: var(--shadow);
  color: var(--fg);
}

/* Chips without a channel thumbnail (just "All videos") have no avatar
   hugging the left edge, so the asymmetric padding above -- which exists
   to balance the round image against the text -- would leave lone text
   looking off-center. */
.chip-no-thumb {
  padding: 0 20px;
}

.chip img {
  width: var(--chip-img);
  height: var(--chip-img);
  border-radius: 50%;
  object-fit: cover;
  background: #333;
}

.chip.active {
  background: var(--chip-bg-active);
  color: var(--chip-fg-active);
}

/* Stacked box-shadows follow each element's own border-radius exactly (a
   plain outline doesn't reliably, especially on the TV's older Chromium):
   a gap matching the page background for separation, a solid warm ring,
   then a soft blurred glow in the same color so it reads as a highlight
   rather than a flat line sitting on the edge. */
.chip:focus-visible, .card:focus-visible, .close-btn:focus-visible, .sort-tab:focus-visible {
  outline: none;
  box-shadow:
    0 0 0 4px var(--bg),
    0 0 0 8px var(--accent),
    0 0 24px 4px rgba(255, 107, 107, 0.6),
    0 10px 20px rgba(0, 0, 0, 0.25);
}

.sort-tabs {
  display: flex;
  gap: 8px;
  padding: 0 24px 16px;
}

.sort-tab {
  display: flex;
  align-items: center;
  height: var(--sort-h); /* fixed, not padding-derived -- see .chip for why */
  border: none;
  background: var(--chip-bg);
  color: var(--fg);
  font-weight: 700;
  font-size: var(--fs-sort);
  padding: var(--sort-pad);
  border-radius: 999px;
  box-shadow: var(--shadow);
}

.sort-tab.active {
  background: var(--accent-2);
  color: #fff;
}

main {
  padding: 8px 24px 40px;
}

.grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 20px;
}

.card {
  background: var(--card-bg);
  border: none;
  border-radius: 18px;
  overflow: hidden;
  box-shadow: var(--shadow);
  text-align: left;
  padding: 0;
  /* transform only, deliberately -- box-shadow isn't GPU-compositable, so
     animating the focus ring's blurred glow (below) forced a full repaint
     on every D-pad move and made navigation feel sluggish on the TV's
     older/weaker Chromium. transform stays cheap; the ring now snaps in
     instantly instead, which reads as instant at this size anyway. */
  transition: transform 0.12s ease;
}

.card:hover, .card:focus-visible {
  transform: translateY(-4px) scale(1.02);
}

.thumb-wrap {
  position: relative;
  /* 16:9 via padding-bottom, not aspect-ratio: TV browsers can be on much
     older Chromium/WebKit than aspect-ratio's ~2021 baseline, and without
     it the box collapses to zero height. */
  padding-bottom: 56.25%;
  background: #222;
}

.thumb-wrap img {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.duration-badge {
  position: absolute;
  right: 8px;
  bottom: 8px;
  background: rgba(0, 0, 0, 0.75);
  color: #fff;
  font-size: var(--fs-badge);
  font-weight: 600;
  padding: var(--badge-pad);
  border-radius: 6px;
}

/* How far into a video the kid last got, mirroring the player's own
   progress bar (same --accent fill) so it reads as the same concept in
   both places. Flush against the thumbnail's bottom edge, like YouTube's
   own watched-progress bar, rather than a badge/label -- recognizable at a
   glance without needing to read anything. */
.progress-indicator {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 4px;
  background: rgba(0, 0, 0, 0.5);
}

.progress-indicator-fill {
  height: 100%;
  background: var(--accent);
}

.card-title {
  padding: var(--card-title-pad);
  font-size: var(--fs-card-title);
  font-weight: 600;
  line-height: var(--card-title-lh);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  text-overflow: ellipsis;
  /* -webkit-line-clamp alone (even paired with an "exact" max-height of
     2 lines + padding) still let bold glyph ascenders from the clipped
     3rd line visibly overshoot into view -- confirmed via an isolated
     test case outside this app entirely, so it's a genuine browser
     text-clipping quirk (ascenders overshooting the nominal line-height
     box), not a bug in this page's own layout. --card-title-max-h is
     deliberately less than 2 lines + padding at every size: it eats into
     line 2's empty leading space (not its visible glyphs -- verified line
     2 still renders in full) and gives the 3rd line's overshoot room to
     stay hidden. */
  max-height: var(--card-title-max-h);
}

.empty-state {
  text-align: center;
  padding: 80px 20px;
  font-size: var(--fs-empty);
  color: #8a8a9a;
}

.search-overlay {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  background: #12121a;
  z-index: 20;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
}

.search-overlay[hidden] { display: none; }

.search-bar {
  position: sticky;
  top: 0;
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 28px 32px;
  color: #fff;
  background: #12121a;
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}

.search-bar svg { flex: 0 0 auto; opacity: 0.7; }

.search-bar input {
  flex: 1;
  background: none;
  border: none;
  outline: none;
  color: #fff;
  font-size: var(--fs-search-input);
  font-weight: 600;
  font-family: inherit;
  caret-color: var(--accent-2);
}

.search-bar input::placeholder { color: rgba(255, 255, 255, 0.4); }

.search-bar .close-btn {
  position: static;
  flex: 0 0 auto;
  width: var(--search-close-btn-size);
  height: var(--search-close-btn-size);
}

.search-results {
  padding: 8px 32px 40px;
}

.search-results .grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; }

.search-results .empty-state { color: rgba(255, 255, 255, 0.5); }

.search-results .card { background: #1e1e28; }
.search-results .card-title { color: #fff; }

.player-overlay {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  background: #000;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 30;
}

.player-overlay[hidden] { display: none; }

.player-frame-outer {
  position: relative;
  width: 100%;
  max-width: 1280px;
}

/* padding-bottom % resolves against the containing block's width, not this
   element's own max-width-capped width, so the ratio box has to be nested
   inside a separately-capped wrapper (.player-frame-outer) rather than
   carrying width/max-width itself. Without that split, on a viewport wider
   than 1280px (any TV) the percentage would resolve against the full
   .player-overlay width, giving the wrong height. */
.player-frame-wrap {
  position: relative;
  padding-bottom: 56.25%; /* 16:9 */
}

.player-frame-wrap iframe {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  width: 100%;
  height: 100%;
  border: 0;
}

/* Shown on top of the iframe (a later DOM sibling within .player-frame-wrap,
   see openPlayer in kid.js) until the underlying video is confirmed to
   actually be advancing (see updatePlayerTick -- NOT onStateChange's
   PLAYING report, which was confirmed to lag the real video by up to a
   couple of real seconds, long enough that the poster was still covering
   a video the kid could already hear). The real gap between opening the
   player and the embedded video visibly starting is otherwise just a
   blank black box with no confirmation the tap registered or which video
   was picked, which is exactly what invites a kid to tap again (each
   extra tap on a still-focused card can even restart the whole load, see
   openPlayer's blur comment). Explicit z-index, not relied-on DOM order --
   the iframe's mount div is created later, after an awaited YouTube API
   load, so ordering alone can't guarantee this stays on top of it. */
.player-loading {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 1;
}

.player-loading.player-loading-hidden {
  display: none;
}

/* Re-using the card's own already-loaded thumbnail costs no extra request.
   object-fit: cover, not contain -- matches how the same image already
   crops to fill the card's 16:9 box in the grid, so there's no visible
   letterboxing switch once the iframe takes over underneath. */
.player-poster {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  background: #000;
}

/* Centered over the poster, confirming to a kid that something is actually
   happening during the loading gap rather than the static thumbnail just
   sitting there unchanged. Has its own dark fill (not just a bare ring) --
   confirmed live that a plain white ring is nearly invisible against a
   bright/light-colored thumbnail (a real screenshot with a mostly-yellow
   frame made it almost disappear), so contrast can't depend on what the
   thumbnail underneath happens to look like. transform-only animation
   (rotation), not anything layout/paint-triggering -- see the "don't
   animate box-shadow" note elsewhere in this file for why that
   distinction matters on the TV's weaker hardware. */
.player-spinner {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 56px;
  height: 56px;
  margin: -28px 0 0 -28px;
  background: rgba(0, 0, 0, 0.45);
  border: 5px solid rgba(255, 255, 255, 0.3);
  border-top-color: #fff;
  border-radius: 50%;
  animation: player-spinner-spin 0.9s linear infinite;
}

@keyframes player-spinner-spin {
  to {
    transform: rotate(360deg);
  }
}

/* Our own playback UI, replacing YouTube's native controls entirely
   (playerVars.controls: 0, see openPlayer in kid.js) -- built because the
   native control bar's time/duration text was confirmed too small to
   read on the real TV, and its heavier native chrome (fade animations,
   button states) was suspected of contributing to sluggish-feeling
   play/pause on the TV's weaker hardware. This is why the player is now
   fullscreened via .player-frame-outer (a plain div) rather than the
   iframe directly like before: only a fullscreened element's own
   descendants render while fullscreen, and this bar needs to be visible
   then too, not just windowed. Purely visual/non-interactive-drag by
   design -- play/pause and seeking are already handled globally by
   keydown (Enter/Left/Right, see below), matching D-pad input rather
   than requiring the bar itself to be focused or dragged. */
.player-controls {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 14px 24px;
  background: linear-gradient(to top, rgba(0, 0, 0, 0.75), transparent);
  opacity: 1;
  transition: opacity 0.2s ease;
}

/* Auto-hidden after a few seconds of no D-pad activity while playing (see
   showControls/CONTROLS_HIDE_MS in kid.js) -- used to just stay up
   permanently, covering the bottom of the video the whole time. Faded via
   opacity rather than `hidden`/`display:none` for a smooth transition, and
   pointer-events is dropped too so the (invisible) bar can't intercept a
   click meant for the video underneath it. */
.player-controls.player-controls-hidden {
  opacity: 0;
  pointer-events: none;
}

.player-play-btn {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--player-play-btn-size);
  height: var(--player-play-btn-size);
  border: none;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.15);
  color: #fff;
}

/* Not the `hidden` attribute/property: confirmed directly that setting
   `svg.hidden = true` on an <svg> element only ever changes the JS
   property, never the actual `hidden` content attribute (hasAttribute
   stayed false throughout) -- `hidden`'s HTML-side reflection is defined
   on HTMLElement, and <svg> is an SVGElement, so it never took effect at
   all. The icon kid.js thought it was hiding was never actually hidden by
   either the browser's own [hidden]{display:none} UA rule or any
   attribute-selector rule of ours -- both play and pause rendered
   permanently, whatever the static markup said at parse time, regardless
   of any later JS toggle. A plain class kid.js toggles directly via
   classList doesn't depend on any of that reflection behavior. */
.player-play-btn svg.icon-hidden {
  display: none;
}

.player-time {
  flex: 0 0 auto;
  font-size: var(--fs-player-time);
  font-weight: 600;
  /* No line-height here read as visibly stretched on the real TV --
     unlike every other text element in this file, which all set one
     explicitly. Likely some interaction between the inherited default and
     whatever fallback font webOS actually resolves the font stack to
     (nothing in it is guaranteed present there). font-variant-numeric:
     tabular-nums was dropped for the same reason -- the most unusual
     typographic property on this element, and not essential (just
     anti-jitter as the digits change), so the simplest thing to remove
     first rather than chase the exact cause on a platform that can't be
     tested directly. */
  line-height: 1.2;
  color: #fff;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
}

.player-progress {
  flex: 1;
  height: var(--player-progress-h);
  border-radius: calc(var(--player-progress-h) / 2);
  background: rgba(255, 255, 255, 0.25);
  overflow: hidden;
}

.player-progress-fill {
  height: 100%;
  width: 0%;
  background: var(--accent);
  border-radius: inherit;
}

.up-next {
  width: 100%;
  height: 100%;
  overflow-y: auto;
  padding: 40px 24px;
  box-sizing: border-box;
}

.up-next h2 {
  color: #fff;
  font-size: var(--fs-upnext-h2);
  margin: 0 0 20px;
}

.up-next .card { background: #1e1e28; }
.up-next .card-title { color: #fff; }

.close-btn {
  position: absolute;
  top: 20px;
  right: 24px;
  width: var(--close-btn-size);
  height: var(--close-btn-size);
  border-radius: 50%;
  border: none;
  background: var(--accent);
  color: #fff;
  font-size: var(--fs-close-btn);
  line-height: 1;
  box-shadow: var(--shadow);
}

/* Small, unobtrusive build identifier -- for confirming which build a
   given device (especially the TV, where checking /kidtube-version
   directly isn't convenient) is actually running, not a kid-facing
   feature. pointer-events: none and no tabindex so it's never clickable
   and never enters the D-pad focus order; low opacity so it reads as a
   watermark, not a UI element competing for attention. */
.build-version {
  position: fixed;
  right: 8px;
  bottom: 6px;
  z-index: 40;
  font-size: 11px;
  color: var(--fg);
  opacity: 0.25;
  pointer-events: none;
  user-select: none;
}
