/* =====================================================================
   SRT-Endpoint — front end v8 "Rack"
   ---------------------------------------------------------------------
   Design system for a broadcast operations console.

   COLOR    Warm-biased neutrals (hue pulled toward the Net-Com orange so
            the greys read as chosen, not inherited). The brand orange is
            reserved for IDENTITY and FOCUS only -- it is never a status.
            Status has its own vocabulary: on-air / standby / fault / info.
   TYPE     System sans for prose and controls; a condensed face for rack
            labels and eyebrows; monospace for every technical value
            (addresses, ports, PIDs, bitrates) with tabular figures.
   LAYOUT   A status ribbon summarising fleet state, above dense tabular
            detail. State is encoded in form (stripe, glyph, pill) as well
            as colour, so a fault survives a colourblind viewer and a
            glance from across the room.

   Theming is token-level: components only ever read var(--token). The
   palette is redefined for light mode, and the [data-theme] attribute
   (set by the header toggle) overrides the OS preference in both
   directions.
   ===================================================================== */

/* =====================================================================
   PALETTE — NetComTV brand system (design pack v1)
   Grounds are cool NAVY (toward the pack --ink #14192a), not the old warm
   brown-black. The single brand accent is AMBER (--amber #f5821e, deepened
   to --brand for links/fills, lightened to --accent-text on navy). Status
   keeps its OWN vocabulary (green on-air / gold standby / red fault) so a
   fault survives a colourblind viewer and a glance across the room -- the
   brand amber is reserved for identity and focus, never for status.
   --band / --band-ink / --band-label drive the pack's navy stat-band idiom
   and the navy table-header rows.
   ===================================================================== */

/* ---------- palette: dark (default) — navy console ---------- */
:root {
  --canvas:      #0A0E18;
  --panel:       #131A2B;
  --panel-2:     #1B2338;
  --panel-3:     #253048;
  --rule:        #2A3450;
  --rule-soft:   #202942;
  --ink:         #EDF0F7;
  --ink-dim:     #98A2B8;
  --ink-faint:   #818BA4;   /* was #667089 = 3.51:1 on --panel, below AA */

  --amber:       #F5821E;   /* THE accent: rules, bar fills (same both themes) */
  --brand:       #F5821E;   /* identity + focus ONLY, never status */
  --brand-ink:   #17130B;   /* text on an amber-filled surface */
  --brand-soft:  rgba(245,130,30,.15);
  --brand-edge:  rgba(245,130,30,.46);
  --accent-text: #FFB066;   /* amber-on-dark: eyebrows / accents over navy */

  --band:        #17223B;   /* navy stat band + table-header ground */
  --band-ink:    #FFFFFF;   /* headline numbers on the band */
  --band-label:  #FFB066;   /* uppercase amber labels on the band */
  --th-ink:      #AEB9D0;   /* table-header label text on navy */

  --on-air:      #43C888;
  --standby:     #E2B23A;
  --fault:       #F0575C;
  /* --alert is an alias of --fault. Nine rules referenced var(--alert,#A6382B)
     while the token itself was never defined anywhere, so every fault
     indicator silently rendered at the fallback: 2.66:1 on --panel, i.e.
     below the 3:1 non-text minimum. Defining it here takes them to 5.14:1. */
  --alert:       #F0575C;
  /* Secondary chart series. Tokenised per theme because the previous
     hardcoded pair dropped to ~3.5:1 on the light ground. */
  --series-b:    #7FB3DC;   /* ingest line */
  --series-c:    #B394D6;   /* push line */
  --info:        #5FA8E6;
  --on-air-bg:   rgba(67,200,136,.14);
  --standby-bg:  rgba(226,178,58,.15);
  --fault-bg:    rgba(240,87,92,.15);
  --info-bg:     rgba(95,168,230,.15);

  --media:       #090D16;   /* video letterbox */
  --shadow:      0 1px 0 rgba(255,255,255,.03), 0 10px 28px -20px #000;
  --focus:       0 0 0 2px var(--canvas), 0 0 0 4px var(--brand);
}

/* ---------- palette: light — the pack's white/navy document look ---------- */
@media (prefers-color-scheme: light) {
  :root {
    --canvas:     #EEF1F6;   /* pack --page-bg */
    --panel:      #FFFFFF;
    --panel-2:    #F7F9FC;   /* pack --soft */
    --panel-3:    #EEF1F6;   /* pack --track */
    --rule:       #E8ECF3;   /* pack --line */
    --rule-soft:  #EEF1F6;
    --ink:        #14192A;   /* pack --ink */
    --ink-dim:    #44506A;   /* pack --body */
    --ink-faint:  #646E85;   /* pack --muted, darkened: #8A93A6 was 3.09:1 */

    --amber:      #F5821E;
    --brand:      #E2660F;   /* pack --amber-deep: links/eyebrows/fills on white */
    --brand-ink:  #FFFFFF;
    --brand-soft: rgba(245,130,30,.12);
    --brand-edge: rgba(226,102,15,.40);
    --accent-text:#E2660F;

    --band:       #14192A;   /* pack navy stat band, the one bold block */
    --band-ink:   #FFFFFF;
    --band-label: #FFB066;
    --th-ink:     #C9D2E0;   /* pack --navy-ink */

    --on-air:     #1B7A4B;
    --standby:    #8A6410;
    --fault:      #B12F35;
    --alert:      #B12F35;
    --series-b:   #3D7DA8;   /* darkened for white ground */
    --series-c:   #6B4E96;
    --info:       #2F6DA6;
    --on-air-bg:  rgba(27,122,75,.10);
    --standby-bg: rgba(138,100,16,.12);
    --fault-bg:   rgba(177,47,53,.10);
    --info-bg:    rgba(47,109,166,.10);

    --media:      #D6DCE6;
    --shadow:     0 1px 0 rgba(255,255,255,.6), 0 10px 24px -20px #9aa6bd;
  }
}

/* ---------- explicit overrides (header toggle) ---------- */
:root[data-theme="dark"] {
  --canvas:#0A0E18; --panel:#131A2B; --panel-2:#1B2338; --panel-3:#253048;
  --rule:#2A3450; --rule-soft:#202942;
  --ink:#EDF0F7; --ink-dim:#98A2B8; --ink-faint:#818BA4;
  --amber:#F5821E; --brand:#F5821E; --brand-ink:#17130B;
  --brand-soft:rgba(245,130,30,.15); --brand-edge:rgba(245,130,30,.46); --accent-text:#FFB066;
  --band:#17223B; --band-ink:#FFFFFF; --band-label:#FFB066; --th-ink:#AEB9D0;
  --on-air:#43C888; --standby:#E2B23A; --fault:#F0575C; --alert:#F0575C; --info:#5FA8E6;
  --series-b:#7FB3DC; --series-c:#B394D6;
  --on-air-bg:rgba(67,200,136,.14); --standby-bg:rgba(226,178,58,.15);
  --fault-bg:rgba(240,87,92,.15); --info-bg:rgba(95,168,230,.15);
  --media:#090D16;
  --shadow:0 1px 0 rgba(255,255,255,.03), 0 10px 28px -20px #000;
}
:root[data-theme="light"] {
  --canvas:#EEF1F6; --panel:#FFFFFF; --panel-2:#F7F9FC; --panel-3:#EEF1F6;
  --rule:#E8ECF3; --rule-soft:#EEF1F6;
  --ink:#14192A; --ink-dim:#44506A; --ink-faint:#646E85;
  --amber:#F5821E; --brand:#E2660F; --brand-ink:#FFFFFF;
  --brand-soft:rgba(245,130,30,.12); --brand-edge:rgba(226,102,15,.40); --accent-text:#E2660F;
  --band:#14192A; --band-ink:#FFFFFF; --band-label:#FFB066; --th-ink:#C9D2E0;
  --on-air:#1B7A4B; --standby:#8A6410; --fault:#B12F35; --alert:#B12F35; --info:#2F6DA6;
  --series-b:#3D7DA8; --series-c:#6B4E96;
  --on-air-bg:rgba(27,122,75,.10); --standby-bg:rgba(138,100,16,.12);
  --fault-bg:rgba(177,47,53,.10); --info-bg:rgba(47,109,166,.10);
  --media:#D6DCE6;
  --shadow:0 1px 0 rgba(255,255,255,.6), 0 10px 24px -20px #9aa6bd;
}

/* ---------- legacy aliases -------------------------------------------
   Older rules and any stray inline styles referenced these names. Kept
   as aliases so nothing silently loses its colour. Prefer the tokens
   above in new work. ------------------------------------------------ */
:root {
  --bg: var(--canvas);
  --bg-elev: var(--panel-2);
  --card-bg: var(--panel);
  --card-bg-2: var(--panel-2);
  --panel2: var(--panel-2);
  --border: var(--rule);
  --border-soft: var(--rule-soft);
  --line: var(--rule);
  --text: var(--ink);
  --text-strong: var(--ink);
  --muted: var(--ink-dim);
  --muted-soft: var(--ink-faint);
  --accent: var(--brand);
  --accent-hover: var(--brand);
  --accent-soft: var(--brand-soft);
  --primary: var(--brand);
  --primary-d: var(--brand);
  --ok: var(--on-air);
  --good: var(--on-air);
  --err: var(--fault);
  --bad: var(--fault);
  --warn: var(--standby);
  --accent-2: var(--standby);
}

/* ---------- type ---------- */
:root {
  --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
          "Helvetica Neue", Arial, sans-serif;
  --cond: "Roboto Condensed", "Arial Narrow", "Helvetica Neue Condensed",
          "Liberation Sans Narrow", var(--sans);
  --mono: ui-monospace, "SF Mono", SFMono-Regular, "Cascadia Mono", Menlo,
          Consolas, "Liberation Mono", monospace;
}

/* ===================================================================
   RESET / SHELL
   =================================================================== */
* { box-sizing: border-box; }

html { background: var(--canvas); }

body {
  margin: 0;
  padding: 0;
  background: var(--canvas);
  color: var(--ink);
  min-height: 100vh;
  font: 14px/1.5 var(--sans);
  -webkit-font-smoothing: antialiased;
}

main {
  max-width: 1640px;
  margin: 20px auto 48px;
  padding: 0 22px;
}

a { color: var(--brand); }

:where(a, button, input, select, summary, [tabindex]):focus-visible {
  outline: none;
  box-shadow: var(--focus);
  border-radius: 5px;
}

::selection { background: var(--brand-soft); color: var(--ink); }

/* Utility used to keep wide tables from scrolling the whole page. */
/* Deliberately NOT a scroll container at desktop widths. Setting overflow-x
   here would compute overflow-y to auto, making this element the scrollport
   for any position:sticky descendant -- which silently breaks the sticky
   table header, since this wrapper has no height limit and so never scrolls
   vertically. Horizontal scrolling is switched on only when the viewport is
   genuinely too narrow (see the media query below), and the sticky header is
   turned off at the same breakpoint. */
/* Purely a horizontal-scroll affordance for narrow viewports. It carries no
   border, radius or background of its own -- the table owns its appearance --
   so that this element can stay a non-scroll-container at desktop width and
   leave the sticky header resolving against the viewport. */
.table-wrap { max-width: 100%; }

/* ===================================================================
   HEADER  (.topnav)
   A single rack-style bar: identity, wayfinding, live telemetry,
   identity chip. Sticky so telemetry stays visible while scrolling.
   =================================================================== */
.topnav {
  position: sticky;
  top: 0;
  z-index: 40;
  display: flex;
  align-items: center;
  gap: 14px;
  min-height: 52px;
  padding: 0 18px;
  background: color-mix(in srgb, var(--panel) 88%, transparent);
  backdrop-filter: saturate(1.4) blur(8px);
  border-bottom: 1px solid var(--rule);
  /* Wraps rather than scrolls. This used to be overflow-x:auto with a 3px
     scrollbar, which put a permanent scrollbar across the top of every page
     on any width where the nav did not quite fit -- visible chrome for no
     benefit. Wrapping keeps every link reachable without it. */
  flex-wrap: wrap;
  row-gap: 0;
}
@supports not (backdrop-filter: blur(2px)) {
  .topnav { background: var(--panel); }
}
/* (topnav scrollbar rules removed: it wraps now, it does not scroll) */
.topnav > * { flex-shrink: 0; }

/* --- identity lockup ---------------------------------------------- */
.brand {
  display: flex;
  align-items: center;
  gap: 9px;
  height: 30px;
  padding-right: 15px;
  border-right: 1px solid var(--rule);
  text-decoration: none;
  color: inherit;
}
/* Two brand logos ship: the WHITE lockup for the dark (navy) header, the
   COLOR lockup for the light header. Show whichever matches the active theme
   -- OS preference by default, overridden by [data-theme] from the toggle.
   Per the brand rule the colour logo must never sit on a dark bar. */
/* Base logo rules set SIZE ONLY. The .logo-dark/.logo-light swap classes own
   `display` so exactly one shows -- do NOT set display here or the higher
   specificity (.brand .logo) would force both logos visible. */
.brand .logo { height: 23px; width: auto; }
/* Generic theme-swap: .logo-dark shows on dark (navy) grounds, .logo-light
   on light. Used by both the header lockup and the login card. */
.logo-light { display: none; }
.logo-dark  { display: block; }
@media (prefers-color-scheme: light) {
  .logo-dark  { display: none; }
  .logo-light { display: block; }
}
:root[data-theme="dark"]  .logo-light { display: none; }
:root[data-theme="dark"]  .logo-dark  { display: block; }
:root[data-theme="light"] .logo-dark  { display: none; }
:root[data-theme="light"] .logo-light { display: block; }
.brand .brand-mark {
  display: grid;
  place-items: center;
  width: 24px; height: 24px;
  border-radius: 5px;
  background: var(--brand);
  color: var(--brand-ink);
  font: 900 13px/1 var(--cond);
  letter-spacing: -.02em;
}
.brand .brand-suffix {
  font: 700 15px/1 var(--cond);
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--ink);
}
.brand .brand-suffix b { color: var(--brand); font-weight: 700; }

/* --- wayfinding --------------------------------------------------- */
.nav-links { display: flex; gap: 1px; align-items: center; }
.topnav a {
  color: var(--ink-dim);
  text-decoration: none;
  padding: 7px 11px;
  border-radius: 6px;
  font-size: 13px;
  line-height: 1;
  transition: background .12s, color .12s;
}
.topnav a:hover { background: var(--panel-2); color: var(--ink); }
.topnav a.active {
  color: var(--ink);
  background: var(--panel-2);
  font-weight: 600;
  box-shadow: inset 0 -2px 0 var(--brand);
}
.topnav .spacer { flex: 1 1 0; min-width: 8px; }

/* --- live telemetry ----------------------------------------------- */
.nav-bw { display: flex; gap: 10px; align-items: center; }
.pill-group {
  display: flex;
  gap: 0;
  align-items: stretch;
  border: 1px solid var(--rule);
  border-radius: 7px;
  background: var(--panel-2);
  overflow: hidden;
}
.nav-bw-pair {
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  padding: 5px 10px;
  border-right: 1px solid var(--rule);
  font-size: 12px;
  line-height: 1.25;
}
.nav-bw-pair:last-child { border-right: none; }
.nav-bw-lbl {
  font: 600 10px/1 var(--cond);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
/* Telemetry values are DATA -> ink + mono, not brand. Brand is identity. */
.nav-bw-val {
  font-family: var(--mono);
  font-weight: 600;
  font-size: 12px;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.nav-bw-sub { color: var(--ink-faint); font-size: 11px; font-family: var(--mono); }
/* Fleet health: the LIVE counter earns colour when something is off-air. */
.nav-bw-val.is-degraded { color: var(--standby); }
.nav-bw-val.is-fault    { color: var(--fault); }

/* --- theme toggle ------------------------------------------------- */
.theme-toggle {
  display: grid;
  place-items: center;
  width: 30px; height: 30px;
  padding: 0;
  border: 1px solid var(--rule);
  border-radius: 7px;
  background: var(--panel-2);
  color: var(--ink-dim);
  cursor: pointer;
  font-size: 14px;
  line-height: 1;
}
.theme-toggle:hover { color: var(--ink); background: var(--panel-3); }

/* --- identity chip ------------------------------------------------ */
.user-chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 4px 11px;
  border: 1px solid var(--rule);
  border-radius: 999px;
  background: var(--panel-2);
  line-height: 1.2;
}
.user-chip .user-name { font-size: 12.5px; font-weight: 600; color: var(--ink); }
.user-chip .user-role {
  font: 600 9.5px/1 var(--cond);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--brand);
  padding: 3px 6px;
  border-radius: 4px;
  background: var(--brand-soft);
}
.topnav .logout { color: var(--ink-faint); font-size: 12.5px; }
.topnav .logout:hover { background: var(--fault-bg); color: var(--fault); }

/* --- impersonation banner ----------------------------------------- */
.impersonate-bar {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  padding: 8px 16px;
  background: var(--standby-bg);
  border-bottom: 1px solid var(--standby);
  color: var(--ink);
  font-size: 13px;
}
.impersonate-bar strong { color: var(--ink); }

/* ===================================================================
   PAGE FURNITURE
   =================================================================== */
.page-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 18px;
  flex-wrap: wrap;
  margin: 0 0 16px;
}
/* The eyebrow (small uppercase amber label above the H1) is the pack's
   signature section-opener device -- amber accent, wide tracking. */
.page-head .eyebrow {
  font: 700 10.5px/1 var(--cond);
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--accent-text);
  margin-bottom: 6px;
}

h1 {
  margin: 0;
  font-size: 21px;
  font-weight: 650;
  letter-spacing: -.01em;
  text-wrap: balance;
}
h2 {
  margin: 30px 0 12px;
  font: 600 11.5px/1 var(--cond);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
h3 {
  margin: 0 0 8px;
  font: 600 11px/1 var(--cond);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-dim);
}

.muted { color: var(--ink-dim); }
.mono {
  font-family: var(--mono);
  font-size: 12.5px;
  font-variant-numeric: tabular-nums;
}

.hint {
  margin: 0 0 12px;
  padding: 10px 13px;
  background: var(--panel-2);
  border: 1px solid var(--rule);
  border-left: 3px solid var(--brand);
  border-radius: 0 7px 7px 0;
  color: var(--ink-dim);
  font-size: 13px;
}
.hint strong { color: var(--ink); }

.empty {
  padding: 44px 32px;
  background: var(--panel);
  border: 1px dashed var(--rule);
  border-radius: 10px;
  text-align: center;
  color: var(--ink-dim);
}

.err-banner {
  margin-bottom: 16px;
  padding: 11px 14px;
  background: var(--fault-bg);
  border: 1px solid var(--fault);
  border-radius: 8px;
  color: var(--ink);
}

/* ===================================================================
   FLEET RIBBON  — summary before detail
   =================================================================== */
/* Five fixed tracks rather than auto-fit. auto-fit with five tiles produces a
   4+1 or 3+2 orphan at intermediate widths, and because the dividers used to
   be faked by painting the container in --rule behind a 1px gap, every
   incomplete row rendered as a solid slab of divider colour. Dividers are now
   drawn per cell, so an incomplete row is harmless, and the column count steps
   down explicitly at sizes that keep the rows balanced. */
/* The fleet ribbon is rendered as the pack's NAVY STAT BAND: one bold navy
   block of headline numbers (white) under amber uppercase labels, closed by
   a 3px amber accent rule. The band is navy in BOTH themes, so the on-band
   status colours are fixed band-safe brights rather than theme tokens. */
.ribbon {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 0;
  margin: 0 0 14px;
  background: var(--band);
  border: 1px solid var(--band);
  border-bottom: 3px solid var(--amber);   /* the accent rule */
  border-radius: 10px 10px 0 0;
  overflow: hidden;
  box-shadow: var(--shadow);
}
.rib {
  display: flex;
  flex-direction: column;
  gap: 5px;
  padding: 13px 15px;
  min-width: 0;
  background: transparent;
  /* faint light hairline between cells on the navy band */
  box-shadow: inset -1px 0 0 rgba(255,255,255,.08);
}
.rib-label {
  font: 700 10px/1 var(--cond);
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--band-label);
}
.rib-val {
  font-family: var(--mono);
  font-size: 22px;
  font-weight: 700;
  line-height: 1.05;
  color: var(--band-ink);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.rib-val .unit {
  font-size: 11.5px;
  font-weight: 500;
  color: rgba(255,255,255,.55);
  margin-left: 3px;
}
.rib-sub { font-size: 11px; color: rgba(255,255,255,.52); }
/* A ribbon cell only takes on colour when it is telling you something.
   Band-safe brights so they read on the navy. */
.rib.is-on-air  .rib-val { color: #5FE0A0; }
.rib.is-standby .rib-val { color: #FFCB5C; }
.rib.is-fault   .rib-val { color: #FF9296; }
.rib.is-standby { background: rgba(226,178,58,.16); }
.rib.is-fault   { background: rgba(240,87,92,.18); }

/* ===================================================================
   TOOLBAR
   =================================================================== */
.toolbar {
  display: flex;
  align-items: center;
  gap: 10px 14px;
  flex-wrap: wrap;
  margin: 0 0 12px;
  min-height: 32px;
}
/* The status note sits beside the button and may be long; let it take the
   remaining width and wrap within itself instead of forcing the button on to
   a line of its own. */
.toolbar .muted {
  flex: 1 1 240px;
  min-width: 0;
  font-size: 12.5px;
}
.toolbar form.inline { flex: none; }

/* ===================================================================
   TABLES  (.grid)
   =================================================================== */
/* NOTE: no `overflow: hidden` here, deliberately. It would make the table its
   own scrollport for position:sticky, so the sticky thead would pin against
   the table box and visibly drift down the page instead of holding at the top
   of the viewport. The rounded corners are therefore produced by rounding the
   corner cells below rather than by clipping. */
.grid {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: 10px;
  font-size: 12.5px;
  box-shadow: var(--shadow);
}
.grid thead tr:first-child th:first-child { border-top-left-radius: 9px; }
.grid thead tr:first-child th:last-child  { border-top-right-radius: 9px; }
.grid tbody tr:last-child td:first-child  { border-bottom-left-radius: 9px; }
.grid tbody tr:last-child td:last-child   { border-bottom-right-radius: 9px; }
.grid th,
.grid td {
  padding: 6px 9px;
  text-align: left;
  vertical-align: middle;
  white-space: nowrap;
  border-bottom: 1px solid var(--rule-soft);
}
/* Navy header row per the pack: a navy band with light (--th-ink) labels. */
.grid thead th {
  position: sticky;
  top: 52px;                 /* clears the sticky header */
  z-index: 2;
  background: var(--band);
  border-bottom: 2px solid var(--amber);
  font: 700 10px/1.4 var(--cond);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--th-ink);
  padding: 9px 9px;
}
.grid tbody tr:last-child td { border-bottom: none; }
.grid tbody tr { transition: background .1s; }
.grid tbody tr:hover { background: var(--panel-2); }
.grid a { color: var(--ink); text-decoration: none; }
.grid a:hover { color: var(--brand); text-decoration: underline; }

.grid th.sortable { cursor: pointer; user-select: none; }
.grid th.sortable:hover { background: color-mix(in srgb, var(--band) 84%, #fff); color: #fff; }
.grid th .sort-arrow { color: var(--accent-text); margin-left: 4px; font-size: 9px; }

/* --- channel table -------------------------------------------------- */
#ch-table td { height: 44px; }
#ch-table td.mono { font-size: 11.5px; color: var(--ink-dim); }
#ch-table td.chid {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--ink-faint);
  text-align: right;
  width: 34px;
}
#ch-table td.actions { white-space: nowrap; }
#ch-table td.actions button,
#ch-table td.actions .btn {
  padding: 2px 8px;
  font-size: 11.5px;
  line-height: 18px;
  min-height: 0;
}
#ch-table td.name a { font-weight: 600; font-size: 13px; }

/* Severity stripe: state is carried by POSITION and SHAPE, not colour
   alone -- readable for a colourblind operator and from a distance. */
#ch-table tbody tr > td:first-child { box-shadow: inset 3px 0 0 transparent; }
#ch-table tbody tr.row-standby > td:first-child { box-shadow: inset 3px 0 0 var(--standby); }
#ch-table tbody tr.row-standby { background: var(--standby-bg); }
#ch-table tbody tr.row-standby:hover {
  background: color-mix(in srgb, var(--standby-bg) 70%, var(--panel-2));
}
#ch-table tbody tr.row-fault > td:first-child { box-shadow: inset 3px 0 0 var(--fault); }
#ch-table tbody tr.row-fault { background: var(--fault-bg); }
/* legacy class name, kept so a stale cached page still highlights */
#ch-table tbody tr.row-placeholder > td:first-child { box-shadow: inset 3px 0 0 var(--standby); }
#ch-table tbody tr.row-placeholder { background: var(--standby-bg); }

/* --- key/value tables ----------------------------------------------- */
.kv {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: 10px;
  overflow: hidden;
}
.kv th,
.kv td {
  padding: 8px 14px;
  text-align: left;
  border-bottom: 1px solid var(--rule-soft);
}
.kv th { width: 200px; font-weight: 500; color: var(--ink-dim); }
.kv td { font-family: var(--mono); font-size: 12.5px; }
.kv tr:last-child th, .kv tr:last-child td { border-bottom: none; }

/* ===================================================================
   STATUS VOCABULARY
   A pill plus a leading dot glyph. Never colour alone.
   =================================================================== */
.state {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 9px 2px 7px;
  border-radius: 999px;
  font: 600 11px/1.6 var(--sans);
  letter-spacing: .02em;
  text-transform: lowercase;
  border: 1px solid transparent;
}
.state::before {
  content: "";
  width: 6px; height: 6px;
  border-radius: 50%;
  background: currentColor;
  flex: none;
}
.state-active {
  background: var(--on-air-bg); color: var(--on-air);
  border-color: color-mix(in srgb, var(--on-air) 35%, transparent);
}
.state-inactive {
  background: var(--panel-2); color: var(--ink-faint);
  border-color: var(--rule);
}
.state-failed {
  background: var(--fault-bg); color: var(--fault);
  border-color: color-mix(in srgb, var(--fault) 40%, transparent);
}
.state-standby,
.state-activating,
.state-deactivating,
.state-warn {
  background: var(--standby-bg); color: var(--standby);
  border-color: color-mix(in srgb, var(--standby) 40%, transparent);
}
/* STANDBY is shouted in caps because it means "not your programming". */
.state-standby { text-transform: uppercase; font-weight: 700; letter-spacing: .06em; }

/* An inline count that earns amber only when it is non-zero. */
.count-standby {
  color: var(--standby);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* Cells that carry a bare number, not a pill. */
td.cc, td.cce, td.scte {
  text-align: center;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
}
td.cc.state-active, td.cce.state-active, td.scte.state-active,
td.cc.state-failed, td.cce.state-failed, td.scte.state-failed,
td.cc.muted, td.cce.muted, td.scte.muted {
  display: table-cell;
  background: none;
  border: none;
  border-radius: 0;
  padding: 6px 9px;
  font-weight: 700;
  text-transform: none;
}
td.cc.state-active::before, td.cce.state-active::before,
td.scte.state-active::before, td.cc.state-failed::before,
td.cce.state-failed::before, td.scte.state-failed::before { content: none; }
td.cce.state-failed, td.cc.state-failed { color: var(--fault); }
td.scte.state-active, td.cc.state-active { color: var(--on-air); }

td.bitrate, td.outrate, td.uptime {
  font-family: var(--mono);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}
td.outrate.state-warn {
  display: table-cell; background: none; border: none;
  padding: 6px 9px; color: var(--standby); border-radius: 0;
}
td.outrate.state-warn::before { content: none; }

/* ===================================================================
   BADGES
   =================================================================== */
.badge {
  display: inline-flex;
  align-items: center;
  padding: 2px 7px;
  border-radius: 5px;
  font: 600 10px/1.6 var(--cond);
  letter-spacing: .09em;
  text-transform: uppercase;
  border: 1px solid transparent;
}
.badge-srt {
  background: var(--info-bg); color: var(--info);
  border-color: color-mix(in srgb, var(--info) 32%, transparent);
}
.badge-mc {
  background: var(--on-air-bg); color: var(--on-air);
  border-color: color-mix(in srgb, var(--on-air) 32%, transparent);
}
.badge-monitor {
  background: var(--panel-2); color: var(--ink-dim);
  border-color: var(--rule);
}
.pull-badge.state-active {
  background: var(--on-air-bg); color: var(--on-air);
  border-color: color-mix(in srgb, var(--on-air) 32%, transparent);
}
.pull-badge.state-failed {
  background: var(--fault-bg); color: var(--fault);
  border-color: color-mix(in srgb, var(--fault) 40%, transparent);
}
.pull-badge.state-active::before, .pull-badge.state-failed::before { content: none; }

/* ===================================================================
   CONTROLS
   =================================================================== */
.actions { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
form.inline { display: inline; margin: 0; }

button, .btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 6px 12px;
  background: var(--panel-2);
  color: var(--ink);
  border: 1px solid var(--rule);
  border-radius: 7px;
  font: 500 12.5px/1.35 var(--sans);
  text-decoration: none;
  cursor: pointer;
  transition: background .12s, border-color .12s, color .12s;
}
button:hover, .btn:hover { background: var(--panel-3); border-color: var(--ink-faint); }
button:active, .btn:active { transform: translateY(.5px); }
button[disabled], .btn[disabled] {
  opacity: .42;
  cursor: not-allowed;
  transform: none;
}
button[disabled]:hover { background: var(--panel-2); border-color: var(--rule); }

button.primary {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--brand-ink);
  font-weight: 600;
}
button.primary:hover {
  background: color-mix(in srgb, var(--brand) 86%, var(--ink));
  border-color: color-mix(in srgb, var(--brand) 86%, var(--ink));
}
button.danger {
  background: var(--fault-bg);
  border-color: color-mix(in srgb, var(--fault) 42%, transparent);
  color: var(--fault);
  font-weight: 600;
}
button.danger:hover {
  background: color-mix(in srgb, var(--fault) 22%, transparent);
  border-color: var(--fault);
}

/* ===================================================================
   MATRIX MONITOR
   =================================================================== */
.matrix-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(232px, 1fr));
  gap: 10px;
}
.mtile {
  display: block;
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: 9px;
  overflow: hidden;
  text-decoration: none;
  color: var(--ink);
  box-shadow: var(--shadow);
  transition: transform .1s, border-color .16s, box-shadow .16s;
}
.mtile:hover {
  transform: translateY(-2px);
  border-color: var(--brand-edge);
}
.mtile.state-active   { border-color: color-mix(in srgb, var(--on-air) 30%, var(--rule)); }
.mtile.state-inactive { opacity: .6; }
.mtile.state-failed   { border-color: var(--fault); }
/* On standby the tile is ringed amber and labelled -- no pulsing. */
.mtile.mtile-placeholder {
  border-color: var(--standby);
  box-shadow: inset 0 0 0 2px var(--standby), var(--shadow);
}
.mtile.mtile-placeholder .mtile-state {
  color: var(--standby);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .06em;
}
.mtile.mtile-placeholder .mtile-name::before {
  content: "▲ ";
  color: var(--standby);
}
.mtile-thumb {
  position: relative;
  aspect-ratio: 16/9;
  background: var(--media);
  overflow: hidden;
}
.mtile-img {
  width: 100% !important;
  height: 100% !important;
  object-fit: cover;
  display: block;
  border: none;
  border-radius: 0;
}
.mtile-name {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  padding: 20px 9px 6px;
  background: linear-gradient(to top, rgba(0,0,0,.88), rgba(0,0,0,0));
  color: #fff;
  font-size: 12px;
  font-weight: 600;
  text-shadow: 0 1px 3px rgba(0,0,0,.9);
}
.mtile-foot {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
  padding: 6px 9px;
  border-top: 1px solid var(--rule-soft);
  font-size: 11px;
  color: var(--ink-dim);
}
.mtile-state { text-transform: lowercase; }
.mtile-bps {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

/* ===================================================================
   VIDEO / THUMBNAILS
   =================================================================== */
.thumb-cell { padding: 2px 6px; width: 96px; }
.thumb {
  width: 96px; height: 54px;
  object-fit: cover;
  display: block;
  background: var(--media);
  border: 1px solid var(--rule);
  border-radius: 4px;
  transition: opacity .3s;
}
#ch-table .thumb { width: 88px; height: 50px; }
.thumb.stale { opacity: .32; filter: grayscale(.75); }

.preview-wrap { margin: 0 0 16px; }
.preview {
  display: block;
  width: 100%;
  max-width: 760px;
  aspect-ratio: 16/9;
  object-fit: contain;
  background: var(--media);
  border: 1px solid var(--rule);
  border-radius: 10px;
}
.preview.stale { opacity: .4; filter: grayscale(.7); }
.preview-meta { margin-top: 7px; color: var(--ink-dim); font-size: 12px; }
.preview-wrap .preview-meta { text-align: center; }

/* ===================================================================
   STAT STRIP  (channel detail)
   =================================================================== */
.stat-strip {
  display: flex;
  gap: 1px;
  margin: 14px 0 10px;
  padding: 0;
  overflow-x: auto;
  background: var(--rule);
  border: 1px solid var(--rule);
  border-radius: 10px;
  box-shadow: var(--shadow);
}
.stat {
  flex: 1 1 0;
  min-width: 96px;
  padding: 10px 12px;
  background: var(--panel);
  border-right: none;
  text-align: left;
}
.stat-label {
  margin-bottom: 5px;
  font: 600 10px/1 var(--cond);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.stat-val {
  font-family: var(--mono);
  font-size: 17px;
  font-weight: 600;
  line-height: 1.1;
  color: var(--ink);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* ===================================================================
   CONFIG PANELS  (channel detail)
   =================================================================== */
.cfg-panels {
  display: grid;
  gap: 12px;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  margin: 10px 0 18px;
}
.cfg-panel {
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: 10px;
  padding: 14px 16px;
  box-shadow: var(--shadow);
}
.cfg-title {
  display: flex;
  align-items: center;
  gap: 7px;
  margin-bottom: 10px;
  padding-bottom: 9px;
  border-bottom: 1px solid var(--rule);
  font: 600 10.5px/1 var(--cond);
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.cfg-panel .kv {
  width: 100%;
  border: none;
  background: transparent;
  border-radius: 0;
}
.cfg-panel .kv th {
  width: 132px;
  padding: 5px 6px;
  font-weight: 500;
  background: transparent;
  font-family: var(--sans);
  color: var(--ink-faint);
  font-size: 12px;
}
.cfg-panel .kv td { padding: 5px 6px; }
.cfg-panel .kv tr { border-bottom: 1px solid var(--rule-soft); }
.cfg-panel .kv tr:last-child { border-bottom: none; }
.cfg-panel .kv tr:last-child th, .cfg-panel .kv tr:last-child td { border-bottom: none; }

/* ===================================================================
   FORMS
   =================================================================== */
.form { max-width: 780px; }
.form fieldset {
  margin: 0 0 16px;
  padding: 18px;
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: 10px;
  box-shadow: var(--shadow);
}
.form legend {
  padding: 0 8px;
  font: 600 10.5px/1 var(--cond);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.form label {
  display: block;
  margin: 12px 0;
  font-size: 12.5px;
  color: var(--ink-dim);
}
.form label.checkbox {
  display: flex;
  align-items: center;
  gap: 9px;
  color: var(--ink);
}
.form input[type=text], .form input[type=password], .form input[type=number],
.form input:not([type=checkbox]):not([type=radio]), .form select, textarea {
  display: block;
  width: 100%;
  margin-top: 5px;
  padding: 8px 11px;
  background: var(--canvas);
  color: var(--ink);
  border: 1px solid var(--rule);
  border-radius: 7px;
  font: 13.5px/1.4 var(--sans);
}
.form input:hover, .form select:hover { border-color: var(--ink-faint); }
.form input::placeholder { color: var(--ink-faint); }
.form .actions { margin-top: 10px; }

.form label.radio-row {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 7px;
  padding: 9px 11px;
  background: var(--canvas);
  border: 1px solid var(--rule);
  border-radius: 7px;
  color: var(--ink);
  cursor: pointer;
}
.form label.radio-row:hover { background: var(--panel-2); border-color: var(--ink-faint); }
.form label.radio-row input[type=radio] { width: auto; margin: 0; }

.user-chans {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(224px, 1fr));
  gap: 3px 14px;
  margin-top: 7px;
  padding: 9px;
  max-height: 360px;
  overflow-y: auto;
  background: var(--canvas);
  border: 1px solid var(--rule);
  border-radius: 8px;
}
.chan-row {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 3px 5px;
  border-radius: 5px;
  cursor: pointer;
}
.chan-row:hover { background: var(--panel-2); }
.chan-row .chan-name { flex: 1; font-size: 12.5px; color: var(--ink); }

/* ===================================================================
   LOGIN
   =================================================================== */
.login-wrap {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 78vh;
}
.login-card {
  min-width: 348px;
  padding: 34px 32px 26px;
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: 14px;
  text-align: center;
  box-shadow: 0 24px 60px -34px rgba(0,0,0,.7), var(--shadow);
}
.login-logo {
  /* display owned by .logo-dark/.logo-light (theme swap); setting it here
     would out-specify the swap and show both logos. */
  width: 78%;
  max-width: 224px;
  height: auto;
  margin: 2px auto 12px;
}
.brand-big {
  margin-bottom: 18px;
  font: 800 21px/1 var(--cond);
  letter-spacing: .1em;
  color: var(--brand);
  text-align: center;
}
.brand-sub {
  margin-bottom: 22px;
  font: 600 11.5px/1 var(--cond);
  letter-spacing: .24em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.login-card form { display: flex; flex-direction: column; gap: 10px; }
.login-card input {
  padding: 10px 12px;
  background: var(--canvas);
  color: var(--ink);
  border: 1px solid var(--rule);
  border-radius: 7px;
  font-size: 14px;
}
.login-card input::placeholder { color: var(--ink-faint); }
.login-card button { justify-content: center; padding: 9px 12px; font-weight: 600; }
.login-card .err { color: var(--fault); font-size: 12.5px; }
.login-foot {
  margin-top: 18px;
  font: 500 10.5px/1 var(--cond);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* ===================================================================
   HARDWARE
   =================================================================== */
.cards {
  display: grid;
  gap: 12px;
  grid-template-columns: repeat(auto-fit, minmax(224px, 1fr));
  margin-bottom: 6px;
}
.card {
  padding: 15px;
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: 10px;
  box-shadow: var(--shadow);
}
.card.wide { grid-column: span 2; }
.card .big {
  font-family: var(--mono);
  font-size: 26px;
  font-weight: 600;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
}
.card .big .unit {
  margin-left: 3px;
  font-size: 14px;
  font-weight: 500;
  color: var(--ink-faint);
}
.bar {
  height: 6px;
  margin: 9px 0;
  background: var(--panel-3);
  border-radius: 3px;
  overflow: hidden;
}
.bar-fill {
  height: 100%;
  background: var(--brand);
  border-radius: 3px;
  transition: width .5s;
}
.cores {
  display: grid;
  gap: 6px;
  grid-template-columns: repeat(auto-fill, minmax(184px, 1fr));
}
.core {
  display: grid;
  grid-template-columns: 48px 1fr 48px;
  gap: 9px;
  align-items: center;
  padding: 6px 10px;
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: 7px;
}
.core .core-label {
  font: 600 10px/1 var(--cond);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.core .core-val {
  text-align: right;
  font-family: var(--mono);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}
.temp {
  display: inline-block;
  margin: 2px;
  padding: 4px 10px;
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: 6px;
  font-family: var(--mono);
  font-size: 12px;
}
.temp.state-warn {
  display: inline-block;
  color: var(--standby);
  border-color: color-mix(in srgb, var(--standby) 42%, transparent);
  background: var(--standby-bg);
}
.temp.state-failed {
  display: inline-block;
  color: var(--fault);
  border-color: var(--fault);
  background: var(--fault-bg);
}
.temp.state-warn::before, .temp.state-failed::before { content: none; }

/* ===================================================================
   LOGS
   =================================================================== */
.log {
  max-height: 480px;
  padding: 13px;
  overflow: auto;
  background: var(--canvas);
  border: 1px solid var(--rule);
  border-radius: 10px;
  color: var(--ink-dim);
  font-family: var(--mono);
  font-size: 12px;
  line-height: 1.55;
  white-space: pre-wrap;
  word-break: break-word;
}

/* ===================================================================
   RESELLER / URL SHEET
   =================================================================== */
.rc-table { width: 100%; }
.pkg-list { display: flex; flex-direction: column; gap: 10px; }
.pkg-group-h, .pkg-head {
  font: 600 10.5px/1 var(--cond);
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.pkg-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 11px;
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: 8px;
}
.pkg-chans { color: var(--ink-dim); font-size: 12px; }
.pp-row { display: flex; align-items: center; gap: 10px; }
.pulls { display: flex; flex-direction: column; gap: 6px; }

.url-sheet {
  padding: 26px;
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: 10px;
}
.sheet-head { margin-bottom: 16px; }
.sheet-meta { color: var(--ink-dim); font-size: 12.5px; }
.sheet-urls { display: flex; flex-direction: column; gap: 6px; }
.url-cell {
  font-family: var(--mono);
  font-size: 12px;
  word-break: break-all;
}
.sheet-note, .sheet-foot { color: var(--ink-faint); font-size: 12px; }
.copy { cursor: pointer; }

.stream-info { color: var(--ink-dim); font-size: 12.5px; }
.aud-lang, .audio-pick { font-family: var(--mono); font-size: 12px; }

/* ===================================================================
   PRINT
   =================================================================== */
@media print {
  .topnav, .no-print, .toolbar, .impersonate-bar { display: none !important; }
  body, html { background: #fff; color: #000; }
  main { max-width: none; margin: 0; padding: 0; }
  .grid, .kv, .card, .url-sheet, .cfg-panel { box-shadow: none; border-color: #bbb; }
}

/* ===================================================================
   RESPONSIVE
   =================================================================== */
/* Below this width the 13-column channel table no longer fits, so the wrapper
   becomes a horizontal scroll container. That makes it the scrollport for
   sticky descendants, so the sticky header is disabled in the same breath --
   the two settings must always change together or the header silently stops
   pinning. */
@media (max-width: 1400px) {
  .table-wrap { overflow-x: auto; }
  .grid thead th { position: static; }
}

/* Ribbon steps 5 -> 3 -> 2 columns. Each step is chosen so the tiles stay
   readable without an orphan sitting alone on its own row. */
@media (max-width: 1180px) {
  .ribbon { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .rib { box-shadow: inset -1px 0 0 rgba(255,255,255,.08), inset 0 -1px 0 rgba(255,255,255,.08); }
}
@media (max-width: 760px) {
  .ribbon { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .rib-val { font-size: 18px; }
}

@media (max-width: 900px) {
  main { padding: 0 14px; margin-top: 14px; }
  .toolbar { align-items: flex-start; }
  .stat-strip { flex-wrap: nowrap; }
  .card.wide { grid-column: auto; }
}

/* ===================================================================
   MOTION
   =================================================================== */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
  .mtile:hover { transform: none; }
}

/* ===================================================================
   CUSTOMER'S-EYE VIEW BANNER
   Shown only while an admin is impersonating. Deliberately loud and
   full-bleed: the whole point of this mode is that the UI is otherwise
   indistinguishable from what the customer sees, so the one thing that
   must never be ambiguous is that you are in it.
   =================================================================== */
.imp-bar {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 8px 18px;
  background: var(--brand-soft);
  border-bottom: 1px solid var(--brand-edge);
  font-size: 12.5px;
  color: var(--ink);
}
.imp-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--brand); flex: none;
}
.imp-exit {
  margin-left: auto;
  color: var(--brand);
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
}
.imp-exit:hover { text-decoration: underline; }
@media print { .imp-bar { display: none; } }

/* ===================================================================
   LIVE PREVIEW (HLS)
   The transcode is on-demand and reaped ~60 s after the last segment
   request, so pausing playback is what frees the CPU. The idle panel
   below is what makes that happen when a tab is left open unattended.
   =================================================================== */
video.preview {
  width: 100%;
  max-width: 854px;
  aspect-ratio: 16 / 9;
  border: 1px solid var(--rule);
  border-radius: 8px;
  display: block;
}
.preview-idle {
  max-width: 854px;
  margin: 0 auto;
  aspect-ratio: 16 / 9;
  border: 1px dashed var(--rule);
  border-radius: 8px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  color: var(--ink-dim);
  font-size: 13px;
  background: var(--panel-2);
}
.preview-idle[hidden] { display: none; }

/* Monitor wall tiles: thumbnail by default, video only where asked for. */
.tile-video {
  width: 100%;
  aspect-ratio: 16 / 9;
  background: #000;
  display: block;
  border: none;
}
.tile-live-badge {
  position: absolute; top: 6px; left: 6px;
  font-size: 9.5px; letter-spacing: .8px; text-transform: uppercase;
  font-weight: 700; color: #fff; background: rgba(200,40,40,.9);
  padding: 1px 6px; border-radius: 3px; pointer-events: none;
}

/* ===================================================================
   OVERVIEW DASHBOARD
   Landing page for every role. The markup is identical between admin
   and reseller; what differs is the payload, which is projected
   server-side -- so there is nothing here that could leak headend
   figures into a customer's view by getting a template condition wrong.
   =================================================================== */
.ov { display: flex; flex-direction: column; gap: 16px; }

.ov-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
.ov-eyebrow { display: flex; align-items: center; gap: 10px; font-size: 10.5px;
  font-weight: 700; letter-spacing: 1.6px; text-transform: uppercase; color: var(--ink-faint); }
.ov-head h1 { margin: 4px 0 0; font-size: 26px; letter-spacing: -.3px; }
.ov-updated { font-size: 11.5px; color: var(--ink-faint); font-variant-numeric: tabular-nums; }
.ov-live { display: inline-flex; align-items: center; gap: 6px; color: var(--brand); }
.ov-live-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--brand);
  animation: ovPulse 2s ease-out infinite; }
@keyframes ovPulse {
  0%   { box-shadow: 0 0 0 0 var(--brand-edge); opacity: 1; }
  70%  { box-shadow: 0 0 0 7px transparent; opacity: .75; }
  100% { box-shadow: 0 0 0 0 transparent; opacity: 1; }
}

/* KPI cards */
.ov-kpis { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(178px, 1fr)); }
.ov-kpi { background: var(--panel); border: 1px solid var(--rule); border-radius: 11px;
  padding: 14px 16px 15px; position: relative; overflow: hidden;
  animation: ovRise .5s cubic-bezier(.2,.7,.3,1) both; }
@keyframes ovRise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
.ov-kpi::before { content: ''; position: absolute; inset: 0 auto 0 0; width: 3px; background: var(--rule); }
.ov-kpi.tone-good::before   { background: var(--on-air); }
.ov-kpi.tone-warn::before   { background: var(--standby); }
.ov-kpi.tone-bad::before    { background: var(--alert); }
.ov-kpi.tone-accent::before { background: var(--brand); }
.ov-kpi-label { font-size: 10.5px; letter-spacing: .8px; text-transform: uppercase; color: var(--ink-faint); }
.ov-kpi-val { font-size: 27px; font-weight: 650; letter-spacing: -.5px; margin-top: 5px;
  font-variant-numeric: tabular-nums; line-height: 1.1; }
.ov-kpi-sub { font-size: 11.5px; color: var(--ink-dim); margin-top: 2px; min-height: 14px; }
.ov-kpi-bar { height: 3px; background: var(--panel-2); border-radius: 2px; margin-top: 9px; overflow: hidden; }
.ov-kpi-bar i { display: block; height: 100%; background: var(--brand); width: 0;
  transition: width .6s cubic-bezier(.2,.7,.3,1); }

/* Cards + layout */
.ov-grid   { display: grid; gap: 16px; grid-template-columns: minmax(0,2.1fr) minmax(0,1fr); }
.ov-grid-2 { display: grid; gap: 16px; grid-template-columns: minmax(0,1.25fr) minmax(0,1fr); }
@media (max-width: 1040px) { .ov-grid, .ov-grid-2 { grid-template-columns: 1fr; } }
.ov-card { background: var(--panel); border: 1px solid var(--rule); border-radius: 12px;
  padding: 15px 17px 17px; animation: ovRise .55s cubic-bezier(.2,.7,.3,1) both .1s; min-width: 0; }
.ov-card-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
.ov-card-head h2 { margin: 0; font-size: 14px; letter-spacing: -.1px; }
.ov-sub { font-size: 12px; margin: 18px 0 8px; color: var(--ink-dim); letter-spacing: .4px; text-transform: uppercase; }

/* Chart */
.ov-chart-wrap { position: relative; }
.ov-chart { width: 100%; height: 250px; display: block; overflow: visible; }
.ov-chart-empty { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  color: var(--ink-faint); font-size: 12.5px; }
.ov-grid-line { stroke: var(--rule); stroke-width: 1; stroke-dasharray: 3 5; opacity: .7; }
.ov-grid-lbl  { fill: var(--ink-faint); font-size: 11px; font-variant-numeric: tabular-nums; }
.ov-line { fill: none; stroke-width: 2; stroke-linejoin: round; stroke-linecap: round;
  /* draw-in on first paint; re-renders inherit the finished state */
  stroke-dasharray: 4000; stroke-dashoffset: 0; animation: ovDraw 1.1s ease-out both; }
@keyframes ovDraw { from { stroke-dashoffset: 4000; } to { stroke-dashoffset: 0; } }
.ov-line-a { stroke: var(--brand); }
.ov-line-b { stroke: var(--series-b); }
.ov-area   { stroke: none; opacity: .16; animation: ovFade .9s ease-out both .2s; }
@keyframes ovFade { from { opacity: 0; } to { opacity: .16; } }
.ov-area-a { fill: var(--brand); }
.ov-area-b { fill: var(--series-b); }
.ov-dot-a  { fill: var(--brand); }
.ov-dot-b  { fill: var(--series-b); }
.ov-dot    { animation: ovPulse 2s ease-out infinite; }
.ov-legend { display: flex; gap: 14px; flex-wrap: wrap; }
.ov-leg { font-size: 11px; color: var(--ink-dim); display: inline-flex; align-items: center; gap: 6px; }
.ov-leg::before { content: ''; width: 9px; height: 3px; border-radius: 2px; }
.ov-leg-a::before { background: var(--brand); }
.ov-leg-b::before { background: var(--series-b); }
.ov-axis { display: flex; justify-content: space-between; font-size: 10.5px; color: var(--ink-faint); margin-top: 4px; }

/* Donut */
.ov-donut-wrap { position: relative; display: flex; justify-content: center; padding: 4px 0 10px; }
.ov-donut { width: 168px; height: 168px; transform: rotate(-90deg); }
.ov-ring-bg { fill: none; stroke: var(--panel-2); stroke-width: 12; }
.ov-ring { fill: none; stroke-width: 12; stroke-linecap: butt;
  transition: stroke-dasharray .7s cubic-bezier(.2,.7,.3,1), stroke-dashoffset .7s cubic-bezier(.2,.7,.3,1); }
.ov-ring-ok  { stroke: var(--on-air); }
.ov-ring-sb  { stroke: var(--standby); }
.ov-ring-bad { stroke: var(--alert); }
.ov-donut-mid { position: absolute; inset: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: center; pointer-events: none; }
.ov-donut-num { font-size: 32px; font-weight: 650; letter-spacing: -.6px; font-variant-numeric: tabular-nums; }
.ov-donut-lbl { font-size: 10.5px; letter-spacing: 1.2px; text-transform: uppercase; color: var(--ink-faint); }
.ov-health-legend { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 7px; }
.ov-health-legend li { display: flex; align-items: center; gap: 8px; font-size: 12.5px; color: var(--ink-dim); }
.ov-health-legend b { margin-left: auto; color: var(--ink); font-variant-numeric: tabular-nums; }
.ov-sw { width: 9px; height: 9px; border-radius: 2px; }
.ov-sw-ok { background: var(--on-air); } .ov-sw-sb { background: var(--standby); }
.ov-sw-bad { background: var(--alert); }

/* Alerts + tables */
.ov-alerts { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 7px; }
.ov-alert { display: flex; align-items: center; gap: 9px; font-size: 12.5px;
  padding: 7px 10px; border-radius: 7px; background: var(--panel-2);
  animation: ovRise .4s cubic-bezier(.2,.7,.3,1) both; }
.ov-alert-dot { width: 7px; height: 7px; border-radius: 50%; flex: none; background: var(--standby); }
.ov-alert-down .ov-alert-dot, .ov-alert-impaired .ov-alert-dot { background: var(--alert); }
.ov-alert-name { font-weight: 600; }
.ov-allclear { color: var(--on-air); font-size: 13px; margin: 2px 0; }
.ov-tbl { width: 100%; border-collapse: collapse; font-size: 12.5px; }
.ov-tbl th { text-align: left; font-size: 10px; letter-spacing: .8px; text-transform: uppercase;
  color: var(--ink-faint); padding: 5px 8px 5px 0; border-bottom: 1px solid var(--rule); font-weight: 600; }
.ov-tbl td { padding: 8px 8px 8px 0; border-bottom: 1px solid var(--rule-soft); vertical-align: top; }
.ov-tbl tr:last-child td { border-bottom: none; }
.ov-tbl .num { text-align: right; font-variant-numeric: tabular-nums; }
.ov-tbl .sm { font-size: 11px; }
.pillx { font-size: 10.5px; padding: 1px 7px; border-radius: 10px; border: 1px solid var(--rule); color: var(--ink-dim); }
.pillx.on { border-color: var(--on-air); color: var(--on-air); }

/* Channel chips */
.ov-chips { display: grid; gap: 7px; grid-template-columns: repeat(auto-fill, minmax(186px, 1fr));
  max-height: 340px; overflow-y: auto; }
.ov-chip { display: flex; align-items: center; gap: 8px; padding: 7px 10px; border-radius: 8px;
  background: var(--panel-2); border: 1px solid transparent; font-size: 12px; min-width: 0;
  animation: ovRise .45s cubic-bezier(.2,.7,.3,1) both; transition: border-color .15s, transform .15s; }
.ov-chip:hover { border-color: var(--brand-edge); transform: translateY(-1px); }
.ov-chip-dot { width: 7px; height: 7px; border-radius: 50%; flex: none; background: var(--on-air); }
.ov-chip-standby .ov-chip-dot { background: var(--standby); }
.ov-chip-impaired .ov-chip-dot, .ov-chip-down .ov-chip-dot { background: var(--alert); }
.ov-chip-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ov-chip-rate { color: var(--ink-faint); font-variant-numeric: tabular-nums; font-size: 11px; white-space: nowrap; }

@media (prefers-reduced-motion: reduce) {
  .ov-kpi, .ov-card, .ov-chip, .ov-alert, .ov-line, .ov-area { animation: none !important; }
  .ov-live-dot, .ov-dot { animation: none !important; }
}

/* third throughput series: push to fixed destinations (not via MediaMTX) */
.ov-line-c { stroke: var(--series-c); }
.ov-area-c { fill: var(--series-c); }
.ov-dot-c  { fill: var(--series-c); }
.ov-leg-c::before { background: var(--series-c); }

/* chips are links now */
a.ov-chip { text-decoration: none; color: inherit; cursor: pointer; }

/* =====================================================================
   TREND SPARKLINES
   A compact bitrate history per row, drawn by sparkline() in base.html
   from the 5-minute rollups in /api/history. The point is peripheral
   vision: an operator scanning the column sees a channel that dipped
   without reading a single number.
   ===================================================================== */
.spark-cell {
  width: 108px;
  padding-top: 4px;
  padding-bottom: 4px;
}
.spark {
  display: block;
  width: 96px;
  height: 26px;
  overflow: visible;
}
/* No data yet (a freshly added channel, or the collector just started):
   a dashed baseline reads as "nothing here yet" rather than "flatlined",
   which is a materially different thing to tell an operator. */
.spark-empty { opacity: .5; }

/* =====================================================================
   TRANSMISSION WALL  (overview)
   Live thumbnails answer "is it actually on?" in a way a number cannot:
   a bitrate can look perfect while the picture is black or frozen. Each
   tile carries a freshness dot from thumb_age, because a thumbnailer can
   die while the channel keeps running -- leaving a still frame that looks
   live and is hours old.
   ===================================================================== */
.ov-wall-card { margin-top: 14px; }
.ov-wall-tools {
  display: flex; align-items: center; gap: 14px;
  font-size: 11px; color: var(--ink-faint);
}
.ov-wall-legend { display: inline-flex; align-items: center; gap: 5px; }
.ov-wall-legend i { width: 7px; height: 7px; border-radius: 50%; display: block; }
.ov-wall-legend i.ov-fresh-ok    { background: var(--on-air); }
.ov-wall-legend i.ov-fresh-stale { background: var(--standby); }
.ov-wall-all { color: var(--brand); text-decoration: none; white-space: nowrap; }
.ov-wall-all:hover { text-decoration: underline; }

.ov-wall {
  display: grid;
  /* Sized to fit a full lineup (30 for a customer, 47 for staff) on one
     screen without scrolling: smaller tiles, tighter gutters. auto-fill
     keeps the last row aligned instead of stretching a lone tile. */
  grid-template-columns: repeat(auto-fill, minmax(118px, 1fr));
  gap: 7px;
  padding: 4px 2px 2px;
}
@media (max-width: 720px) {
  .ov-wall { grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); gap: 6px; }
}
.ov-tile {
  display: block; text-decoration: none; color: inherit;
  border: 1px solid var(--rule); border-radius: 7px;
  background: var(--panel-2); overflow: hidden;
  animation: ovTileIn .34s ease both;
  transition: border-color .15s ease, transform .15s ease;
}
.ov-tile:hover { border-color: var(--brand-edge); transform: translateY(-1px); }
.ov-tile:focus-visible { box-shadow: var(--focus); outline: none; }
@keyframes ovTileIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .ov-tile { animation: none; } }

.ov-tile-media {
  position: relative; display: block;
  aspect-ratio: 16 / 9; background: var(--media);
}
.ov-tile-media img {
  width: 100%; height: 100%; object-fit: cover; display: block;
}
.ov-tile-fresh {
  position: absolute; top: 5px; right: 5px;
  width: 7px; height: 7px; border-radius: 50%;
  box-shadow: 0 0 0 2px rgba(0,0,0,.45);
}
.ov-fresh-ok    { background: var(--on-air); }
.ov-fresh-stale { background: var(--standby); }

/* A stale preview is dimmed and desaturated so it cannot be mistaken for a
   live picture at a glance across the room. */
.ov-tile-stale .ov-tile-media img { filter: grayscale(.75) brightness(.62); }
.ov-tile-standby { border-color: color-mix(in srgb, var(--standby) 45%, var(--rule)); }

.ov-tile-foot {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 5px; padding: 4px 6px 5px;
}
.ov-tile-name {
  font-size: 11px; font-weight: 600; color: var(--ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  min-width: 0;
}
.ov-tile-rate {
  font-size: 9.5px; color: var(--ink-faint); white-space: nowrap;
  font-variant-numeric: tabular-nums; flex: none;
}
.ov-wall-empty { padding: 18px; color: var(--ink-faint); font-size: 13px; }

/* =====================================================================
   CHART: range switch, crosshair, hover readout
   ===================================================================== */
.ov-chart-head-right {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
}
.ov-range { display: inline-flex; border: 1px solid var(--rule); border-radius: 6px; overflow: hidden; }
.ov-range button {
  appearance: none; border: 0; background: transparent; cursor: pointer;
  font: inherit; font-size: 11px; line-height: 1;
  padding: 5px 9px; color: var(--ink-dim);
  border-right: 1px solid var(--rule);
  font-variant-numeric: tabular-nums;
}
.ov-range button:last-child { border-right: 0; }
.ov-range button:hover { background: var(--panel-3); color: var(--ink); }
.ov-range button.is-on { background: var(--brand); color: var(--brand-ink); font-weight: 600; }
.ov-range button:focus-visible { box-shadow: var(--focus); outline: none; }

.ov-chart-wrap { position: relative; }
.ov-crosshair {
  position: absolute; top: 0; bottom: 0; width: 1px;
  background: var(--ink-faint); opacity: .55; pointer-events: none;
}
.ov-tip {
  position: absolute; top: 8px; z-index: 4; pointer-events: none;
  background: var(--panel); border: 1px solid var(--rule);
  border-radius: 7px; padding: 8px 10px; box-shadow: var(--shadow);
  font-size: 11.5px; min-width: 150px;
}
.ov-tip-t {
  font-weight: 700; color: var(--ink); margin-bottom: 5px;
  font-variant-numeric: tabular-nums;
}
.ov-tip-row { display: flex; align-items: center; gap: 6px; margin-top: 3px; }
.ov-tip-sw { width: 8px; height: 8px; border-radius: 2px; flex: none; }
.ov-tip-lbl { color: var(--ink-dim); flex: 1; white-space: nowrap; }
.ov-tip-val {
  color: var(--ink); font-weight: 600; font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.ov-tip-u { color: var(--ink-faint); font-weight: 400; margin-left: 2px; font-size: 10px; }
.ov-sw-a { background: var(--amber); }
.ov-sw-b { background: var(--series-b); }
.ov-sw-c { background: var(--series-c); }

/* =====================================================================
   AVAILABILITY STRIP
   One cell per 5-minute bucket. Answers "did anything drop while I was
   away?", which a live gauge cannot -- it only ever shows this instant.
   State is encoded in height as well as colour so it survives a
   colourblind viewer.
   ===================================================================== */
.ov-avail-card { margin-top: 14px; }
.ov-avail {
  display: flex; align-items: flex-end; gap: 2px;
  height: 34px; padding: 4px 2px 0;
  overflow: hidden;
}
.ov-av {
  flex: 1 1 0; min-width: 2px; border-radius: 2px; display: block;
  transition: height .2s ease, background-color .2s ease;
}
.ov-av-ok   { height: 60%; background: var(--on-air); }
.ov-av-warn { height: 85%; background: var(--standby); }
.ov-av-bad  { height: 100%; background: var(--fault); }
.ov-av-none { height: 22%; background: var(--rule); }
.ov-av:hover { filter: brightness(1.25); }
.ov-avail-note { font-size: 11px; color: var(--ink-faint); }
.ov-avail-axis {
  display: flex; justify-content: space-between;
  font-size: 10px; color: var(--ink-faint); padding: 4px 2px 0;
  font-variant-numeric: tabular-nums;
}
.ov-avail-empty { font-size: 12px; color: var(--ink-faint); padding: 6px 2px; }

/* ---- channel filter (dashboard toolbar) ---------------------------- */
.ch-filter { margin-left: auto; display: flex; align-items: center; gap: 8px; }
.ch-filter input {
  appearance: none; font: inherit; font-size: 12.5px;
  padding: 5px 10px; min-width: 190px;
  color: var(--ink); background: var(--panel-2);
  border: 1px solid var(--rule); border-radius: 6px;
}
.ch-filter input::placeholder { color: var(--ink-faint); }
.ch-filter input:focus-visible { box-shadow: var(--focus); outline: none; border-color: var(--brand-edge); }
.ch-filter-count {
  font-size: 11px; color: var(--ink-faint);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}

/* ---- overview state banner ----------------------------------------
   Shown only when the raw numbers would mislead on their own (e.g. a
   fully-armed lineup with no receivers yet reads as "0 Mb/s"). */
.ov-banner {
  margin: 0 0 14px; padding: 11px 14px;
  border: 1px solid var(--rule); border-left-width: 3px;
  border-radius: 7px; background: var(--panel);
  font-size: 13px; color: var(--ink-dim); line-height: 1.5;
}
.ov-banner strong { color: var(--ink); font-weight: 650; }
.ov-banner-ready { border-left-color: var(--on-air); background: var(--on-air-bg); }
.ov-banner-info  { border-left-color: var(--info);   background: var(--info-bg); }
.ov-banner-warn  { border-left-color: var(--standby);background: var(--standby-bg); }
.ov-banner-bad   { border-left-color: var(--fault);  background: var(--fault-bg); }

/* ---- lost-contact state -------------------------------------------
   The console keeps the last good render (blanking it would be worse),
   but stops presenting it as current: the pill says Reconnecting and the
   figures fade, so nobody reads a frozen snapshot as live. */
.ov-live-lost .ov-live-dot { background: var(--standby); animation: none; }
.ov-live-lost { color: var(--standby); }
.ov-stale .ov-kpis,
.ov-stale .ov-chart-wrap,
.ov-stale .ov-donut-wrap,
.ov-stale .ov-wall { opacity: .55; transition: opacity .3s ease; }

.ov-wall-count {
  font-size: 11px; color: var(--ink-faint);
  font-variant-numeric: tabular-nums; margin-right: 2px;
}
