/* Piggy UI styles. Dark by default; no framework. */
:root {
  --bg: #0f1115;
  --panel: #171a21;
  --panel-2: #1e222b;
  --border: #2a2f3a;
  --text: #e6e8ee;
  --muted: #949bab;
  --accent: #f472b6;
  --ok: #22c55e;
  --warn: #f59e0b;
  --danger: #ef4444;
  --radius: 10px;
}

@media (prefers-color-scheme: light) {
  :root {
    --bg: #f6f7f9;
    --panel: #ffffff;
    --panel-2: #f0f2f5;
    --border: #dde1e8;
    --text: #1a1d24;
    --muted: #667085;
  }
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 15px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
code { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: .9em; }

/* ------------------------------------------------------------------ layout */
.topbar {
  display: flex; align-items: center; gap: 1.5rem;
  padding: .75rem 1.5rem;
  background: var(--panel);
  border-bottom: 1px solid var(--border);
  position: sticky; top: 0; z-index: 20;
}
.brand { font-weight: 700; font-size: 1.15rem; color: var(--text); }
.topbar nav { display: flex; gap: 1rem; margin-right: auto; }
.topbar nav a { color: var(--muted); }
.topbar nav a:hover { color: var(--text); }
.nav-toggle {
  display: none; background: none; border: none; cursor: pointer; padding: 0;
  align-items: center; color: var(--muted);
}
.nav-toggle:hover { color: var(--text); }
.nav-toggle svg { width: 20px; height: 20px; fill: currentColor; }

main { padding: 1rem; max-width: 1800px; margin: 0 auto; }

h1 { font-size: 1.5rem; margin: 0 0 1rem; }
h2 { font-size: 1.1rem; margin: 0 0 .75rem; }
h3 { font-size: .95rem; margin: 1rem 0 .5rem; color: var(--muted); }

.page-head {
  display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap;
  gap: .5rem 1.5rem; margin-bottom: 1rem;
}
.page-head h1 { margin: 0; }
.page-head-right { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; }
.columns { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
.columns.three { grid-template-columns: 1fr 1fr 1fr; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 1rem; }
.row { display: flex; gap: .75rem; flex-wrap: wrap; }
.row > label { flex: 1 1 140px; }

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

@media (max-width: 700px) {
  .nav-toggle { display: flex; }
  .topbar nav {
    display: none; flex-direction: column; gap: 0;
    position: absolute; top: 100%; left: 0; right: 0;
    background: var(--panel); border-bottom: 1px solid var(--border);
    box-shadow: 0 12px 32px rgba(0,0,0,.35); z-index: 30;
    margin-right: 0;
  }
  .topbar nav.open { display: flex; }
  .topbar nav a { padding: .75rem 1.5rem; border-top: 1px solid var(--border); }
  .topbar nav a:first-child { border-top: none; }
}

/* ------------------------------------------------------------------- cards */
.card {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1rem;
  margin-bottom: 1rem;
}
.card.narrow { max-width: 620px; }
.empty { text-align: center; padding: 3rem 1rem; }

.site-card header { display: flex; justify-content: space-between; align-items: center; gap: .5rem; }
.site-card h2 { margin: 0; font-size: 1.05rem; }

.title-row { display: flex; align-items: center; gap: 1em; min-width: 0; }
.flag { font-size: 1em; line-height: 1; flex-shrink: 0; }
/* Wall of screens only -- see #sites-view[data-view="wall"] .wall-status. */
.wall-status { display: none; }
.map-link { display: inline-flex; align-items: center; color: var(--muted); flex-shrink: 0; font-size: 1rem; }
.map-link:hover { color: var(--accent); }
.map-pin-icon { width: 1em; height: 1em; fill: currentColor; display: block; }
.map-link.large { font-size: 3.75rem; }

/* Status row: text on the left, the configure gear (and, when halted, the
   reset button) right-aligned on the same line instead of its own row at
   the card's bottom. */
.status-row { display: flex; align-items: center; justify-content: space-between; gap: .5rem; }
.status-actions { display: flex; align-items: center; gap: .4rem; flex-shrink: 0; }

.configure-btn, .reset-btn {
  flex-shrink: 0;
  width: 22px; height: 22px; display: flex; align-items: center; justify-content: center;
  background: var(--panel-2); border: 1px solid var(--border); border-radius: 999px;
  color: var(--muted);
}
.configure-btn:hover { color: var(--text); border-color: var(--accent); text-decoration: none; }
.configure-btn svg, .reset-btn svg { width: 13px; height: 13px; fill: currentColor; }
.configure-btn.large { width: 40px; height: 40px; }
.configure-btn.large svg { width: 24px; height: 24px; }

/* Acquisition has stopped retrying and needs a human action (e.g. a fresh
   YouTube cookie) -- accent-colored so it reads as actionable, not just
   another icon button. Hidden by default; app.js shows it only while
   SiteStatus.failure_permanent is true. */
.reset-btn { cursor: pointer; color: var(--accent); border-color: var(--accent); }
.reset-btn:hover { background: var(--accent); color: var(--panel); }
.reset-btn:disabled { opacity: .5; cursor: default; background: var(--panel-2); color: var(--muted); border-color: var(--border); }
.reset-btn[hidden] { display: none; }

.thumb { display: block; position: relative; aspect-ratio: 16/9; background: var(--panel-2); border-radius: 8px; overflow: hidden; margin-bottom: .75rem; }
.thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.thumb-empty {
  width: 100%; height: 100%; display: grid; place-items: center;
  color: var(--muted); font-size: .85rem;
}
.thumb-overlay {
  position: absolute; inset: 0; display: none; flex-direction: column; align-items: center;
  justify-content: center; gap: .25rem; background: #2b2b2f; color: #fff;
  text-align: center; font-size: .85rem; padding: .5rem;
}
.thumb-overlay-detail { color: #d0d0d5; font-size: .75rem; }
.thumb.sleeping img, .thumb.disabled img { display: none; }
.thumb.sleeping .thumb-overlay, .thumb.disabled .thumb-overlay { display: flex; }
.thumb-boxes { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.thumb.sleeping .thumb-boxes, .thumb.disabled .thumb-boxes { display: none; }

dl.meta { display: grid; grid-template-columns: auto 1fr; gap: .25rem .75rem; margin: .75rem 0 0; font-size: .875rem; }
dl.meta dt { color: var(--muted); }
dl.meta dd { margin: 0; overflow-wrap: anywhere; }
.quota-pct { color: var(--muted); font-size: .75rem; }

/* ------------------------------------------------------------- site views */
/* The Sites page's #sites-view wraps both the card .grid and a dense
   .site-table, toggled by data-view (set by Piggy.initSiteView() and, to
   avoid a flash of the wrong layout on load, by an inline script in
   index.html that runs before either renders). "grid" is the default and
   needs no override -- these rules only kick in for the other three. */
.site-table { display: none; width: 100%; }
#sites-view[data-view="text"] .grid { display: none; }
#sites-view[data-view="text"] .site-table { display: table; }
.site-table tbody tr:hover { background: var(--panel-2); }

/* Thumbnails list: the existing three card children (.thumb, header,
   dl.meta) get re-placed onto a grid instead of stacking, with .thumb
   spanning both rows in a fixed-width first column -- no markup change. */
#sites-view[data-view="thumbs-list"] .site-card {
  display: grid; grid-template-columns: 140px 1fr; column-gap: 1rem; align-items: start;
}
#sites-view[data-view="thumbs-list"] .site-card .thumb {
  grid-row: 1 / 3; grid-column: 1; margin-bottom: 0;
  /* Without this, .thumb's default align-self: stretch gives it a definite
     height from the spanned row track (driven by header+meta content),
     which fights its own aspect-ratio: 16/9. align-self: start lets the
     height come from the 140px column width via aspect-ratio instead. */
  align-self: start;
}
#sites-view[data-view="thumbs-list"] .site-card header { grid-column: 2; grid-row: 1; }
#sites-view[data-view="thumbs-list"] .site-card dl.meta { grid-column: 2; grid-row: 2; margin-top: .25rem; }
#sites-view[data-view="thumbs-list"] .meta-analysis,
#sites-view[data-view="thumbs-list"] .meta-detect,
#sites-view[data-view="thumbs-list"] .meta-weather,
#sites-view[data-view="thumbs-list"] .meta-stream,
#sites-view[data-view="thumbs-list"] .reset-btn {
  display: none;
}

/* Wall of screens: tiles as large as possible while fitting every enabled
   site on screen with no scrolling, minimal text, no card chrome (just the
   grid gap as a small margin between tiles). Column count is computed by
   Piggy.initSiteView()'s layoutWallGrid() -- see app.js -- since "largest
   tile that still fits N items in the viewport" isn't something CSS alone
   can solve; the auto-fill rule here is only a fallback for the instant
   before that JS runs. The status line normally shown in dl.meta reappears
   next to the title (.wall-status, right-justified via header's existing
   justify-content: space-between) rather than overlaying the thumbnail --
   the sleeping/disabled overlay is unrelated to this and still applies in
   every view, wall included. Disabled sites are skipped entirely here. */
#sites-view[data-view="wall"] .grid {
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  column-gap: 1em; row-gap: .4rem;
  /* layoutWallGrid() sets fixed pixel columns, not 1fr, so when the row
     height (not the row width) is what caps tile size, there's leftover
     width to center rather than leave stacked on one side. */
  justify-content: center;
}
#sites-view[data-view="wall"] .site-card {
  padding: 0; margin-bottom: 0; border: none; background: transparent; border-radius: 0;
}
#sites-view[data-view="wall"] .site-card.disabled-site { display: none; }
#sites-view[data-view="wall"] .site-card .thumb { margin-bottom: .2rem; }
#sites-view[data-view="wall"] .site-card h2 { font-size: .8rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#sites-view[data-view="wall"] .site-card .badges,
#sites-view[data-view="wall"] .site-card dl.meta { display: none; }
#sites-view[data-view="wall"] .site-card .wall-status {
  display: block; flex-shrink: 0; max-width: 45%;
  font-size: .7rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
#sites-view[data-view="wall"] .thumb-overlay { font-size: .7rem; padding: .3rem; }
#sites-view[data-view="wall"] .thumb-overlay-icon {
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
#sites-view[data-view="wall"] .thumb-overlay-detail { font-size: .6rem; }

/* ----------------------------------------------------------------- widgets */
.btn {
  display: inline-block; padding: .5rem .9rem;
  background: var(--accent); color: #17121a;
  border: none; border-radius: 8px; font-weight: 600; font-size: .9rem;
  cursor: pointer;
}
.btn:hover { filter: brightness(1.1); text-decoration: none; }
.btn.small { padding: .35rem .7rem; font-size: .825rem; }
.btn.ghost { background: transparent; color: var(--text); border: 1px solid var(--border); }
.btn.danger { background: transparent; color: var(--danger); border: 1px solid var(--danger); }

/* A bare <input type="file"> otherwise keeps its OS-native button, which
   looks like nothing else in the app. */
input[type="file"] { padding: .3rem; }
input[type="file"]::file-selector-button,
input[type="file"]::-webkit-file-upload-button {
  display: inline-block; padding: .5rem .9rem; margin-right: .75rem;
  background: var(--accent); color: #17121a;
  border: none; border-radius: 8px; font-weight: 600; font-size: .9rem;
  cursor: pointer;
}
input[type="file"]::file-selector-button:hover,
input[type="file"]::-webkit-file-upload-button:hover { filter: brightness(1.1); }

.link { background: none; border: none; color: var(--accent); cursor: pointer; padding: 0; font-size: .85rem; }
.link.danger { color: var(--danger); }

.danger-zone {
  margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid var(--border);
}
.danger-zone h3 { margin: 0 0 .35rem; font-size: .95rem; color: var(--danger); }

.pill { padding: .1rem .5rem; border-radius: 999px; font-size: .75rem; border: 1px solid var(--border); }
.pill.ok { color: var(--ok); border-color: var(--ok); }
.pill.off { color: var(--muted); }
.pill.bad { color: var(--danger); border-color: var(--danger); }

/* ---------------------------------------------------------------- live view */
/* The player itself needs no rule: the global `video` style above already
   gives it the 16/9 letterboxed box every other player on the site uses. */
.live-bar {
  display: flex; flex-wrap: wrap; align-items: center; gap: .6rem;
  margin-top: .6rem;
}
.live-bar .btn { margin-left: auto; }
.live-bar .btn ~ .btn { margin-left: 0; }
.snippet {
  padding: .5rem .7rem; border: 1px solid var(--border); border-radius: 6px;
  font-size: .8rem; overflow-x: auto; white-space: pre; margin: .4rem 0;
}
.row-actions { display: flex; gap: .35rem; flex-wrap: wrap; }

/* --------------------------------------------------------------- TV codes */
.inline-form { display: flex; gap: .4rem; flex-wrap: wrap; margin: .6rem 0; }
.inline-form input { flex: 1 1 16rem; }
.plain-list { list-style: none; padding: 0; margin: .6rem 0 0; }
.tv-token {
  padding: .6rem .7rem; border: 1px solid var(--border); border-radius: 6px;
  margin-bottom: .5rem; display: grid; gap: .2rem;
  grid-template-columns: 1fr auto; align-items: center;
}
.tv-token > :not(button) { grid-column: 1; }
.tv-token button { grid-column: 2; grid-row: 1 / span 3; }
.tv-token-label { font-weight: 600; }
/* The URL is the thing being copied by eye onto a TV keyboard, so it is set
   larger than body text and monospaced -- proportional digits and a small
   size are exactly what makes someone mistype a code from across a room. */
.tv-token-url {
  font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: 1.05rem; word-break: break-all;
}

/* ------------------------------------------------------------ fleet summary */
/* Grid, not flex: the middle column centers the stat group in the space
   between the page title and the health badge, while the two 1fr columns
   (one empty, one holding the badge) balance each other so that centering
   is relative to the whole row rather than just the leftover space after
   the stats' own width. */
.fleet-summary {
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center;
  flex: 1; column-gap: 1rem; font-size: .875rem;
}
.fleet-stats {
  grid-column: 2; display: flex; align-items: center; flex-wrap: wrap;
  justify-content: center; gap: 1rem;
}
.fleet-stat { color: var(--muted); }
.fleet-stat strong { color: var(--text); font-weight: 600; }
.fleet-window { font-weight: 400; }
.fleet-sep { color: var(--border); }
#fleet-health-badge { grid-column: 3; justify-self: end; }
.pill.fleet-green { color: var(--ok); border-color: var(--ok); }
.pill.fleet-yellow { color: var(--warn); border-color: var(--warn); }
.pill.fleet-red { color: var(--danger); border-color: var(--danger); }

/* ------------------------------------------------------------------- modal */
.modal {
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--panel); color: var(--text);
  padding: 1.25rem; max-width: 520px; width: calc(100% - 2rem);
}
.modal::backdrop { background: rgba(0, 0, 0, .6); }

.script-modal {
  width: min(1400px, 95vw); max-width: none; max-height: 85vh;
}
/* Same [open]-scoped flex trick as .emoji-modal: lets the <pre> claim the
   remaining height instead of forcing the whole dialog (and page) to scroll. */
.script-modal[open] { display: flex; flex-direction: column; }
.script-modal h2, .script-modal > p { flex: 0 0 auto; }
.script-preview {
  flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: hidden;
  background: var(--panel-2); border: 1px solid var(--border); border-radius: 8px;
  padding: .75rem 1rem; margin: 0;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: .78rem; line-height: 1.4;
  /* Wide + wrapping rather than a fixed-width scroll box: the modal is
     already sized to fit nearly the whole viewport, so wrapping is a rare
     fallback (a very long line or a narrow window), not the common case. */
  white-space: pre-wrap; overflow-wrap: anywhere;
}
.script-preview code { font: inherit; }

/* -------------------------------------------------------------- icon picker */
.emoji-select-btn {
  display: inline-flex; align-items: center; gap: .5rem;
  background: var(--panel-2); color: var(--text);
  border: 1px solid var(--border); border-radius: 8px;
  padding: .4rem .75rem; font: inherit; font-size: .9rem; cursor: pointer;
}
.emoji-select-btn:hover { border-color: var(--accent); }
.emoji-preview { font-size: 1.3rem; line-height: 1; display: inline-flex; min-width: 1.3rem; justify-content: center; }
.emoji-preview svg { color: var(--muted); }
.emoji-select-label { color: var(--muted); }

.emoji-modal {
  width: min(600px, 94vw); max-height: 92vh;
}
/* The UA stylesheet hides a <dialog> via `dialog:not([open]) { display: none }`
   -- scope the flex layout to [open] so a closed dialog stays hidden instead
   of this rule overriding that and leaving it visible/click-interceptable. */
.emoji-modal[open] { display: flex; flex-direction: column; }
.emoji-modal h2 { flex: 0 0 auto; }
.emoji-tabs {
  display: flex; flex-wrap: wrap; flex: 0 0 auto;
  gap: .25rem; border-bottom: 1px solid var(--border); margin-bottom: .75rem;
}
.emoji-tab-btn {
  background: none; border: none; cursor: pointer;
  padding: .5rem .75rem; margin-bottom: -1px;
  font: inherit; font-size: .85rem; color: var(--muted);
  border-bottom: 2px solid transparent;
}
.emoji-tab-btn:hover { color: var(--text); }
.emoji-tab-btn.active { color: var(--text); border-bottom-color: var(--accent); font-weight: 600; }

#emoji-search { flex: 0 0 auto; }

/* Cell size/gap/row-count are fixed constants shared with the pagination math
   in app.js (CELL_SIZE/GRID_GAP/PAGE_ROWS) -- a page's item count adapts to
   however many columns fit at this fixed size, so cells never shrink to
   cram more in; instead there's another page. The explicit height (7 rows)
   keeps the picker the same size across every page/category, including a
   last page with fewer items. overflow stays visible so a hovered cell's 2x
   scale isn't clipped by the grid's own box. */
.emoji-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(44px, 1fr));
  grid-auto-rows: 44px;
  /* box-sizing: content-box so this height is just the 7 row-tracks + gaps,
     independent of the padding/border below -- the global reset makes every
     element border-box by default, which would otherwise eat this height
     budget and clip/overflow the last row by exactly padding + border. */
  box-sizing: content-box;
  height: calc(7 * 44px + 6 * .25rem);
  align-content: start;
  gap: .25rem; margin-top: .6rem; overflow: visible;
  flex: 0 0 auto;
  padding: .5rem; border: 1px solid var(--border); border-radius: 8px; background: var(--panel-2);
}
.emoji-cell {
  position: relative; /* positioning root for .emoji-name, always on so it doesn't shift on hover */
  width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;
  background: none; border: none; border-radius: 6px; cursor: pointer; padding: 0;
  transition: background-color .12s ease;
}
.emoji-cell:hover, .emoji-cell:focus-visible {
  z-index: 5; /* clear neighboring cells/rows, including their name labels */
  background: color-mix(in srgb, var(--accent) 18%, var(--panel-2));
  outline: none;
}
/* Only the glyph scales on hover -- .emoji-name is its sibling, not a
   descendant, so it never inherits this transform and stays readable at
   normal size instead of blowing up to 3x along with the emoji. */
.emoji-glyph {
  display: block; font-size: 1.5rem; line-height: 1;
  transform: scale(1); transition: transform .12s ease;
}
.emoji-cell:hover .emoji-glyph, .emoji-cell:focus-visible .emoji-glyph { transform: scale(3); }

.emoji-name {
  /* Centered under the cell by default; app.js clamps this via --name-shift
     for cells near the grid's left/right edge, where a naively centered
     11rem-wide box would extend past the dialog's own boundary and get
     clipped instead of wrapping. */
  position: absolute; top: 100%; left: 50%;
  transform: translateX(calc(-50% + var(--name-shift, 0px)));
  margin-top: .4rem; width: 11rem; z-index: 1;
  background: var(--panel); color: var(--text);
  border: 1px solid var(--border); border-radius: 6px;
  padding: .3rem .5rem; box-shadow: 0 2px 8px rgba(0, 0, 0, .2);
  font-size: .8rem; line-height: 1.25; text-align: center;
  /* Clamp to 2 lines for the handful of long names (e.g. "person in
     motorized wheelchair facing right") rather than growing the box or
     wrapping indefinitely. */
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; text-overflow: ellipsis;
  opacity: 0; pointer-events: none; transition: opacity .12s ease;
}
.emoji-cell:hover .emoji-name, .emoji-cell:focus-visible .emoji-name { opacity: 1; }
.emoji-grid p.muted { grid-column: 1 / -1; text-align: center; padding: 1rem 0; }

.emoji-pagination {
  display: flex; align-items: center; justify-content: center; gap: .75rem;
  flex: 0 0 auto; margin-top: .5rem; color: var(--muted); font-size: .85rem;
}
.emoji-page-btn {
  background: var(--panel-2); color: var(--text);
  border: 1px solid var(--border); border-radius: 6px;
  width: 1.75rem; height: 1.75rem; line-height: 1;
  font: inherit; font-size: 1rem; cursor: pointer;
}
.emoji-page-btn:hover:not(:disabled) { border-color: var(--accent); }
.emoji-page-btn:disabled { opacity: .4; cursor: default; }
.key-reveal {
  display: flex; align-items: center; gap: .5rem;
  background: var(--panel-2); border: 1px solid var(--border); border-radius: 8px;
  padding: .6rem .75rem; margin-top: .5rem;
}
.key-reveal code { overflow-wrap: anywhere; flex: 1; }

.badges { display: flex; align-items: center; gap: .4rem; flex-shrink: 0; }
.stream-row { display: flex; align-items: center; gap: .4rem; }
.source-badge {
  padding: .1rem .45rem; border-radius: 4px; font-size: .7rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .02em; line-height: 1.4;
  color: #fff; text-decoration: none; display: inline-block;
}
.source-badge:hover { filter: brightness(1.1); text-decoration: none; }
.source-badge.yt { display: inline-flex; align-items: center; padding: .2rem .35rem; background: #dc2626; }
.source-badge.yt svg { width: 11px; height: 11px; fill: #fff; }
.source-badge.rtsp { background: #4a6fa0; }
.source-badge.archival { background: var(--muted); color: var(--panel); }

.tag {
  background: var(--panel-2); border: 1px solid var(--border);
  padding: .05rem .4rem; border-radius: 4px; font-size: .75rem;
  font-family: ui-monospace, monospace;
}

.spinner {
  display: inline-block; width: .8em; height: .8em; margin-right: .4em;
  border: 2px solid currentColor; border-right-color: transparent;
  border-radius: 50%; vertical-align: -.15em;
  animation: spin .7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

.muted { color: var(--muted); }
.ok-text { color: var(--ok); }
.warn-text { color: var(--warn); }
.danger-text { color: var(--danger); }
.sleep-text { color: var(--muted); }
.hint { color: var(--muted); font-size: .825rem; margin: .25rem 0; max-width: 75ch; }

/* -------------------------------------------------------------------- tabs */
.tabs {
  display: flex; gap: .25rem; flex-wrap: wrap;
  border-bottom: 1px solid var(--border);
  margin-bottom: 1rem;
}
.tab-btn {
  background: none; border: none; cursor: pointer;
  padding: .6rem .9rem; margin-bottom: -1px;
  font: inherit; font-size: .9rem; color: var(--muted);
  border-bottom: 2px solid transparent;
}
.tab-btn:hover { color: var(--text); }
.tab-btn.active { color: var(--text); border-bottom-color: var(--accent); font-weight: 600; }
.tab-panel[hidden] { display: none; }

/* ------------------------------------------------------------------- forms */
/* Every other page already wraps its .form in a .card.narrow (620px); the
   configure page's tab panels are full-width cards, so .form needs its own
   cap here or a field sizes to the whole card instead of a sane column. */
.form { display: flex; flex-direction: column; gap: .75rem; max-width: 620px; }
.form label { display: flex; flex-direction: column; gap: .25rem; font-size: .875rem; color: var(--muted); }
.form label.check, label.inline { flex-direction: row; align-items: center; gap: .5rem; }

input, select, textarea {
  background: var(--panel-2); color: var(--text);
  border: 1px solid var(--border); border-radius: 6px;
  padding: .45rem .6rem; font: inherit; font-size: .9rem;
  width: 100%;
}
input[type="checkbox"] { width: auto; }
input:focus, select:focus, textarea:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
textarea { resize: vertical; font-family: ui-monospace, monospace; }

.zone-controls {
  display: flex; gap: .5rem; align-items: end; flex-wrap: wrap; margin-bottom: .75rem;
}
/* The global `width: 100%` rule would otherwise force every select/input
   here onto its own full-width line instead of sitting in the row. */
.zone-controls select, .zone-controls input, .zone-controls textarea {
  width: auto; flex: 1 1 200px;
}

/* ------------------------------------------------------------------ player */
/* aspect-ratio is a 16:9 placeholder for before a clip loads (avoids layout
   jank) -- initReview's syncVideoAspectRatio() overwrites it with the real
   ratio on 'loadedmetadata', since footage can be portrait (9:16 cameras).
   object-fit: contain is a fallback for the moment before that happens: it
   letterboxes instead of stretching, which is also what containGeometry's
   box-overlay math assumes.
   max-height caps how tall a portrait clip can grow: width stays 100% of
   the card, but without a cap a 9:16 video at full card width would compute
   a height far past the viewport, leaving only its top slice on screen
   (looks like a zoomed-in crop) instead of the whole frame. object-fit:
   contain then pillarboxes the now width-constrained portrait video within
   the capped box, which containGeometry already accounts for. */
video {
  width: 100%; aspect-ratio: 16/9; object-fit: contain; background: #000;
  border-radius: 8px; display: block; max-height: 75vh;
}
.video-wrap { position: relative; }
.player-boxes-canvas {
  position: absolute; inset: 0; width: 100%; height: 100%;
  pointer-events: none; border-radius: 8px;
}

/* Clip info on the left, overlay-style toggle on the right; wraps to two
   rows on a narrow screen rather than squeezing the info text. */
.player-controls {
  display: flex; align-items: center; justify-content: space-between;
  gap: .75rem; flex-wrap: wrap; margin-top: .4rem;
}
.segmented { display: inline-flex; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
.segmented button {
  background: var(--panel); color: var(--muted); border: 0;
  border-left: 1px solid var(--border);
  padding: .25rem .6rem; font-size: .8rem; cursor: pointer;
}
.segmented button:first-child { border-left: 0; }
.segmented button:hover { color: var(--text); }
.segmented button.active { background: var(--panel-2); color: var(--text); }

/* --------------------------------------------------------- review timeline */
#timeline-container { width: 100%; margin-top: .75rem; }
.timeline-widget { position: relative; }
.timeline-canvas { display: block; width: 100%; border-radius: 8px; cursor: grab; touch-action: none; }
.timeline-canvas:active { cursor: grabbing; }
/* No `display` on the bare class: an equal-specificity author rule would
   outrank the UA [hidden] rule and defeat `playButton.hidden = true` (see
   the same gotcha noted above #metrics-chart). Flex centering only applies
   once :not([hidden]) is also true. */
.timeline-play-btn {
  position: absolute; transform: translate(-50%, -50%);
  width: 22px; height: 22px; border-radius: 999px; padding: 0;
  border: 1px solid var(--accent); background: var(--panel); color: var(--accent);
  font-size: .7rem; line-height: 1; cursor: pointer; pointer-events: auto;
}
.timeline-play-btn:not([hidden]) { display: flex; align-items: center; justify-content: center; }
.timeline-play-btn:hover { background: var(--accent); color: #17121a; }

/* Same [hidden]-vs-bare-class gotcha as .timeline-play-btn above. Font
   matches the ruler labels drawn on the canvas (11px system-ui). */
.timeline-tooltip {
  position: absolute; z-index: 5; pointer-events: none;
  max-width: 260px; padding: .35rem .55rem; border-radius: 6px;
  border: 1px solid var(--border); background: var(--panel); color: var(--text);
  font: 11px system-ui, sans-serif; line-height: 1.4;
  box-shadow: 0 2px 8px rgba(0, 0, 0, .25);
}
.timeline-tooltip:not([hidden]) { display: block; }
.timeline-tooltip-detail { color: var(--muted); }

/* The Status track's combined capture+analysis tooltip can run to several
   lines (breakdown + error detail) -- roughly double a thumbnail's
   footprint (120x64) gives it room to stay readable instead of a wall of
   tiny wrapped text. */
.timeline-tooltip-lg { max-width: 240px; font-size: 13px; line-height: 1.5; }

/* Fixed to the viewport (not the container) and sized/positioned in JS to
   exactly match the video element's own rect -- see positionScrubOverlay().
   Centers a smaller floating thumbnail card rather than filling the whole
   player, so it reads as a preview hovering *over* the video (still
   visible, dimmed, underneath) instead of looking like the video itself
   changed -- the two states (live player vs. scrub preview) stay visually
   distinct. */
.timeline-scrub-overlay {
  position: fixed; z-index: 6; pointer-events: none;
  align-items: center; justify-content: center;
  overflow: hidden;
}
.timeline-scrub-overlay:not([hidden]) { display: flex; }

/* Even dim wash across the whole player, so it's clear the player itself is
   momentarily "inactive" underneath the floating preview. */
.timeline-scrub-dim {
  position: absolute; inset: 0; z-index: 0;
  background: rgba(0, 0, 0, .4);
}

/* The floating card itself (thumbnail or "No Data"). Sized in JS (see
   positionScrubOverlay()) rather than with a CSS max-width/height: a plain
   <img> with width/height:auto never scales *larger* than its own intrinsic
   pixels, and thumbnails are generated at a fixed resolution well below
   most players' rendered size, so a CSS-only cap left it stuck at native
   size instead of filling 80% of the player. The blurred box-shadow is the
   second, stronger darkening layer, composited on top of the flat dim
   above and following border-radius automatically for the rounded-corner
   requirement. The dark border is what makes the card itself read as a
   distinct object against that shadow. */
.timeline-scrub-img, .timeline-scrub-nodata {
  border-radius: 8px;
  border: 3px solid rgba(0, 0, 0, .85);
  box-shadow: 0 0 55px 15px rgba(0, 0, 0, .65);
}
.timeline-scrub-img:not([hidden]) { display: block; }

.timeline-scrub-nodata {
  position: relative; z-index: 1;
  background: #000;
  font: 700 28px system-ui, sans-serif; color: rgba(255, 255, 255, .18);
}
.timeline-scrub-nodata:not([hidden]) { display: flex; align-items: center; justify-content: center; }

/* Wraps scrubImg + scrubBoxes so the boxes canvas can be absolutely
   positioned against the image's own computed card size (see
   positionScrubOverlay()) without duplicating its aspect-ratio math. */
.timeline-scrub-card { position: relative; z-index: 1; }
.timeline-scrub-boxes {
  position: absolute; inset: 0; width: 100%; height: 100%;
  pointer-events: none;
}

/* Object-counts breakdown: thin grid between every cell, but a heavier rule
   across the very top (ahead of the header row) to set it off from the
   summary line above. */
.timeline-tooltip-table { border-collapse: collapse; margin-top: .3rem; width: 100%; }
.timeline-tooltip-table th, .timeline-tooltip-table td {
  border: 1px solid var(--border); padding: .1rem .4rem; text-align: right; font-weight: normal;
}
.timeline-tooltip-table th:first-child, .timeline-tooltip-table td:first-child { text-align: left; }
.timeline-tooltip-table thead th { border-top: 2px solid var(--text); color: var(--muted); }

/* Same [hidden]-vs-bare-class gotcha as .timeline-play-btn above. `left`/
   `top` (set in timeline.js) are the box's own top-left corner -- sized and
   positioned to sit in the gap's blank space beside a thumbnail, not
   centered over it, so no transform here. */
.timeline-gap-link {
  position: absolute; z-index: 4; box-sizing: border-box;
  flex-direction: column; align-items: center; justify-content: center; gap: .1rem;
  border: 1px solid var(--border); border-radius: 8px; background: var(--panel);
  color: var(--text); padding: .2rem; cursor: pointer; pointer-events: auto;
  line-height: 1.15; overflow: hidden;
}
.timeline-gap-link:not([hidden]) { display: flex; }
.timeline-gap-link:hover { border-color: var(--accent); color: var(--accent); }
.timeline-gap-link-arrow { font-size: 1.15rem; }
.timeline-gap-link-text { font-size: .65rem; color: var(--muted); text-align: center; }
.timeline-gap-link:hover .timeline-gap-link-text { color: var(--accent); }

.timeline-legend { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: .5rem; }
.timeline-legend-chip {
  font-size: .78rem; padding: .15rem .6rem; border-radius: 999px;
  border: 1px solid var(--border); background: transparent; color: var(--muted);
  cursor: pointer;
}
.timeline-legend-chip.active { color: var(--text); border-color: var(--accent); }

table { width: 100%; border-collapse: collapse; font-size: .85rem; }
th, td { text-align: left; padding: .4rem .5rem; border-bottom: 1px solid var(--border); }
th { color: var(--muted); font-weight: 600; position: sticky; top: 0; background: var(--panel); }
table.rules td { vertical-align: top; }

/* ------------------------------------------------------------------- zones */
.zone-canvas-wrap { position: relative; display: block; width: 100%; }
.zone-canvas-wrap img { width: 100%; height: auto; display: block; border-radius: 8px; }
#zone-canvas { position: absolute; inset: 0; cursor: crosshair; }

.zone-list { list-style: none; margin: 0 0 .75rem; padding: 0; font-size: .85rem; }
.zone-list li { display: flex; gap: .5rem; align-items: center; padding: .3rem 0; border-bottom: 1px solid var(--border); }
.zone-list li { flex-wrap: wrap; }

/* A dashed-border placeholder shown instead of a list's or table's rows
   when nothing is configured yet. OCR zones, SAM3 zones, weather axes,
   metric webhooks, alert rules, and text overlays all use it, so it
   stays consistent instead of each tab writing its own "None." text.
   Declared after .zone-list li so its own rules win the specificity tie:
   .zone-list li is a flex row, which would otherwise shrink the placeholder
   to its text width instead of the full row. */
tr.empty-row td, li.empty-row { border-bottom: none; padding: 0; }
li.empty-row { display: block; }
.empty-state {
  display: block; box-sizing: border-box; width: 100%;
  text-align: center; padding: 1.1rem .75rem;
  border: 1px dashed var(--border); border-radius: var(--radius);
  color: var(--muted); font-size: .85rem;
}

.zone-sublist { list-style: none; margin: .25rem 0 0; padding: 0 0 0 1rem; width: 100%; }
.zone-sublist li { display: flex; gap: .5rem; align-items: center; padding: .15rem 0; border: none; }

.new-rule { margin-top: 1rem; }
.new-rule summary { cursor: pointer; color: var(--accent); font-size: .9rem; }
.new-rule .form { margin-top: .75rem; }

/* -------------------------------------------------------------------- bell */
.bell-wrap { position: relative; }
.bell { background: none; border: none; font-size: 1.2rem; cursor: pointer; position: relative; color: var(--text); }
.badge {
  position: absolute; top: -6px; right: -8px;
  background: var(--danger); color: #fff;
  border-radius: 999px; padding: 0 .3rem;
  font-size: .68rem; font-weight: 700; line-height: 1.4;
}
.bell-menu {
  position: absolute; right: 0; top: 2rem; width: 340px;
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: 0 12px 32px rgba(0,0,0,.35);
  z-index: 30;
}
.bell-menu-head {
  display: flex; justify-content: space-between; align-items: center;
  padding: .6rem .8rem; border-bottom: 1px solid var(--border);
}
.bell-menu ul { list-style: none; margin: 0; padding: 0; max-height: 380px; overflow-y: auto; }
.bell-menu li { padding: .6rem .8rem; border-bottom: 1px solid var(--border); cursor: pointer; font-size: .85rem; }
.bell-menu li:hover { background: var(--panel-2); }
.bell-menu li.unread { border-left: 3px solid var(--accent); }
.n-title { font-weight: 600; }
.n-body { color: var(--muted); font-size: .8rem; }
.bell-menu time { color: var(--muted); font-size: .72rem; }

/* ---------------------------------------------------------------- refresh */
.refresh-wrap { display: flex; align-items: center; gap: .1rem; margin-right: 2em; }
.refresh-btn-nav {
  background: none; border: none; cursor: pointer; padding: 0;
  display: flex; align-items: center; color: var(--muted);
}
.refresh-btn-nav:hover { color: var(--text); }
.refresh-btn-nav svg { width: 16px; height: 16px; fill: currentColor; }
@keyframes spin { to { transform: rotate(360deg); } }
.refresh-btn-nav.spinning svg { animation: spin .6s linear; }
.refresh-rate-wrap { position: relative; }
.refresh-rate-btn-nav {
  background: none; border: none; cursor: pointer; padding: 2px;
  display: flex; align-items: center; color: var(--muted);
}
.refresh-rate-btn-nav:hover { color: var(--text); }
.refresh-rate-btn-nav svg { width: 10px; height: 10px; fill: currentColor; }
.refresh-menu {
  position: absolute; left: 0; top: 1.5rem; min-width: 90px;
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: 0 12px 32px rgba(0,0,0,.35);
  z-index: 30; padding: .3rem;
}
.refresh-menu-item {
  display: block; width: 100%; text-align: left;
  background: none; border: none; color: var(--text);
  padding: .4rem .6rem; border-radius: 6px; font-size: .85rem; cursor: pointer;
}
.refresh-menu-item:hover { background: var(--panel-2); }
.refresh-menu-item.active { color: var(--accent); font-weight: 600; }

/* ------------------------------------------------------------------- toast */
.toast {
  position: fixed; bottom: 1.5rem; left: 50%; transform: translateX(-50%);
  background: var(--panel); border: 1px solid var(--border);
  border-radius: 8px; padding: .6rem 1rem; font-size: .875rem;
  box-shadow: 0 8px 24px rgba(0,0,0,.35); z-index: 50;
}
.toast.ok { border-color: var(--ok); }
.toast.error { border-color: var(--danger); }

progress { width: 100%; height: .5rem; }

/* ---------------------------------------------------------------- user menu */
.user-menu { display: flex; align-items: center; gap: .6rem; margin-left: .75rem; }
.error { color: var(--danger); font-size: .85rem; }

/* -------------------------------------------------------------- channels UI */
.channel-list { list-style: none; margin: 0; padding: 0; }
.channel-list li {
  display: flex; align-items: center; gap: .6rem; padding: .6rem 0;
  border-bottom: 1px solid var(--border);
}
.channel-list .label { font-weight: 600; }
.badge-type {
  font-size: .7rem; padding: .1rem .5rem; border-radius: 999px;
  background: var(--panel-2); color: var(--muted); text-transform: uppercase;
}
