/* ============================================================
   DESIGN TOKENS -- Retro Guide Live Integration gate.
   Carries forward the original --panel/--amber/--teal/--red values
   verbatim (nothing that already worked was thrown away); adds the
   magenta/azure synthwave range from the approved reference art and
   design artifact. Single committed visual world (CRT broadcast
   console) -- dark-only by design, every color painted explicitly.
   ============================================================ */
:root {
  --bg: #07070d;
  --bg-deep: #030305;
  --panel: #12121c;
  --panel-raised: #191926;
  --grid-line: #262636;
  --grid-soft: #1b1b28;
  --text: #e8e8f0;
  --text-dim: #9a9aae;
  --text-faint: #55556a;

  --amber: #ffb000;   /* tuned / on-air */
  --teal: #29e6c8;    /* guide focus */
  --magenta: #ff2f9e; /* flagship / station-identity channels (01, 13) */
  --azure: #3d8bff;   /* system / status */
  --red: #ff4d4d;      /* technical error */

  --font-display: "Audiowide", "Arial Narrow", sans-serif;
  --font-mono: "JetBrains Mono", "Consolas", "Courier New", monospace;

  /* Motion Pass 3, section 5: authored-video -> live-SVG handoff. Short
     enough to read as "signal lock", not a dissolve -- tune here only. */
  --crossfade-ms: 140ms;

  /* Motion Pass 4A, Change 1: how long after the live ident is visible
     EMAGIN8 begins resolving, and how long its own reveal takes. */
  --emagin8-reveal-delay: 350ms;
  --emagin8-reveal-duration: 260ms;

  /* Right-Rail Hierarchy Correction: the FOCUS dashboard dock's fixed
     outer height -- the single source of truth .right-column's
     grid-template-rows and #focus-panel's own height both read from, so
     the two can never drift apart into two different numbers.

     Previously 230px. Live TV-photo evidence (field test, CH03/04/11)
     showed that height permanently claiming ~40% of the right rail --
     whether or not FOCUS was actually being hovered/highlighted -- left
     ON AIR's own preview-card too little room: measured, its video
     shrank to 7-27px, a sliver instead of a usable picture. FOCUS is
     navigation state; ON AIR is the primary television state, and per
     the "ON AIR gets priority, FOCUS is compact" rule this dock's own
     internal children (label/channel-number/hero/2-line title/episode)
     are now sized to fit this smaller box (see the FOCUS section below),
     not the other way around. Content that still doesn't fit clamps
     (line-clamp/ellipsis) or clips (this box's own overflow: hidden)
     rather than growing this value.

     Deliberately a flat px, not a vh-scaled clamp() -- an earlier
     attempt at scaling this DOWN as the viewport shortened (to free
     more room for ON AIR) just moved the exact same "fixed content,
     shrinking container" overflow bug from .preview-card into THIS
     box instead: FOCUS's own children have their own fixed budget (see
     below) and, unlike ON AIR's video, nothing left inside FOCUS to
     sacrifice first. Keeping this flat means FOCUS's fit is a fixed,
     one-time-verified budget instead of a moving target across every
     viewport height; short-viewport headroom for ON AIR's own content
     is recovered downstream instead (.preview-video's own lower floor,
     plus the @media (max-height: 700px) block further below). */
  --focus-dock-height: 150px;
}

* {
  box-sizing: border-box;
}

/* Human Acceptance Correction (Step 1 TV retest): a global backstop for
   the `hidden` attribute -- this is the THIRD time an author rule's own
   `display` value (no [hidden] guard) outranked the browser's default
   [hidden]{display:none} UA styling and left an "inactive" surface
   visibly rendering (.preview-video, #focus-panel, and now
   .preview-card:fullscreen .preview-weather, which unconditionally set
   display:flex and leaked Weather text under an ordinary MEDIA channel's
   fullscreen video). `!important` here is deliberate and load-bearing: it
   is what lets this one rule win over every future higher-specificity
   selector too, not just the ones already found, without auditing every
   [hidden] toggle in this file by hand again. If a future element
   genuinely needs to override this, that is itself worth a second look
   -- `hidden` is meant to mean "not displayed," full stop. */
[hidden] {
  display: none !important;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-mono);
  min-height: 100vh;
  position: relative;
  overflow-x: hidden;
  background-image:
    radial-gradient(ellipse 900px 500px at 10% -10%, rgba(61, 139, 255, 0.08), transparent 60%),
    radial-gradient(ellipse 700px 500px at 100% 0%, rgba(255, 47, 158, 0.06), transparent 55%);
}

/* Broadcast-monitor scanline texture -- deliberately subtle, purely
   decorative, and non-interactive (pointer-events: none). CRT-effect
   rule: if this ever competes with legibility, it loses -- kept at a low
   opacity for exactly that reason. */
.crt-overlay {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 50;
  background: repeating-linear-gradient(
    to bottom,
    rgba(255, 255, 255, 0.03) 0px,
    rgba(255, 255, 255, 0.03) 1px,
    transparent 1px,
    transparent 3px
  );
  mix-blend-mode: overlay;
}

.power-standby-overlay {
  position: fixed;
  inset: 0;
  z-index: 100;
  background: #000;
}

.channel-entry-indicator {
  position: fixed;
  top: 18px;
  right: 24px;
  z-index: 60;
  background: var(--panel);
  border: 2px solid var(--amber);
  border-radius: 4px;
  padding: 6px 14px;
  font-size: 1.6rem;
  font-weight: 700;
  color: var(--amber);
  letter-spacing: 0.1em;
  font-family: var(--font-mono);
}

/* ============================================================
   SHELL
   ============================================================ */
.shell {
  display: flex;
  height: 100vh;
}

.rail {
  width: 76px;
  flex: none;
  background: var(--bg-deep);
  border-right: 1px solid var(--grid-line);
  padding: 20px 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 20px;
}

.rail-mark {
  width: 36px;
  height: 36px;
}

/* Human Acceptance Correction: the raster crop (407x592, portrait) is not
   square like the inline SVG it replaced -- object-fit: contain keeps it
   undistorted inside this still-36x36 box, matching .boot-ident-mark's own
   object-fit rule for the same source crop at boot-ident's larger size. */
.rail-mark-img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.rail-item {
  font-size: 9.5px;
  letter-spacing: 0.14em;
  color: var(--text-faint);
  writing-mode: vertical-rl;
  text-orientation: mixed;
}

.rail-item.is-active {
  color: var(--teal);
  text-shadow: 0 0 10px rgba(41, 230, 200, 0.5);
}

.main {
  flex: 1;
  min-width: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

/* ---------- masthead ---------- */
.station-header {
  position: relative;
  overflow: hidden;
  padding: 16px 24px;
  border-bottom: 1px solid var(--grid-line);
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 16px;
  background: linear-gradient(180deg, #0c0c16, var(--bg));
}

.station-header::before {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(ellipse 520px 220px at 6% 0%, rgba(255, 176, 0, 0.09), transparent 65%),
    radial-gradient(ellipse 420px 220px at 26% 120%, rgba(255, 47, 158, 0.09), transparent 60%);
  pointer-events: none;
}

.station-ident {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.station-ident-text {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 1.5rem;
  letter-spacing: 0.03em;
  line-height: 1;
  margin: 0;
  background: linear-gradient(90deg, var(--amber), var(--magenta) 55%, var(--azure));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  filter: drop-shadow(0 0 12px rgba(255, 176, 0, 0.25));
}

.station-tagline {
  font-size: 0.65rem;
  letter-spacing: 0.22em;
  color: var(--teal);
}

.masthead-right {
  position: relative;
  display: flex;
  align-items: center;
  gap: 24px;
}

.signal {
  font-size: 9.5px;
  letter-spacing: 0.08em;
  color: var(--text-faint);
  text-transform: uppercase;
  text-align: right;
}

.clock {
  text-align: right;
}

.clock-time {
  font-size: 1.3rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.clock-date {
  font-size: 0.65rem;
  color: var(--text-dim);
  letter-spacing: 0.08em;
}

/* ============================================================
   GUIDE LAYOUT
   ============================================================ */
/* Human Acceptance Correction, Step 1 final delta: minmax(0, 1fr), not
   plain 1fr -- a grid item's automatic minimum width defaults to `auto`
   (effectively its own content's min-content size), which a bare `1fr`
   track does NOT override; under real-browser conditions this test
   environment's exact-pixel viewport emulation doesn't reproduce (a
   native vertical scrollbar reserving ~15-17px of horizontal space
   whenever page content is taller than the viewport, which it
   measurably is here -- document.documentElement.scrollHeight (1122px)
   > window.innerHeight (1080px) at the 1920x1080 acceptance size), a
   content-driven minimum in .grid-frame's own descendants could refuse
   to shrink by that same margin and force this whole track (and the
   page) wider than available space. minmax(0, 1fr) is the standard,
   correct fix: it tells the track "you may shrink to zero before your
   own content's minimum applies," matching .right-column's own row 1
   immediately below, which already uses this pattern. */
.guide-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 300px;
  gap: 16px;
  padding: 16px;
  flex: 1;
  min-height: 0;
}

/* Human Acceptance Correction, Step 1 TV retest: a CSS Grid with an
   EXPLICIT, fixed second row for FOCUS -- not flex-with-a-max-height (the
   previous attempt), which still let the panel's outer height vary with
   its own content (title length, hero presence/aspect, episode text).
   Grid row 2's fixed size (--focus-dock-height, matching #focus-panel's
   own height below) is the actual source of truth for the dock's
   geometry; the panel never grows or shrinks to fit what's inside it.
   Row 1 (.preview) takes the remaining space via minmax(0, 1fr) --
   whether or not .preview currently has content (it becomes display:none
   during guide-active mode, per body.guide-active .preview below), the
   FOCUS row's own size and position never move, which is the actual
   "permanent structural region" requirement: a fixed dashboard slot, not
   a card that happens to currently sit somewhere. */
.right-column {
  display: grid;
  grid-template-rows: minmax(0, 1fr) var(--focus-dock-height);
  gap: 16px;
  min-height: 0;
}

@media (max-width: 900px) {
  .guide-layout {
    grid-template-columns: 1fr;
  }
}

.grid-frame {
  background: var(--panel);
  border: 1px solid var(--grid-line);
  border-radius: 4px;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

.time-header {
  display: grid;
  grid-template-columns: 148px minmax(0, 1fr);
  border-bottom: 1px solid var(--grid-line);
  background: var(--panel-raised);
}

.time-header .corner {
  padding: 8px 12px;
  font-size: 9.5px;
  letter-spacing: 0.16em;
  color: var(--text-faint);
  text-transform: uppercase;
  display: flex;
  align-items: center;
}

.time-ticks {
  position: relative;
  display: flex;
  min-width: 0;
}

/* Human Acceptance Correction, Step 1 final delta: min-width: 0 -- a
   flex item's automatic minimum defaults to its own content's natural
   size (here, a clock-time label string like "10:36 AM"), which
   `flex: 1` alone does not override. At a fixed tick COUNT (driven by
   WINDOW_MS, not by viewport width) each tick's flex-allocated share
   shrinks as the viewport narrows, making this the one part of the
   guide most likely to hit its own text's minimum width first. Every
   other place in this file where similar per-item text could vary
   (.chan-meta, .focus-episode, .seg) already guards this the same way;
   this was the one remaining gap. */
.time-ticks .tick {
  flex: 1;
  min-width: 0;
  padding: 8px 0 8px 8px;
  font-size: 10.5px;
  color: var(--text-dim);
  border-left: 1px solid var(--grid-soft);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.now-line {
  position: absolute;
  top: 0;
  bottom: -2000px;
  width: 2px;
  background: var(--red);
  box-shadow: 0 0 10px rgba(255, 77, 77, 0.7);
  z-index: 3;
  pointer-events: none;
}

.now-line::before {
  content: "NOW";
  position: absolute;
  top: -1px;
  left: 4px;
  font-size: 9px;
  letter-spacing: 0.12em;
  color: var(--red);
  background: var(--panel-raised);
  padding: 0 3px;
  white-space: nowrap;
}

.rows {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
}

/* Human Acceptance Correction, Step 1 final delta: minmax(0, 1fr) for the
   same reason as .guide-layout above -- .track (this column's own grid
   item) has no overflow property of its own (defaults to visible), so
   without this its automatic minimum size would fall back to `auto`
   rather than being guaranteed to shrink. .track's actual children
   (.gridlines, .seg, .gap-badge) are all position:absolute and so
   contribute ~0 to that auto-minimum in practice, but stating minmax(0,
   1fr) explicitly here removes any dependency on that being true forever
   -- a future normal-flow addition to .track's markup should not be able
   to silently reintroduce page-level horizontal overflow. */
.row {
  display: grid;
  grid-template-columns: 148px minmax(0, 1fr);
  border-bottom: 1px solid var(--grid-soft);
  position: relative;
}

.row:last-child {
  border-bottom: none;
}

.chan-id {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 12px;
  border-right: 1px solid var(--grid-line);
  background: none;
  border-top: none;
  border-bottom: none;
  border-left: none;
  color: var(--text);
  font-family: inherit;
  cursor: pointer;
  text-align: left;
  width: 100%;
}

.chan-id:hover {
  background: rgba(255, 255, 255, 0.02);
}

.chan-num {
  font-family: var(--font-display);
  font-size: 1.15rem;
  width: 24px;
  flex: none;
  text-align: center;
}

.chan-meta {
  min-width: 0;
  display: flex;
  flex-direction: column;
}

.chan-name {
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: 0.02em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.chan-tagline {
  font-size: 9px;
  color: var(--text-dim);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.row[data-tuned="true"] .chan-id {
  background: linear-gradient(90deg, rgba(255, 176, 0, 0.14), transparent);
  box-shadow: inset 3px 0 0 var(--amber);
}

.chan-id.theme-flagship .chan-num {
  color: var(--magenta);
}

.chan-id.theme-calm .chan-num {
  color: var(--teal);
}

/* Tuned always wins over a channel's own theme color for the number --
   re-declared last, at matching specificity to the theme rules above, so
   cascade order (not accidental specificity) is what decides this on
   purpose. "TUNED = amber" is the one hierarchy signal that must never be
   ambiguous. */
.row[data-tuned="true"] .chan-id .chan-num {
  color: var(--amber);
  text-shadow: 0 0 10px rgba(255, 176, 0, 0.6);
}

.track {
  position: relative;
  min-height: 42px;
}

.track .gridlines {
  position: absolute;
  inset: 0;
  display: flex;
  pointer-events: none;
}

.track .gridlines i {
  flex: 1;
  border-left: 1px solid var(--grid-soft);
}

/* Human Acceptance Correction, Step 1 final delta: appearance: none --
   this is a real <button> element (app.js's renderGuideRows()), and
   without this it keeps Chrome's native platform button chrome
   (confirmed via direct computed-style inspection: appearance: auto),
   which imposes its own native minimum content box (measured: 18px)
   independent of and overriding the authored left/width percentages.
   For a segment whose computed width legitimately needs to be a few
   pixels (a very short entry positioned near the right edge of the
   visible window, per this same gate's left/width clamp fix above this
   element in the DOM), that native floor pushed the segment's right
   edge past its own track's right edge -- the actual final source of
   the measured #guide-rows overflow, on top of (not a replacement for)
   the min/max clamp fix. */
.seg {
  appearance: none;
  position: absolute;
  top: 5px;
  bottom: 5px;
  border-radius: 3px;
  background: var(--grid-soft);
  border: 1px solid var(--grid-line);
  color: var(--text-dim);
  font-family: inherit;
  display: flex;
  align-items: center;
  padding: 0 8px;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: 10.5px;
  cursor: pointer;
  text-align: left;
}

.seg:not(.on-now) {
  /* A looping channel repeats the same title many times across the
     visible window -- dimming the repeats (rather than hiding or
     relabeling them, which would misrepresent real schedule truth) keeps
     the eye on the one segment that's actually on now. */
  opacity: 0.62;
}

.seg.on-now {
  background: linear-gradient(180deg, rgba(255, 176, 0, 0.22), rgba(255, 176, 0, 0.08));
  border-color: var(--amber);
  color: var(--text);
  font-weight: 600;
  opacity: 1;
}

.seg.clip-start {
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
}

.seg.clip-end {
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
}

.row.is-guide-highlighted-row .track,
.is-guide-highlighted-row .track {
  box-shadow: inset 0 0 0 2px var(--teal);
}

.gap-badge {
  position: absolute;
  top: 5px;
  bottom: 5px;
  left: 8px;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 10px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-faint);
  border: 1px dashed var(--grid-line);
  border-radius: 3px;
  padding: 0 10px;
  background: repeating-linear-gradient(135deg, transparent 0 8px, rgba(255, 255, 255, 0.015) 8px 16px);
}

/* ---------- detail panel (existing #preview element IDs, restyled) ---------- */
.preview {
  background: var(--panel);
  border: 1px solid var(--grid-line);
  border-radius: 4px;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  justify-content: center;
  padding: 20px;
  /* Dashboard scrollbar correction: this box is already correctly HEIGHT-
     bounded by .right-column's own grid row (minmax(0, 1fr), itself
     inside .shell's fixed height: 100vh chain) -- confirmed via direct
     measurement: .preview's own clientHeight never changed across any
     channel, only its scrollHeight did, growing past it by 93-194px
     depending on program/episode/up-next text length. The real defect
     was downstream, in .preview-card's own children never having a
     sizing chain that could shrink to fit (see .preview-card/.preview-
     video below) -- overflow-y: auto was silently absorbing that
     mismatch into an internal scrollbar instead of surfacing it. Now
     that the content itself is properly bounded, this is a plain
     guardrail: content must never scroll here at all in normal operation.
     min-height: 0 lets THIS box itself shrink within its own grid row
     the same way every other track in this chain already does. */
  overflow: hidden;
  min-height: 0;
}

.preview:has(.preview-card:not([hidden])) {
  justify-content: flex-start;
}

.preview-empty {
  color: var(--text-faint);
  letter-spacing: 0.15em;
  font-size: 11px;
}

.preview-card {
  width: 100%;
  height: 100%;
  text-align: center;
  /* True Fullscreen TV Mode: the channel OSD (below) is positioned
     absolute relative to this card, in both dashboard and fullscreen --
     harmless here since the OSD is only ever unhidden while
     document.fullscreenElement is set (see app.js's showChannelOsd()). */
  position: relative;
  /* Dashboard scrollbar correction: this card's children used to be
     plain block-flow siblings with no shared sizing chain at all -- each
     one (channel number, status, video, mute button, program, episode,
     progress bar, timing, up next, debug) simply stacked at its own
     intrinsic height regardless of how much room .preview actually had,
     which is exactly why a longer program/episode/up-next string (a
     different channel, e.g. CH02) could push the total past the
     panel's real height. A flex column here is what lets .preview-video
     below become the one element that actually shrinks to absorb the
     difference -- see its own comment. min-height: 0 is the classic
     flex-item default-minimum override this whole chain depends on. */
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
}

.preview-channel-number {
  flex: none;
  font-family: var(--font-display);
  font-size: 3rem;
  line-height: 1;
  color: var(--amber);
  text-shadow: 0 0 12px rgba(255, 176, 0, 0.5);
}

.preview-card.theme-calm .preview-channel-number {
  color: var(--teal);
  text-shadow: 0 0 12px rgba(41, 230, 200, 0.5);
}

.preview-card.theme-flagship .preview-channel-number {
  color: var(--magenta);
  text-shadow: 0 0 12px rgba(255, 47, 158, 0.5);
}

.preview-status {
  flex: none;
  margin-top: 4px;
  letter-spacing: 0.2em;
  font-size: 0.8rem;
  color: var(--teal);
}

.preview-status.is-no-programming {
  color: var(--text-dim);
}

/* Dashboard scrollbar correction: this is now the ONE element in the
   card that actually shrinks -- flex: 1 1 auto + min-height: 0 (the
   classic flex-item default-minimum override) lets it absorb whatever
   height everything else (channel number, status, mute button, program/
   episode/progress/timing/up-next/debug, all flex: none below) doesn't
   need, rather than always claiming up to max-height regardless of
   available room. object-fit: contain added because height is no
   longer purely intrinsic/fixed -- without it, a real video frame could
   distort once this box's rendered height stops matching the source's
   own aspect ratio. min-height: 0 deliberately allows the thumbnail to
   shrink away to nothing before any of the text information around it
   is ever clipped -- confirmed via direct measurement that even with a
   40px floor, the text stack ALONE could still exceed the panel's
   available height at a real tested viewport size; the thumbnail is the
   correct thing to sacrifice first, never the program/episode/timing
   text. */
.preview-video {
  display: block;
  width: 100%;
  flex: 1 1 auto;
  /* Right-Rail Hierarchy Correction: a real floor, not 0 -- with
     .preview-program's own height now fixed regardless of title length
     (see below), every OTHER sibling's height is deterministic across
     every channel, which makes this element's flex-computed leftover
     space deterministic too, not something that could still collapse to
     a 7px sliver the way CH11's actual video did before this fix
     (measured: 7px at the old --focus-dock-height). At the common/
     reference sizes (1366x768, 1920x1080 and taller) this element sits
     comfortably above this floor (measured 109-240px); 40px is a
     deliberately lower last-resort minimum than an earlier 72px attempt,
     which itself still measurably overflowed .preview-card's fixed text
     stack at shorter real window heights (1366x700 and below, and even
     1280x720) -- the video is still the one thing this whole chain
     sacrifices first (never the program/episode/timing text), all the
     way down to this floor if a window is short enough to demand it. */
  min-height: 40px;
  max-height: 240px;
  object-fit: contain;
  margin-top: 12px;
  background: #000;
  border: 1px solid var(--grid-line);
  border-radius: 3px;
}

/* Pre-existing gap, surfaced by CH13 Weather v0: this project defines no
   blanket [hidden]{display:none} rule (see #focus-panel[hidden]'s own
   equivalent override below) -- without this, .preview-video's own
   `display: block` above (an author rule) outranks the browser's default
   [hidden] styling (user-agent origin, always loses to an author rule at
   equal specificity), so setting video.hidden = true left a visible black
   242px box in every state that ever hid it (off-air, media-error, and
   now weather), not only the new WEATHER case that made it obvious. */
.preview-video[hidden] {
  display: none;
}

/* Human Acceptance Correction: F/FULL_PROGRAM now requests real browser
   Fullscreen on this element (see app.js's CMD_FULL_PROGRAM handler) in
   addition to its pre-existing "close every interface layer" behavior --
   this is the same preview-card content, scaled up, not a new view.
   True Fullscreen TV Mode gate: "the program owns the television screen"
   -- fullscreen no longer reserves any layout space for dashboard
   metadata below/around the video (that used to be a fixed
   `calc(100% - 140px)` video max-height, cutting the picture down to fit
   text around it). The metadata itself is hidden outright, below, so the
   video/weather surface can use the true available screen while
   preserving aspect ratio (object-fit: contain -- never stretched,
   never cropped, letterbox/pillarbox as needed). */
/* POWER / AUDIO CORRECTION 1 -- powered_on is the top-level visibility gate
   for the broadcast presentation, independent of presentation kind: while the
   card is data-powered="off", every child surface (MEDIA video, WEATHER card,
   OFF_AIR/empty treatments, programme metadata, mute control) is hidden and
   the in-card standby surface is shown. In fullscreen this is the ONLY way a
   viewer can see "off": the page-level standby/boot overlays are outside the
   fullscreen subtree. The transient channel OSD is exempt (it no-ops while
   off anyway). Runtime/schedule state is untouched underneath. */
.preview-card[data-powered="off"] > :not(.preview-standby):not(.channel-osd) {
  visibility: hidden;
}
.preview-card[data-powered="off"] {
  background: #000;
}
.preview-standby {
  position: absolute;
  inset: 0;
  background: #000;
  z-index: 4;
}
.preview-card[data-powered="off"] .preview-standby {
  display: block;
}

.preview-card:fullscreen {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background: var(--bg-deep);
  padding: 0;
}

.preview-card:fullscreen .preview-video {
  flex: 1;
  width: 100%;
  height: 100%;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  margin-top: 0;
  border: none;
  border-radius: 0;
}

/* True Fullscreen TV Mode gate: every persistent dashboard/informational
   element hidden outright in true fullscreen -- not shrunk, not
   repositioned, removed from the rendered surface entirely. The right
   CURRENT CHANNEL dashboard column (outside #preview-card, untouched by
   this rule) keeps every one of these exactly as before; only the
   fullscreen presentation loses them, replaced by the transient
   channel-osd below. Listed explicitly (one selector per element) rather
   than a catch-all, so a future new persistent field added inside
   #preview-card is NOT silently hidden by default -- it must be added
   here deliberately, the same discipline this file already uses for
   MEDIA_ERROR_LABELS/ERROR_CARD_LABELS' own explicit enumeration. */
.preview-card:fullscreen .preview-channel-number,
.preview-card:fullscreen .preview-status,
.preview-card:fullscreen .preview-program,
.preview-card:fullscreen .preview-episode,
.preview-card:fullscreen .preview-progress,
.preview-card:fullscreen .preview-timing,
.preview-card:fullscreen .preview-upnext,
.preview-card:fullscreen .preview-debug,
.preview-card:fullscreen .preview-mute-toggle,
.preview-card:fullscreen .appliance-mute-toggle,
.preview-card:fullscreen .listen-here-toggle {
  display: none;
}

/* Transient channel-number OSD (True Fullscreen TV Mode gate): overlays
   the program surface without affecting its layout/geometry -- position:
   absolute takes it out of flow entirely, so it can never resize or
   reflow the video/weather beneath it. Only ever unhidden by app.js's
   showChannelOsd() while document.fullscreenElement is set; harmless,
   invisible, non-interactive markup the rest of the time. Upper-right,
   as a traditional CRT/tube television channel-number display -- small,
   unobtrusive, Rabbit Ears' own receiver typography (--font-display,
   the same face .preview-channel-number already uses). */
.channel-osd {
  position: absolute;
  top: 28px;
  right: 36px;
  font-family: var(--font-display);
  font-size: 2.75rem;
  line-height: 1;
  color: var(--amber);
  text-shadow: 0 0 14px rgba(255, 176, 0, 0.55), 0 2px 6px rgba(0, 0, 0, 0.85);
  letter-spacing: 0.04em;
  pointer-events: none;
  z-index: 5;
  opacity: 1;
  transition: opacity 400ms ease;
}

.channel-osd.is-fading {
  opacity: 0;
}

/* Matches .preview-channel-number's own per-theme color, immediately
   above -- same "Rabbit Ears receiver typography/style" the CRT channel
   number always used, just transient instead of persistent now. */
.preview-card.theme-calm .channel-osd {
  color: var(--teal);
  text-shadow: 0 0 14px rgba(41, 230, 200, 0.55), 0 2px 6px rgba(0, 0, 0, 0.85);
}

.preview-card.theme-flagship .channel-osd {
  color: var(--magenta);
  text-shadow: 0 0 14px rgba(255, 47, 158, 0.55), 0 2px 6px rgba(0, 0, 0, 0.85);
}

/* True Fullscreen TV Mode gate: cursor auto-hide. Scoped under :fullscreen
   too (belt-and-suspenders alongside app.js's own "cursor-idle is only
   ever added while fullscreen" guard, matching this project's established
   defensive-coding style elsewhere) -- the dashboard/guide cursor can
   never be hidden by this rule regardless of what class app.js might add
   to #preview-card outside true fullscreen. No layout impact: cursor is a
   paint-only property, never affects box geometry. */
.preview-card:fullscreen.cursor-idle {
  cursor: none;
}

.preview-mute-toggle {
  flex: none;
  margin-top: 6px;
  background: var(--panel-raised);
  border: 1px solid var(--grid-line);
  color: var(--text);
  font-family: inherit;
  letter-spacing: 0.1em;
  padding: 6px 14px;
  border-radius: 3px;
  cursor: pointer;
}

.preview-mute-toggle:hover {
  border-color: var(--amber);
}

/* Technical/media-delivery outcomes (Gate 1) -- deliberately distinct
   styling from a broadcast-state (off-air) card: amber/dashed rather than
   the off-air red, so it reads as "delivery problem", not "nothing airing". */
.preview-media-error {
  flex: none;
  margin-top: 12px;
  padding: 12px;
  border: 1px dashed var(--amber);
  border-radius: 3px;
  color: var(--amber);
  letter-spacing: 0.12em;
  font-size: 0.8rem;
}

/* Dashboard scrollbar correction: clamped/truncated, never allowed to
   grow the card past its own line budget -- a long program title (e.g.
   CH02's real content) used to simply wrap to as many lines as it took,
   one of the real, measured contributors (24-52px of variance across
   channels) to .preview overflowing into its own scrollbar. Same
   2-line-clamp pattern #focus-panel's own .focus-program already uses
   for the identical reason.

   Right-Rail Hierarchy Correction: min-height: 2.4em (2 lines at this
   font's own line-height) added -- line-clamp alone only caps the
   MAXIMUM at 2 lines, it does not stop a genuinely 1-line title from
   rendering at 1 line's height, which is exactly why CH03 (1 line, 20px
   measured) and CH11 (2 lines, 40px measured) gave .preview-video two
   different amounts of leftover flex space to absorb -- the literal
   cause of "different title lengths produce visibly different content
   pressure." Reserving the full 2-line height unconditionally makes
   every sibling's height identical regardless of channel, which is what
   makes .preview-video's own flex-computed leftover space (and so its
   rendered height) stable across channels too. */
.preview-program {
  flex: none;
  margin-top: 8px;
  min-height: 2.4em;
  font-size: 1.05rem;
  font-weight: 700;
  line-height: 1.2;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.preview-episode {
  flex: none;
  color: var(--teal);
  margin-top: 2px;
  font-size: 0.85rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.preview-progress {
  flex: none;
  margin-top: 8px;
  height: 6px;
  background: var(--grid-soft);
  border-radius: 3px;
  overflow: hidden;
}

.preview-progress-bar {
  height: 100%;
  background: linear-gradient(90deg, var(--amber), var(--magenta));
}

.preview-timing {
  flex: none;
  margin-top: 4px;
  font-size: 0.75rem;
  color: var(--text-dim);
}

.preview-upnext {
  flex: none;
  margin-top: 6px;
  padding-top: 6px;
  border-top: 1px solid var(--grid-line);
  font-size: 0.7rem;
  color: var(--text-faint);
  letter-spacing: 0.04em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.preview-debug {
  flex: none;
  margin-top: 6px;
  font-size: 0.65rem;
  color: var(--text-faint);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Human Acceptance Correction (CH13 Weather v0): lives in the same
   #preview-card as every other channel's video, so F/FULL_PROGRAM's
   existing Fullscreen wiring (.preview-card:fullscreen below) already
   scales this up with no special-casing of its own.

   Dashboard scrollbar correction: this is CH13's own equivalent of
   .preview-video -- the one flexible element that shrinks to absorb
   whatever height the fixed channel-number/status/debug lines around it
   don't need, rather than always claiming a fixed 40px of margin+padding
   regardless of available room (confirmed via direct measurement: CH13
   overflowed .preview-card by 38px with the OLD fixed flex: none/
   margin-top: 20px/padding: 20px 0 -- the exact same class of bug as
   .preview-video, just in a sibling sub-tree renderPreview() swaps in
   for entry_kind === "WEATHER"). min-height: 0 is the same flex-item
   default-minimum override used throughout this whole chain. */
.preview-weather {
  flex: 1 1 auto;
  min-height: 0;
  margin-top: 12px;
  padding: 8px 0;
  overflow: hidden;
  /* PRE-PUBLIC CORRECTION v0: the main card is a weather stage -- the shared
     sky artwork (.wx-sky, absolute) sits behind the readout. */
  position: relative;
  min-height: 150px;
  border-radius: 6px;
  background: linear-gradient(180deg, #0b1d2e 0%, #061019 70%, var(--bg-deep) 100%);
}
.preview-weather > .weather-temp,
.preview-weather > .weather-conditions,
.preview-weather > .weather-location,
.preview-weather > .weather-updated {
  position: relative;
  z-index: 1;
}
.preview-weather .wx-sun { top: 10%; right: 6%; width: 18%; }
.preview-weather .wx-moon { top: 9%; right: 8%; width: 14%; }
.preview-weather .wx-cloud { top: 18%; width: 34%; height: 15%; }
.preview-weather .wx-cloud-b { left: 52%; top: 34%; width: 26%; height: 12%; }
.preview-weather .weather-conditions { text-transform: uppercase; letter-spacing: 0.12em; }
.preview-weather .weather-location { text-transform: uppercase; letter-spacing: 0.14em; }
.preview-card:fullscreen .preview-weather .wx-sun { width: 14%; top: 12%; right: 10%; }
.preview-card:fullscreen .preview-weather .wx-moon { width: 11%; top: 11%; right: 12%; }

.weather-temp {
  font-family: var(--font-display);
  font-size: 3.2rem;
  line-height: 1;
  color: var(--teal);
  text-shadow: 0 0 14px rgba(41, 230, 200, 0.45);
}

.weather-conditions {
  margin-top: 8px;
  font-size: 1rem;
  letter-spacing: 0.05em;
}

.weather-location {
  margin-top: 6px;
  font-size: 0.8rem;
  color: var(--text-dim);
}

.weather-updated {
  margin-top: 10px;
  font-size: 0.7rem;
  letter-spacing: 0.04em;
  color: var(--text-faint);
}

.preview-card:fullscreen .preview-weather {
  flex: 1;
  width: 100%; /* PRE-PUBLIC CORRECTION v0: the stage fills the screen so the artwork reads from TV distance */
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

.preview-card:fullscreen .weather-temp {
  font-size: 8rem;
}

.preview-card:fullscreen .weather-conditions {
  font-size: 1.6rem;
}

/* Right-Rail Hierarchy Correction: short-viewport compaction.
   "Resize browser vertically shorter... dashboard should reflow or
   proportionally compress... never turn into normal document
   scrolling" -- live measurement below 1366x700 found .preview-card's
   fixed text stack, even with its video already at its own 40px floor,
   still didn't fit (16-111px of real, would-be-silently-clipped
   overflow, confirmed via direct scrollHeight/clientHeight diffing at
   1366x650/600/550). Every rule here only tightens spacing/hides the
   single least-essential line (raw asset/entry diagnostics -- never
   program/episode/progress/timing, the actual "what's on and how much
   is left" information) -- never a fallback to overflow/scroll. Covers
   real window heights down to 1366x650 with zero measured overflow;
   documented as a known remaining gap below that (see field-test
   corrections notes), since a browser viewport shorter than ~600px is
   not a realistic TV/kiosk field-test condition. */
@media (max-height: 700px) {
  .preview-debug {
    display: none;
  }
  .preview-channel-number {
    font-size: 2.2rem;
  }
  .preview-status {
    margin-top: 2px;
  }
  .preview-mute-toggle {
    margin-top: 4px;
  }
  .preview-program {
    margin-top: 6px;
  }
  .preview-episode {
    margin-top: 1px;
  }
  .preview-progress {
    margin-top: 6px;
  }
  .preview-timing {
    margin-top: 2px;
  }
  .preview-upnext {
    margin-top: 3px;
    padding-top: 3px;
  }
}

/* ============================================================
   Broadcast Presentation Profiles, Gate 3 -- ambient/focus pilot.

   AMBIENT (currently TUNED program's presentation): scoped to exactly
   three elements, .preview, .station-header, and .grid-frame -- never
   .rows/.track/.seg (individual channel rows/segments untouched), so
   guide grid STRUCTURE/geometry stays exactly as it was. Applied via a
   data-attribute + one CSS custom property on :root (see
   applyAmbientPresentation() in app.js), and ONLY when the resolved
   property's source !== "DEFAULT" -- an unconfigured channel renders
   pixel-identical to before this gate.

   Gate 3A, section 16: .grid-frame's own wash was added because Gate 3
   found ambient effectively disappeared while guide-active (.preview,
   the other ambient-scoped element, is hidden by the pre-existing
   body.guide-active .preview{display:none} rule) -- .grid-frame is NOT
   hidden in that mode, so this is what keeps "the room" feeling ambient-
   tinted while browsing, without touching per-row .seg/.track elements
   (which would compete with focus-panel's own, stronger treatment) or
   changing grid-template-columns/row layout at all.

   FOCUS (currently HIGHLIGHTED/hovered guide entry's presentation):
   #focus-panel is a normal-flow sibling of .preview inside .right-column
   (Human Acceptance Correction, Step 1A) -- deliberately NOT nested
   inside .preview itself, since body.guide-active .preview{display:none}
   still hides the whole preview aside while browsing the grid, which is
   exactly when keyboard-driven focus needs to be visible. .right-column
   stays present in both guide-active and normal mode (see that rule,
   below the .guide-layout definition) specifically so FOCUS always has a
   real column to render into -- never a position:fixed/absolute overlay.

   Both background_treatment and detail_layout_variant only ever reach
   the DOM through a fixed lookup table (BACKGROUND_TREATMENT_CLASS /
   LAYOUT_VARIANT_CLASS in app.js) -- an unrecognized value can never
   become a class name or attribute value, so profile data can only ever
   select among these pre-authored treatments, never inject arbitrary
   CSS/HTML. accent_primary is separately re-validated client-side
   against the same '#RRGGBB' pattern the server enforces before ever
   reaching a style property.
   ============================================================ */

:root[data-ambient-background="WARM_PAINTERLY"] .preview {
  background: linear-gradient(165deg, rgba(217, 122, 66, 0.12), var(--panel) 55%);
  border-color: rgba(217, 122, 66, 0.35);
}
:root[data-ambient-background="WARM_PAINTERLY"] .station-header {
  background: linear-gradient(90deg, rgba(217, 122, 66, 0.08), transparent 70%);
}
:root[data-ambient-background="WARM_PAINTERLY"] .grid-frame {
  background: linear-gradient(165deg, rgba(217, 122, 66, 0.05), var(--panel) 40%);
  border-color: rgba(217, 122, 66, 0.22);
}

:root[data-ambient-background="COLD_INDUSTRIAL"] .preview {
  background: linear-gradient(165deg, rgba(111, 168, 192, 0.10), var(--panel) 55%);
  border-color: rgba(111, 168, 192, 0.35);
}
:root[data-ambient-background="COLD_INDUSTRIAL"] .station-header {
  background: linear-gradient(90deg, rgba(111, 168, 192, 0.08), transparent 70%);
}
:root[data-ambient-background="COLD_INDUSTRIAL"] .grid-frame {
  background: linear-gradient(165deg, rgba(111, 168, 192, 0.05), var(--panel) 40%);
  border-color: rgba(111, 168, 192, 0.22);
}

.preview-card.has-presentation-accent .preview-channel-number {
  color: var(--presentation-accent);
  text-shadow: 0 0 12px var(--presentation-accent);
}

/* Human Acceptance Correction, Step 1 TV retest: a normal-flow block
   inside .right-column's fixed second grid row -- no position:fixed/
   absolute, no right/bottom placement, no z-index/box-shadow overlay
   treatment. height (not max-height) matches --focus-dock-height exactly
   -- this panel's outer box is a FIXED dashboard slot, never a card that
   resizes with its own content. overflow: hidden is the hard backstop:
   every child below is individually clamped (fixed hero height,
   line-clamped title, single-line-ellipsis episode) so genuine overflow
   should be rare, but if content still doesn't fit, it clips silently
   here rather than ever growing this box. */
#focus-panel {
  width: 100%;
  height: var(--focus-dock-height);
  overflow: hidden;
  background: var(--panel-raised);
  border: 1px solid var(--grid-line);
  border-radius: 4px;
  padding: 12px;
  display: flex;
  flex-direction: column;
}

/* Focus Inspector Layout Correction v1: a compact horizontal header row
   -- FOCUS's own outer height budget was previously spent on the label
   and channel number sitting on SEPARATE lines (see the "before" mockup
   in that gate's own brief), which is exactly the vertical space this
   correction reclaims for the preview/info body below. */
.focus-header {
  flex: none;
  display: flex;
  flex-direction: row;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 6px;
}

.focus-panel-label {
  letter-spacing: 0.2em;
  font-size: 0.65rem;
  color: var(--text-faint);
}

.focus-channel-number {
  font-family: var(--font-display);
  font-size: 1.3rem;
  line-height: 1;
  color: var(--teal);
}

/* The media-inspector body: a fixed-height row (fills whatever the
   header above didn't use) split into a left PREVIEW column and a right
   INFO column -- see .focus-preview/.focus-info below. min-height: 0 is
   the same flex-item default-minimum override used throughout this
   file's other flex chains (.preview-card, .main, etc.). */
.focus-body {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: row;
  align-items: stretch;
  gap: 10px;
}

/* Focus Inspector Layout Correction v1: the LEFT column of .focus-body.
   Previously each of hero-art/hero-fallback/preview-video carried its
   OWN fixed height (32px) and stacked vertically above the title text --
   the exact "very wide shallow black media box, tiny visible image"
   defect this gate exists to fix. Now this wrapper alone owns the
   region's geometry: `aspect-ratio: 4 / 3` together with `height: 100%`
   (stretched by .focus-body's `align-items: stretch`) makes its WIDTH
   follow from the available body height, rather than an arbitrary
   percentage of the panel's width -- 4:3 is Rabbit Ears' own dominant
   real-content ratio (Bob Ross, Magnum, Superfriends, Airwolf, American
   Woodshop; see that gate's own brief), so this is the shape that
   actually fits most of the library, not 16:9 forced onto everything.
   overflow: hidden is a guardrail only -- every child below is already
   bounded to this exact box via position:absolute;inset:0. */
.focus-preview {
  flex: none;
  position: relative;
  height: 100%;
  aspect-ratio: 4 / 3;
  background: var(--bg-deep);
  border: 1px solid var(--grid-line);
  border-radius: 3px;
  overflow: hidden;
}

/* Human Acceptance Correction, Step 1 TV follow-up: object-fit: contain,
   not cover -- every hero_art asset today is a real single-frame video
   still (presentation.extract_art.extract_hero_art(), one ffmpeg
   -frames:v 1 grab with its own aspect-preserving -vf scale), never a
   custom-designed decorative crop. `cover` was cropping actual subject
   matter out of frame (confirmed on TV: Bob Ross). `contain` letterboxes/
   pillarboxes against the same --bg-deep background every other empty/
   inactive surface in this UI already uses, so the bars read as
   intentional, not broken. This still applies unchanged here -- only the
   BOX this fills is bigger and shaped by .focus-preview now, never by
   this element's own dimensions (a 16:9 source still contains correctly,
   letterboxed top/bottom inside the 4:3 frame; see that gate's own "16:9
   / other aspect ratios" requirement). If a future presentation profile
   ever adds an explicit "this hero art is pre-cropped decorative
   artwork, cover is correct" signal, that would be a targeted
   `.focus-hero-art[data-fit="cover"]`-style override, not a change to
   this default -- no such signal exists yet. */
.focus-hero-art {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* Gate 3A, section 19: most programs will never have custom hero_art, so
   this is a first-class, intentional design state -- not a debug/
   "missing" appearance. A quiet diagonal hairline texture evokes a
   broadcast test card/color-bar convention, fitting this product's own
   retro-CRT identity rather than looking like a gap. No text label --
   focus-channel-number/focus-program already carry identity; an empty
   textured card reads as "this channel has no picture feed yet", which
   is true and fine, not as "something failed to load". Occupies the
   exact same .focus-preview region as focus-hero-art/focus-preview-video
   -- whichever of the three is shown, this region's geometry never
   changes (Focus Inspector Layout Correction v1, "fallback / no preview
   state" requirement). */
.focus-hero-fallback {
  position: absolute;
  inset: 0;
  background:
    repeating-linear-gradient(
      135deg,
      rgba(255, 255, 255, 0.035) 0px,
      rgba(255, 255, 255, 0.035) 1px,
      transparent 1px,
      transparent 10px
    ),
    var(--bg-deep);
}

/* The RIGHT column of .focus-body -- title/episode/action-hint. min-
   width: 0 is the flex-item default-minimum override (horizontal
   analogue of the min-height: 0 used elsewhere in this file) so long
   text can actually ellipsis/clamp instead of forcing this column wider
   than .focus-body has room for. justify-content: space-between keeps
   the title block pinned to the top and .focus-action-hint pinned to
   the bottom regardless of how many lines the title/episode actually use
   -- a short title never leaves the hint floating in the middle. */
.focus-info {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}

/* Clamped, not grown: a long program title takes at most 2 lines and
   ellipsizes past that -- one of the two content-driven-height sources
   the original "dimensions change with title length" defect named.
   Right-Rail Hierarchy Correction's font-size trim (0.95rem -> 0.8rem)
   is kept; #focus-panel's own fixed height + overflow: hidden remain the
   hard backstop either way. */
.focus-program {
  flex: none;
  font-size: 0.8rem;
  font-weight: 700;
  line-height: 1.15;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.focus-episode {
  flex: none;
  color: var(--teal);
  margin-top: 3px;
  font-size: 0.7rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Focus Inspector Layout Correction v1: a static, non-interactive
   message only -- no pin/record behavior exists yet (see index.html's
   own comment on #focus-action-hint). Reserves the exact region a future
   PINNED FOCUS pass would swap for real action controls (e.g. RECORD /
   LIBRARY), so that pass only needs to change this one element's
   content, never FOCUS's outer geometry, preview size, or this info
   column's layout. */
.focus-action-hint {
  flex: none;
  margin-top: 4px;
  font-size: 0.6rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  color: var(--text-faint);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* detail_layout_variant treatments -- the focus panel's own card shape,
   never the guide grid's. SOFT_CARD: rounder, warmer border. HARD_
   GEOMETRIC: square corners + an angular clipped edge, a cheap but
   real "hard-edged" signature distinct from every other rounded panel
   in this UI. */
#focus-panel.presentation-soft-card {
  border-radius: 12px;
  border-color: rgba(217, 122, 66, 0.4);
}
#focus-panel.presentation-soft-card .focus-preview {
  border-radius: 8px;
}

#focus-panel.presentation-hard-geometric {
  border-radius: 0;
  border-color: rgba(111, 168, 192, 0.5);
  clip-path: polygon(0 0, 100% 0, 100% 90%, 90% 100%, 0 100%);
}
#focus-panel.presentation-hard-geometric .focus-preview {
  border-radius: 0;
}

/* Guide Motion Preview v0: identical box to .focus-hero-art/
   .focus-hero-fallback immediately above -- FOCUS's own outer geometry
   (--focus-dock-height) is never touched; motion preview substitutes
   INTO the exact same .focus-preview region, never adding or resizing
   anything. object-fit: contain (never cover) for the same reason
   .focus-hero-art already uses it -- these are real video frames, never
   a pre-cropped decorative asset. */
.focus-preview-video {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  background: var(--bg-deep);
}

/* ---------- bottom ticker ---------- */
.ticker {
  border-top: 1px solid var(--grid-line);
  background: var(--bg-deep);
  padding: 10px 24px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: 10.5px;
  letter-spacing: 0.05em;
  color: var(--text-dim);
  gap: 20px;
  flex-wrap: wrap;
}

.ticker-live {
  color: var(--teal);
}

.station-footer {
  padding: 8px 24px 20px;
  color: var(--text-faint);
  font-size: 0.65rem;
  letter-spacing: 0.1em;
}

/* Laptop/HDMI field-test fix: GUIDE must be a visibly distinct TV mode,
   not merely an internal navigation cursor. Retro Guide Live Integration
   gate: adapted to the timeline layout -- the grid-frame (not a flat
   .guide-grid tile wall) takes over the full width while browsing, and
   the row-level highlight (.is-guide-highlighted-row) is what carries the
   cyan focus frame now. Presentation only; the GUIDE/SELECT/BACK state
   machine in app.js is unchanged. */
/* Human Acceptance Correction, Step 1A: the grid no longer collapses to a
   single column while guide-active -- .right-column (and #focus-panel
   inside it) must stay a real, in-flow column in both modes, since FOCUS
   needs somewhere to render without resorting to position:fixed again.
   .preview itself still becomes display:none below, exactly as before;
   only the grid-frame's own "full width while browsing" cosmetic (an
   earlier, now-superseded preference) is given up in service of this
   gate's explicit "FOCUS must never float" requirement. */
body.guide-active .preview {
  display: none;
}

body.guide-active .grid-frame::after {
  content: "CHANNEL GUIDE   UP/DOWN BROWSE   ENTER TUNE   ESC BACK";
  order: -1;
}

body.guide-active .time-header .corner::before {
  content: "GUIDE";
}

/* ============================================================
   BOOT / WARMUP -- Analog Boot / Warmup UX pass.
   A composable, CSS-driven power/warmup sequence -- boot.js only ever
   sets #boot-overlay's data-boot-state attribute (plus starts/stops the
   static-noise canvas loop); every visual transition below reacts to
   that attribute alone. Same design tokens as the rest of this file
   (--amber/--magenta/--azure/--teal, --bg/--bg-deep) -- no new palette.
   Sits above everything (z-index 200, above .power-standby-overlay's
   100) since it is a distinct, browser-local "is the set on at all"
   layer -- see boot.js's own docstring for why that stays separate from
   the server-authoritative POWER command's standby overlay.
   ============================================================ */

.boot-overlay {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: #000;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  opacity: 1;
  pointer-events: auto;
  transition: opacity 420ms ease, visibility 0ms linear 420ms;
}

.boot-overlay[data-boot-state="active"] {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}

.boot-overlay[data-boot-state="off"] .boot-phosphor,
.boot-overlay[data-boot-state="off"] .boot-scanline,
.boot-overlay[data-boot-state="off"] .boot-signal-field,
.boot-overlay[data-boot-state="off"] .boot-ident,
.boot-overlay[data-boot-state="off"] .boot-noise {
  opacity: 0;
}

/* -- phosphor dot: "tube gets power" -- a small radial glow at center. */
.boot-phosphor {
  position: absolute;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: radial-gradient(circle, #fff 0%, var(--amber) 35%, transparent 72%);
  opacity: 0;
  transform: scale(0.4);
  transition: opacity 300ms ease, transform 300ms ease;
  filter: drop-shadow(0 0 14px rgba(255, 176, 0, 0.6));
}

.boot-overlay[data-boot-state="warmup"] .boot-phosphor,
.boot-overlay[data-boot-state="acquire-signal"] .boot-phosphor {
  opacity: 1;
  transform: scale(1);
}

.boot-overlay[data-boot-state="powering-off"] .boot-phosphor {
  opacity: 1;
  transform: scale(1);
  animation: boot-phosphor-collapse 700ms ease-in forwards;
}

@keyframes boot-phosphor-collapse {
  0% { opacity: 1; transform: scale(1); }
  60% { opacity: 1; transform: scale(1); }
  85% { opacity: 1; transform: scale(0.6); }
  100% { opacity: 0; transform: scale(0); }
}

/* -- scanline: "thin horizontal white light -> expands", and (reversed
   on power-off) "image compresses vertically -> thin horizontal line". */
.boot-scanline {
  position: absolute;
  left: 50%;
  top: 50%;
  height: 2px;
  width: 0;
  transform: translate(-50%, -50%);
  background: linear-gradient(90deg, transparent, var(--amber), #fff 50%, var(--magenta), transparent);
  opacity: 0;
  box-shadow: 0 0 18px rgba(255, 255, 255, 0.5);
}

.boot-overlay[data-boot-state="warmup"] .boot-scanline {
  animation: boot-scanline-expand 900ms ease-out forwards;
  animation-delay: 250ms;
}

.boot-overlay[data-boot-state="acquire-signal"] .boot-scanline {
  opacity: 1;
  width: 100%;
  animation: boot-scanline-jitter 220ms ease-in-out infinite;
}

.boot-overlay[data-boot-state="powering-off"] .boot-scanline {
  opacity: 1;
  width: 100%;
  animation: boot-scanline-collapse 700ms ease-in forwards;
}

@keyframes boot-scanline-expand {
  0% { opacity: 0; width: 0; }
  20% { opacity: 1; width: 0; }
  100% { opacity: 1; width: 100%; }
}

@keyframes boot-scanline-jitter {
  0%, 100% { transform: translate(-50%, -50%); }
  40% { transform: translate(-50%, calc(-50% - 3px)); }
  70% { transform: translate(-50%, calc(-50% + 2px)); }
}

@keyframes boot-scanline-collapse {
  0% { opacity: 1; width: 100%; }
  55% { opacity: 1; width: 100%; }
  85% { opacity: 1; width: 8%; }
  100% { opacity: 0; width: 0; }
}

/* -- signal field: "signal begins locking, color elements emerge" --
   Production Ident Asset Seam pass: an authored-graphics <img> (see
   index.html's own boot-art/signal-field.svg reference) rather than
   hand-coded CSS bars -- boot.js/this stylesheet only ever fade/scale
   the whole image as a unit, never reach inside it, so swapping the
   file's content for final artwork needs no CSS change here. */
.boot-signal-field {
  position: absolute;
  left: 18%;
  /* CSS replaced-element quirk: with left+right both set but width left
     at its 'auto' initial value, a replaced element (an <img>, here) is
     sized to its OWN intrinsic aspect ratio rather than stretched to
     fill the gap between them -- 'right' would then be silently ignored.
     Setting width explicitly (rather than right) is what makes it
     actually span the intended left:18%..right:18% region. */
  width: 64%;
  top: 50%;
  transform: translateY(-50%) scaleX(0.4);
  transform-origin: center;
  opacity: 0;
  pointer-events: none;
  transition: none;
}

.boot-overlay[data-boot-state="acquire-signal"] .boot-signal-field {
  opacity: 0.85;
  animation: boot-signal-field-in 480ms ease-out forwards;
}

.boot-overlay[data-boot-state="ident"] .boot-signal-field {
  opacity: 0.35; /* recede once the ident mark takes visual focus */
  transform: translateY(-50%) scaleX(1);
  animation: none;
  transition: opacity 500ms ease;
}

.boot-overlay[data-boot-state="powering-off"] .boot-signal-field {
  opacity: 0;
  transform: translateY(-50%) scaleX(0.4);
  transition: transform 300ms ease-in, opacity 300ms ease-in;
}

@keyframes boot-signal-field-in {
  0% { transform: translateY(-50%) scaleX(0.4); }
  100% { transform: translateY(-50%) scaleX(1); }
}

/* -- static noise: canvas painted by boot.js's own drawNoiseFrame(), only
   while ACQUIRE_SIGNAL -- CSS only ever toggles its opacity/visibility,
   never drives the noise itself. image-rendering:pixelated keeps the
   tiny backing buffer cheap while still reading as analog static at
   full screen size. */
.boot-noise {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  mix-blend-mode: screen;
  image-rendering: pixelated;
  transition: opacity 200ms ease;
}

.boot-overlay[data-boot-state="acquire-signal"] .boot-noise {
  opacity: 0.5;
}

/* -- ident: rabbit-silhouette.svg + rabbit-ears-wordmark.svg +
   emagin8-mark.svg (Production Ident Asset Seam pass -- all three
   authored-graphics <img> references, see index.html), resolving in
   during IDENT and held briefly before the whole overlay fades. The
   entrance animation (opacity/scale on the wrapping elements) is
   deliberately applied OUTSIDE each image, never depending on its
   internal content, so it works identically whether these files are
   still placeholder or final production art. */
.boot-ident {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  opacity: 0;
  transform: scale(0.82);
  /* opacity crosses fast against .boot-authored-layer's own fade above
     (section 5); the scale entrance stays its own slower, separate beat --
     a snappier opacity swap doesn't need a snappier scale to feel right. */
  transition: opacity var(--crossfade-ms) ease, transform 500ms ease;
}

.boot-overlay[data-boot-state="ident"] .boot-ident,
.boot-overlay[data-boot-state="active"] .boot-ident {
  opacity: 1;
  transform: scale(1);
}

.boot-ident-mark {
  width: 72px;
  height: 72px;
  /* PASS 4 STAGE B: the raster icon crop (boot-ident-icon-raster.png) is
     portrait (~407x592), not square like the old SVG silhouette it
     replaced -- object-fit: contain is what keeps it undistorted inside
     this still-72x72 box, not a layout/timing change. */
  object-fit: contain;
  filter: drop-shadow(0 0 18px rgba(255, 176, 0, 0.35));
  transition: transform 420ms ease;
}

/* Motion Pass 3, section 7: full-screen ident -> dashboard-corner handoff.
   boot.js's runIdentToActiveHandoff() sets these three custom properties
   from the boot mark's and the rail-mark's real getBoundingClientRect()
   just before the overlay itself starts its own opacity fade (both
   trigger in the same tick, so they run concurrently) -- the mark visibly
   closes the gap to its exact corner position/size while everything else
   here fades away, landing on top of the rail-mark that was already there
   the whole time underneath the opaque overlay. */
.boot-overlay.is-handing-off .boot-ident-mark {
  transform: translate(var(--handoff-dx, 0px), var(--handoff-dy, 0px)) scale(var(--handoff-scale, 1));
}

.boot-overlay.is-handing-off .boot-ident-wordmark,
.boot-overlay.is-handing-off .boot-signal-field {
  transition: opacity 300ms ease;
  opacity: 0;
}

.boot-ident-wordmark {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}

.boot-ident-title {
  width: 260px;
  height: auto;
  filter: drop-shadow(0 0 16px rgba(255, 176, 0, 0.25));
}

/* Motion Pass 4A, Change 1: EMAGIN8 resolves as its own later, separate
   beat rather than arriving with the rest of .boot-ident at once. Base
   state (opacity 0, translateY(2px)) applies whenever data-boot-state is
   anything OTHER than ident/active -- i.e. every WARMUP/ACQUIRE_SIGNAL
   and every post-power-off OFF -- so this genuinely re-triggers on every
   boot cycle purely from the state-attribute change, no JS involved.
   --emagin8-reveal-delay/-duration live in :root (see DESIGN TOKENS)
   so this stays as tunable as --crossfade-ms already is. */
.boot-ident-tagline {
  width: 130px;
  height: auto;
  opacity: 0;
  transform: translateY(2px);
  filter: drop-shadow(0 0 0 rgba(41, 230, 200, 0));
  transition:
    opacity var(--emagin8-reveal-duration) ease,
    transform var(--emagin8-reveal-duration) ease,
    filter var(--emagin8-reveal-duration) ease;
  transition-delay: var(--emagin8-reveal-delay);
}

.boot-overlay[data-boot-state="ident"] .boot-ident-tagline,
.boot-overlay[data-boot-state="active"] .boot-ident-tagline {
  opacity: 0.85;
  transform: translateY(0);
  /* restrained phosphor bloom, reusing --teal -- no new color introduced */
  filter: drop-shadow(0 0 6px rgba(41, 230, 200, 0.35));
}

/* Motion Pass 4A, Change 2: one restrained, non-looping signal sweep
   during the IDENT hold -- boot.js's triggerLivingSignalShimmer() adds
   .is-active once, part-way through the existing 1400ms IDENT window
   (never a new state, never a lengthened hold). animation-iteration-count
   is deliberately the shorthand's implicit 1 -- never "infinite". */
.boot-ident-shimmer {
  position: absolute;
  inset: -10% -20%;
  pointer-events: none;
  background: linear-gradient(100deg, transparent 40%, rgba(255, 255, 255, 0.35) 50%, transparent 60%);
  opacity: 0;
  transform: translateX(-60%);
}

.boot-ident-shimmer.is-active {
  animation: boot-ident-shimmer-sweep 420ms ease-in-out 1;
}

@keyframes boot-ident-shimmer-sweep {
  0% { opacity: 0; transform: translateX(-60%); }
  15% { opacity: 0.9; }
  85% { opacity: 0.5; }
  100% { opacity: 0; transform: translateX(60%); }
}

/* -- optional authored hero layer (Hybrid / Hyper Frames Readiness pass)
   -- see boot.js's own hasAuthoredBootLayer()/AUTHORED_LAYER_ENABLED.
   Hidden and inert by default; boot.js only ever reveals it once the
   asset has actually loaded and can play, and it always sits ABOVE the
   procedural layers below it so it can fully replace them without a
   markup change when enabled. */
.boot-authored-layer {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  pointer-events: none;
  /* Fast on purpose -- this is the "hide the seam" half of the
     authored->SVG crossfade (section 5); .boot-ident's own entrance
     below fades in on the same --crossfade-ms so the two cross rather
     than leaving a visible gap of bare signal-field/black between them. */
  transition: opacity var(--crossfade-ms) ease;
}

.boot-overlay[data-boot-state="acquire-signal"] .boot-authored-layer.is-authored-active {
  opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
  /* boot.js already skips straight to ACTIVE/OFF for reduced-motion --
     this is a pure-CSS backstop in case JS runs before this stylesheet
     or for any state the script still passes through momentarily.
     .boot-ident-tagline is listed explicitly (Motion Pass 4A): it has
     its OWN transition/transition-delay separate from its .boot-ident
     parent, which this block does not otherwise reach into -- without
     this, reduced-motion users would still see EMAGIN8's settle-in delay
     even though the state jumps straight past IDENT to ACTIVE. The
     living-signal shimmer needs no entry here: boot.js never calls
     triggerLivingSignalShimmer() on the reduced-motion path at all (see
     that file's own early-return), so .is-active is simply never added. */
  .boot-overlay,
  .boot-phosphor,
  .boot-scanline,
  .boot-signal-field,
  .boot-ident,
  .boot-ident-tagline {
    animation: none !important;
    transition-duration: 1ms !important;
    transition-delay: 0ms !important;
  }
}

/* ============================================================
   ACCESS SCREEN -- TRUSTED-TEAM REMOTE ACCESS v0.
   A single small centered card; deliberately reuses the shell's own
   design tokens (:root above) rather than introducing a second visual
   language for what is still, functionally, the front door of the same
   CRT broadcast console -- see access.html.
   ============================================================ */
.access-body {
  min-height: 100vh;
  margin: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg);
}

.access-card {
  position: relative;
  z-index: 1;
  width: min(360px, calc(100vw - 48px));
  padding: 32px 28px;
  background: var(--panel);
  border: 1px solid var(--grid-line);
  border-radius: 4px;
  box-shadow: 0 0 0 1px var(--bg-deep), 0 24px 60px rgba(0, 0, 0, 0.5);
  text-align: center;
}

.access-mark {
  font-family: var(--font-display);
  font-size: 22px;
  letter-spacing: 0.12em;
  color: var(--amber);
}

.access-heading {
  margin-top: 8px;
  margin-bottom: 24px;
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.16em;
  color: var(--text-dim);
}

.access-label {
  display: block;
  margin-bottom: 8px;
  font-size: 11px;
  letter-spacing: 0.08em;
  color: var(--text-faint);
  text-align: left;
}

.access-input {
  width: 100%;
  padding: 10px 12px;
  margin-bottom: 16px;
  background: var(--bg-deep);
  border: 1px solid var(--grid-line);
  border-radius: 3px;
  color: var(--text);
  font-family: var(--font-mono);
  font-size: 14px;
}

.access-input:focus {
  outline: none;
  border-color: var(--teal);
}

.access-submit {
  width: 100%;
  padding: 10px 12px;
  background: var(--amber);
  border: none;
  border-radius: 3px;
  color: var(--bg-deep);
  font-family: var(--font-mono);
  font-weight: 600;
  font-size: 13px;
  letter-spacing: 0.1em;
  cursor: pointer;
}

.access-submit:disabled {
  opacity: 0.6;
  cursor: default;
}

.access-error {
  margin-top: 14px;
  font-size: 12px;
  letter-spacing: 0.04em;
  color: var(--red);
}

/* LAN DEVICE ACCEPTANCE v0: the honest "secure session login unavailable
   on this origin" notice on the access screen (see access.html). Same
   type scale as .access-error, amber rather than red: it is a property
   of the origin, not a denied credential. */
.access-notice {
  margin: 0 0 18px;
  padding: 12px 14px;
  border: 1px solid var(--amber, #d9a520);
  font-size: 12px;
  line-height: 1.5;
  letter-spacing: 0.03em;
  color: var(--amber, #d9a520);
  text-align: left;
}

.access-notice-title {
  margin-bottom: 6px;
  font-weight: 600;
  letter-spacing: 0.08em;
}


/* =====================================================================
   FIRST IMPRESSION POLISH v0
   ===================================================================== */

/* -- FOCUS presentation slot: WEATHER STATION card ------------------------
   Same box as the motion preview (.focus-preview, 4:3), silent, data-driven
   (Channel 13's authoritative weather), Rabbit Ears colours. The sky layer
   is a lightweight CSS-only condition treatment chosen by data-condition
   (clear / cloudy / rain / snow / storm / fog); it is decorative only and
   stops under prefers-reduced-motion. */
.focus-weather {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding: 8px 10px 8px;
  background: linear-gradient(180deg, #0b1d2e 0%, #061019 70%, var(--bg-deep) 100%);
  overflow: hidden;
}
.focus-weather-strip {
  position: absolute;
  top: 6px;
  left: 10px;
  font-size: 0.58rem;
  letter-spacing: 0.22em;
  color: var(--teal);
  opacity: 0.85;
}
.focus-weather-temp {
  font-family: var(--font-display);
  font-size: 2rem;
  line-height: 1;
  color: var(--teal);
  text-shadow: 0 0 12px rgba(41, 230, 200, 0.45);
}
.focus-weather-cond {
  margin-top: 3px;
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.focus-weather-loc {
  margin-top: 2px;
  font-size: 0.6rem;
  letter-spacing: 0.04em;
  color: var(--text-dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.wx-sky { position: absolute; inset: 0; pointer-events: none; }
.wx-sky i { position: absolute; display: block; }
/* PRE-PUBLIC CORRECTION v0: SHARED weather artwork. Any element carrying
   .wx-stage[data-condition] (the WEATHER FOCUS card and the main Channel 13
   card) gets the same offline, CSS-native sky: stars / sun / moon / clouds /
   precipitation / flash, revealed per condition class. Future condition
   motion attaches to these same classes; everything stops under
   prefers-reduced-motion. Day sky vs night sky: the stage background
   follows the class, so a night observation never reads as a sunny
   afternoon. */
.wx-stage[data-condition="clear-night"],
.wx-stage[data-condition="partly-night"] {
  background: linear-gradient(180deg, #060b16 0%, #0a1524 60%, var(--bg-deep) 100%);
}
.wx-sun {
  top: 14%; right: 12%; width: 26%; aspect-ratio: 1; border-radius: 50%;
  background: radial-gradient(circle, rgba(255, 196, 61, 0.95) 0 38%, rgba(255, 176, 0, 0.25) 42%, transparent 70%);
  animation: wx-sun-breathe 6s ease-in-out infinite;
}
.wx-moon {
  top: 12%; right: 14%; width: 20%; aspect-ratio: 1; border-radius: 50%;
  background: radial-gradient(circle at 62% 40%, transparent 0 46%, rgba(232, 238, 246, 0.92) 48%);
  box-shadow: 0 0 14px rgba(200, 220, 255, 0.35);
}
/* Hidden by default at the same specificity as the class rules below that
   reveal it (`.wx-sky i` would otherwise win with display:block). */
.wx-sky .wx-moon {
  display: none;
}
.wx-stars {
  inset: 0;
  background-image:
    radial-gradient(circle, rgba(255, 255, 255, 0.85) 0 0.7px, transparent 1.2px),
    radial-gradient(circle, rgba(255, 255, 255, 0.6) 0 0.6px, transparent 1.1px);
  background-size: 37px 29px, 53px 41px;
  background-position: 3px 5px, 19px 11px;
  opacity: 0; /* stars exist only in the night classes below (which also animate them) */
}
.wx-cloud {
  top: 26%; width: 44%; height: 18%; border-radius: 999px;
  background: rgba(214, 226, 236, 0.55);
  filter: blur(0.5px);
  animation: wx-cloud-drift 18s linear infinite;
}
.wx-cloud-a { left: -10%; }
.wx-cloud-b { left: 40%; top: 40%; width: 34%; height: 14%; opacity: 0.7; animation-duration: 24s; }
.wx-precip {
  inset: 0;
  background-image: repeating-linear-gradient(115deg, transparent 0 14px, rgba(160, 205, 255, 0.55) 14px 15px, transparent 15px 24px);
  background-size: 48px 48px;
  opacity: 0;
  animation: wx-precip-fall 0.9s linear infinite;
}
.wx-flash {
  inset: 0;
  background: rgba(235, 240, 255, 0.55);
  opacity: 0;
}
/* -- class rules ------------------------------------------------------- */
.wx-stage[data-condition="clear-day"] .wx-cloud,
.wx-stage[data-condition="clear-day"] .wx-moon { display: none; }
.wx-stage[data-condition="clear-night"] .wx-sun,
.wx-stage[data-condition="clear-night"] .wx-cloud { display: none; }
.wx-stage[data-condition="clear-night"] .wx-moon { display: block; }
.wx-stage[data-condition="clear-night"] .wx-stars,
.wx-stage[data-condition="partly-night"] .wx-stars { opacity: 0.8; animation: wx-stars-twinkle 5s ease-in-out infinite; }
.wx-stage[data-condition="partly-day"] .wx-sun { opacity: 0.85; }
.wx-stage[data-condition="partly-day"] .wx-cloud-b { display: none; }
.wx-stage[data-condition="partly-night"] .wx-sun { display: none; }
.wx-stage[data-condition="partly-night"] .wx-moon { display: block; }
.wx-stage[data-condition="partly-night"] .wx-cloud { background: rgba(150, 165, 185, 0.5); }
.wx-stage[data-condition="partly-night"] .wx-cloud-b { display: none; }
.wx-stage[data-condition="cloudy"] .wx-sun { opacity: 0.25; }
.wx-stage[data-condition="cloudy"] .wx-cloud { background: rgba(190, 200, 212, 0.6); }
.wx-stage[data-condition="rain"] .wx-precip,
.wx-stage[data-condition="storm"] .wx-precip { opacity: 0.6; }
.wx-stage[data-condition="rain"] .wx-sun,
.wx-stage[data-condition="storm"] .wx-sun,
.wx-stage[data-condition="snow"] .wx-sun,
.wx-stage[data-condition="fog"] .wx-sun { display: none; }
.wx-stage[data-condition="rain"] .wx-cloud { background: rgba(150, 165, 185, 0.7); }
.wx-stage[data-condition="snow"] .wx-precip {
  opacity: 0.7;
  background-image: radial-gradient(circle, rgba(240, 248, 255, 0.85) 0 1.5px, transparent 2px);
  background-size: 22px 22px;
  animation-duration: 3s;
}
.wx-stage[data-condition="storm"] .wx-cloud { background: rgba(110, 124, 140, 0.85); }
.wx-stage[data-condition="storm"] .wx-flash { animation: wx-flash 9s linear infinite; }
.wx-stage[data-condition="fog"] .wx-cloud { top: 45%; width: 90%; height: 26%; opacity: 0.35; filter: blur(4px); }
@keyframes wx-sun-breathe { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.06); } }
@keyframes wx-cloud-drift { from { transform: translateX(-12%); } to { transform: translateX(22%); } }
@keyframes wx-precip-fall { from { background-position: 0 0; } to { background-position: 0 48px; } }
@keyframes wx-stars-twinkle { 0%, 100% { opacity: 0.55; } 50% { opacity: 0.9; } }
@keyframes wx-flash { 0%, 91%, 100% { opacity: 0; } 92% { opacity: 0.8; } 93% { opacity: 0; } 95% { opacity: 0.5; } 96% { opacity: 0; } }
/* Unavailable observation: the honest label sits on a neutral cloudy sky --
   no sun or moon is ever drawn for a reading we do not have. */
.wx-stage[data-available="false"] .wx-sun,
.wx-stage[data-available="false"] .wx-moon { display: none; }
@media (prefers-reduced-motion: reduce) {
  /* specificity above every per-class animation rule (e.g. the storm flash) */
  .wx-stage[data-condition] .wx-sky i { animation: none; }
  .wx-stage[data-condition="clear-night"] .wx-stars,
  .wx-stage[data-condition="partly-night"] .wx-stars { opacity: 0.7; }
  .wx-stage[data-condition="storm"] .wx-flash { opacity: 0; }
}

/* -- FOCUS fallback surfaces say what they are ---------------------------
   A hatched box alone read as "missing". The label is set by app.js:
   NO PREVIEW (media without a cached derivative) / OFF AIR (non-media). */
.focus-hero-fallback::after {
  content: attr(data-label);
  position: absolute;
  left: 0; right: 0; bottom: 8px;
  text-align: center;
  font-size: 0.6rem;
  letter-spacing: 0.2em;
  color: var(--text-faint);
}

/* -- Guide cells: real ellipsis, and one readable label over a run of
   narrow repeats ------------------------------------------------------------
   .seg was a flex container, and text-overflow only applies to block-level
   inline content, so long labels were hard-clipped at the cell edge. Block
   + line-height centring gives a real "…". A run of consecutive cells too
   narrow to carry text (CH01: 113 short-form entries, CH11) keeps every real
   cell (schedule truth, hover, click) and gains ONE overlaid label across
   the run, pointer-events none, so the row reads as a programme instead of
   a strip of empty boxes. */
.seg {
  display: block;
  line-height: 31px;
}
.seg-run-label {
  position: absolute;
  top: 5px;
  bottom: 5px;
  padding: 0 8px;
  line-height: 31px;
  font-size: 10.5px;
  color: var(--text-dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  pointer-events: none;
  z-index: 2;
}
.seg-run-label .seg-run-count {
  margin-left: 6px;
  color: var(--text-faint);
  letter-spacing: 0.04em;
}

/* -- No engineering identifiers on the viewer's dashboard -----------------
   asset_id / entry_id / media_ref belong in logs and the API, not under
   UP NEXT. Kept in the DOM for tooling; never painted. */
.preview-debug {
  display: none;
}

/* -- ACCESS REQUIRED: the team's first screen wears the Rabbit Ears mark ------ */
.access-logo {
  display: block;
  width: 44px;
  height: 44px;
  margin: 0 auto 10px;
  opacity: 0.95;
}
.access-tagline {
  text-align: center;
  font-size: 0.62rem;
  letter-spacing: 0.28em;
  color: var(--teal);
  margin-top: -2px;
  margin-bottom: 14px;
}


/* -- APPLIANCE MUTE v0 ---------------------------------------------------
   The television's mute, under the ON-AIR window. Reads the authoritative
   state (MUTE / UNMUTE, aria-pressed), keyboard focusable, never an
   engineering control. */
.appliance-mute-toggle {
  flex: none;
  align-self: flex-start;
  margin-top: 10px;
  background: transparent;
  border: 1px solid var(--grid-line);
  color: var(--text-dim);
  font-family: inherit;
  font-size: 0.7rem;
  letter-spacing: 0.18em;
  padding: 6px 14px;
  border-radius: 3px;
  cursor: pointer;
  transition: border-color 120ms ease, color 120ms ease;
}
.appliance-mute-toggle::before {
  content: "";
  display: inline-block;
  width: 9px;
  height: 9px;
  margin-right: 8px;
  border-radius: 50%;
  background: var(--teal);
  box-shadow: 0 0 8px rgba(41, 230, 200, 0.6);
  vertical-align: -1px;
}
.appliance-mute-toggle[aria-pressed="true"] {
  border-color: var(--amber);
  color: var(--amber);
}
.appliance-mute-toggle[aria-pressed="true"]::before {
  background: var(--amber);
  box-shadow: 0 0 8px rgba(255, 176, 0, 0.6);
}
.appliance-mute-toggle:hover,
.appliance-mute-toggle:focus-visible {
  border-color: var(--amber);
  color: var(--text);
  outline: none;
}
.mute-osd {
  display: none;
}
.preview-card:fullscreen .mute-osd:not([hidden]) {
  display: block;
  position: absolute;
  right: 36px;
  bottom: 28px;
  padding: 6px 14px;
  border: 1px solid var(--amber);
  border-radius: 3px;
  font-family: var(--font-display);
  font-size: 1rem;
  letter-spacing: 0.2em;
  color: var(--amber);
  text-shadow: 0 0 10px rgba(255, 176, 0, 0.5);
  background: rgba(0, 0, 0, 0.45);
  pointer-events: none;
  z-index: 5;
}


/* -- MOBILE CORRECTION v0 ---------------------------------------------------
   Touch input mode (<html data-input-mode="touch">, set by app.js from
   `(hover: none)`). Nothing below applies to a hover-capable pointer: the
   accepted desktop dashboard is byte-for-byte the same there. */
.touch-controls {
  display: none;
}
:root[data-input-mode="touch"] .touch-controls:not([hidden]) {
  display: flex;
  flex-direction: column;
  gap: 8px;
  /* bottom of the left rail (76px column, empty below GUIDE in both
     orientations): never over the wordmark, clock, guide or FOCUS */
  position: fixed;
  left: 8px;
  bottom: 14px;
  width: 60px;
  /* above .boot-overlay (200) and .power-standby-overlay (100): POWER must
     stay reachable while the set is OFF */
  z-index: 210;
}
.touch-power-toggle,
.touch-controls .appliance-mute-toggle {
  flex: none;
  align-self: auto;
  margin-top: 0;
  width: 60px;
  min-height: 44px;
  padding: 6px 2px;
  background: rgba(6, 8, 14, 0.85);
  border: 1px solid var(--grid-line);
  border-radius: 3px;
  color: var(--text-dim);
  font-family: inherit;
  font-size: 0.55rem;
  letter-spacing: 0.12em;
  text-align: center;
  cursor: pointer;
}
.touch-power-toggle::before,
.touch-controls .appliance-mute-toggle::before {
  content: "";
  display: block;
  width: 9px;
  height: 9px;
  margin: 0 auto 5px;
  border-radius: 50%;
  background: var(--text-faint);
  box-shadow: none;
}
.touch-power-toggle[aria-pressed="true"]::before,
.touch-controls .appliance-mute-toggle[aria-pressed="false"]::before {
  background: var(--teal);
  box-shadow: 0 0 8px rgba(41, 230, 200, 0.6);
}
.touch-controls .appliance-mute-toggle[aria-pressed="true"]::before {
  background: var(--amber);
  box-shadow: 0 0 8px rgba(255, 176, 0, 0.6);
}
.touch-power-toggle[aria-pressed="true"] {
  color: var(--text);
}
.touch-controls .appliance-mute-toggle:disabled {
  opacity: 0.35;
  cursor: default;
}
.focus-watch {
  display: none;
}
:root[data-input-mode="touch"] .focus-watch {
  display: inline-block;
  flex: none;
  align-self: flex-start;
  margin-top: 6px;
  min-height: 36px;
  padding: 6px 18px;
  background: transparent;
  border: 1px solid var(--amber);
  border-radius: 3px;
  color: var(--amber);
  font-family: inherit;
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.2em;
  cursor: pointer;
}
:root[data-input-mode="touch"] .focus-action-hint {
  display: none;
}

/* -- PUBLIC TEAM PREVIEW v0: LISTEN HERE ------------------------------------
   This browser's own listening opt-in, beside the appliance MUTE under the
   on-air window. Off = dim (muted default); on = teal "LISTENING". */
.listen-here-toggle {
  flex: none;
  align-self: flex-start;
  margin-top: 8px;
  background: transparent;
  border: 1px solid var(--grid-line);
  color: var(--text-dim);
  font-family: inherit;
  font-size: 0.7rem;
  letter-spacing: 0.18em;
  padding: 6px 14px;
  border-radius: 3px;
  cursor: pointer;
  transition: border-color 120ms ease, color 120ms ease;
}
.listen-here-toggle::before {
  content: "";
  display: inline-block;
  width: 9px;
  height: 9px;
  margin-right: 8px;
  border-radius: 50%;
  background: var(--text-faint);
  vertical-align: -1px;
}
.listen-here-toggle[aria-pressed="true"] {
  border-color: var(--teal);
  color: var(--teal);
}
.listen-here-toggle[aria-pressed="true"]::before {
  background: var(--teal);
  box-shadow: 0 0 8px rgba(41, 230, 200, 0.6);
}
.listen-here-toggle:hover,
.listen-here-toggle:focus-visible {
  border-color: var(--teal);
  color: var(--text);
  outline: none;
}
.touch-controls .listen-here-toggle {
  align-self: auto;
  margin-top: 0;
  width: 60px;
  min-height: 44px;
  padding: 6px 2px;
  background: rgba(6, 8, 14, 0.85);
  font-size: 0.5rem;
  letter-spacing: 0.1em;
  text-align: center;
}
.touch-controls .listen-here-toggle::before {
  display: block;
  margin: 0 auto 5px;
}
