/* camviewer — dark by default. A camera wall lives in a back office at 2am;
   a bright UI next to a black frame destroys whatever night vision the person
   looking at it has, and washes out exactly the dark detail they are reading. */

:root {
  /* Native form controls (Safari/iPhone above all) follow the dark palette:
     a <select> would otherwise paint white with our light text on it. */
  color-scheme: dark;
  --bg:       #0b0e13;
  --panel:    #11151b;
  --panel-2:  #161b23;
  --line:     #232a34;
  --text:     #e6edf5;
  --muted:    #8b96a5;
  --accent:   #4da3ff;
  --ok:       #3fb950;
  --warn:     #d29922;
  --err:      #ff6b6b;
  --radius:   6px;
}

* { box-sizing: border-box; }

/* The whole UI hides elements with the `hidden` attribute (modals, tooltips,
   no-signal overlays, panels). A normal author rule like `.modal{display:grid}`
   OUTRANKS the browser's own `[hidden]{display:none}` (author beats UA), so
   without this every such element — the modal backdrop above all — stays
   painted after JS sets `hidden`, dimming the app. `!important` makes `hidden`
   win regardless of the element's own display rule. */
[hidden] { display: none !important; }

/* Viewer role (e.g. police): the server already refuses every admin endpoint,
   but the UI should not tease controls it cannot use. Anything marked
   [data-admin] is hidden once the app knows it is running as a non-admin. */
body[data-role="viewer"] [data-admin] { display: none !important; }

.userlist { display: grid; gap: 6px; margin: 10px 0; }
.userlist .urow { display: flex; align-items: center; gap: 8px; padding: 6px 8px;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 6px; font-size: 13px; }
.userlist .urole { font-size: 11px; padding: 1px 6px; border-radius: 4px; background: var(--line); color: var(--muted); }
.userlist .urole.admin { background: #3a2a08; color: #f0c04a; }
.userlist .uspacer { flex: 1; }
.userform { display: grid; gap: 6px; margin-top: 10px; }
.form-title { margin: 0; font-size: 13px; font-weight: 600; }
.userform input, .userform select { width: 100%; padding: 6px 8px; background: var(--panel-2);
  border: 1px solid var(--line); color: var(--text); border-radius: 6px; font-size: 12px; }

html, body {
  margin: 0;
  height: 100%;
  background: var(--bg);
  color: var(--text);
  font: 14px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif;
  overscroll-behavior: none;
}
/* The whole app fits the window: top bar, then the stage + side panel taking
   exactly what is left. Nothing here scrolls the page itself — lists scroll
   inside their panels, and the video, transport and timeline are always on
   screen together. */
body { display: flex; flex-direction: column; overflow: hidden; }

button, select, input { font: inherit; color: inherit; }
code, kbd { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: .92em; }
kbd { background: var(--panel-2); border: 1px solid var(--line); border-radius: 4px; padding: 1px 5px; }
.muted { color: var(--muted); }
.pad { padding: 12px 14px; }

.warn {
  background: #2a2010; border: 1px solid #5a4416; color: #e8c46a;
  padding: 8px 10px; border-radius: var(--radius); margin: 8px 0; font-size: 13px;
}
.err { color: var(--err); }
.fatal { padding: 24px; color: var(--err); white-space: pre-wrap; }

/* ---------- login gate ------------------------------------------------- */

.gate { display: grid; place-items: center; flex: 1; }
.gate form {
  background: var(--panel); border: 1px solid var(--line); border-radius: 10px;
  padding: 28px 30px; width: min(340px, 90vw); display: grid; gap: 10px;
}
.gate h1 { margin: 0; font-size: 20px; }
.gate p { margin: 0 0 6px; }
.gate input, .gate button {
  padding: 9px 11px; border-radius: var(--radius); border: 1px solid var(--line);
  background: var(--panel-2);
}
.gate button { background: var(--accent); color: #04121f; border: 0; font-weight: 600; cursor: pointer; }

/* ---------- top bar ---------------------------------------------------- */

.topbar {
  flex: none;
  display: flex; align-items: center; gap: 14px;
  padding: 8px 12px; background: var(--panel); border-bottom: 1px solid var(--line);
}
.brand { display: flex; align-items: center; gap: 8px; }
.topbar-mid { display: flex; align-items: center; gap: 8px; margin-left: auto; }
.topbar-right { display: flex; align-items: center; gap: 6px; }

.badge {
  font-size: 11px; padding: 2px 7px; border-radius: 20px;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--muted);
  white-space: nowrap;
}
.badge.sim  { background: #3a2a08; border-color: #6b4f10; color: #f0c04a; }
.badge.okb  { background: #10281a; border-color: #1d5233; color: #6fd48a; }
.badge.errb { background: #2c1416; border-color: #6b2429; color: #ff9a9a; }
.badge.bwb  { background: #1c1f24; border-color: #3a414b; color: #cfd6df; }

.pill, .tbtn {
  background: var(--panel-2); border: 1px solid var(--line); color: var(--text);
  border-radius: var(--radius); padding: 6px 11px; cursor: pointer;
}
.pill:hover, .tbtn:hover { background: #1d2430; }
.pill.on, .tbtn.on { background: #14331f; border-color: #266b3c; color: #7ee0a0; }
#btnLive.on { background: #331414; border-color: #7a2626; color: #ff8f8f; }
/* HD/SD is always lit: HD green, SD amber. */
#btnStream.hd { background: #14331f; border-color: #266b3c; color: #7ee0a0; }
#btnStream.sd { background: #3a2a08; border-color: #6b4f10; color: #f0c04a; }

/* Who is signed in, beside Sign out: plain text that opens their account. */
.whoami {
  background: none; border: 0; color: var(--muted); cursor: pointer; padding: 6px 4px;
  font-size: 12.5px; max-width: 12ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.whoami:hover { color: var(--text); text-decoration: underline; }

.zoomers { display: flex; }
.zoomers button {
  background: var(--panel-2); border: 1px solid var(--line); border-right: 0;
  padding: 6px 10px; cursor: pointer; color: var(--muted);
}
.zoomers button:first-child { border-radius: var(--radius) 0 0 var(--radius); }
.zoomers button:last-child  { border-right: 1px solid var(--line); border-radius: 0 var(--radius) var(--radius) 0; }
.zoomers button:hover { color: var(--text); background: #1d2430; }
.zoomers button.on { background: #14331f; color: #7ee0a0; }

input[type=date] {
  background: var(--panel-2); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 5px 8px; color-scheme: dark;
}

/* ---------- layout ----------------------------------------------------- */

.layout {
  display: grid;
  /* Wide enough that every tab label sits comfortably with room for a badge. */
  grid-template-columns: minmax(0, 1fr) 440px;
  flex: 1; min-height: 0;   /* whatever the top bar leaves, however tall it wraps */
}
.stage { display: flex; flex-direction: column; min-width: 0; min-height: 0; }
.side {
  border-left: 1px solid var(--line); background: var(--panel);
  display: flex; flex-direction: column; min-height: 0;
}

/* ---------- video ------------------------------------------------------ */

.video-area {
  flex: 1; min-height: 0; display: grid; gap: 3px; padding: 3px; background: #05070a;
}
/* Rows are minmax(0, 1fr), not 1fr: a plain 1fr row refuses to shrink below
   its content's intrinsic height, and a video's is its 16:9 of the pane width
   — which made the stage taller than the window and forced the page to scroll
   to reach the timeline. Fixed rows per page size, so a short last page keeps
   the same pane size. */
.video-area.grid-1  { grid-template-columns: 1fr; grid-template-rows: minmax(0, 1fr); }
.video-area.grid-4  { grid-template-columns: 1fr 1fr; grid-template-rows: repeat(2, minmax(0, 1fr)); }
.video-area.grid-9  { grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(3, minmax(0, 1fr)); }
.video-area.grid-16 { grid-template-columns: repeat(4, 1fr); grid-template-rows: repeat(4, minmax(0, 1fr)); }

/* Grid paging controls in the top bar. */
.pager {
  display: flex; align-items: center;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--radius);
}
.pager button {
  background: none; border: 0; color: var(--text); padding: 4px 9px; cursor: pointer;
  font-size: 16px; line-height: 1.2;
}
.pager button:hover { background: #1d2430; }
.pager button[disabled] { opacity: .35; cursor: default; background: none; }
.pager span { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 12px; color: var(--muted); padding: 0 2px; }
select.pill { padding: 5px 8px; }

.pane {
  position: relative; background: #000; overflow: hidden; min-height: 0; min-width: 0;
  border-radius: 3px; cursor: pointer;
  /* Double-tap opens the pane menu: no browser double-tap zoom, no long-press selection/callout. */
  touch-action: manipulation; user-select: none; -webkit-touch-callout: none;
}

/* The pane menu (right-click / long-press): which camera shows in this pane. */
.ctxmenu {
  position: fixed; z-index: 80; min-width: 190px; padding: 4px;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 6px;
  box-shadow: 0 10px 30px rgba(0,0,0,.55); font-size: 13px;
}
.ctxmenu .head { padding: 4px 8px 5px; color: var(--muted); font-size: 11px; letter-spacing: .04em; text-transform: uppercase; }
.ctxmenu .sep { height: 1px; margin: 4px 0; background: var(--line); }
.ctxmenu button {
  display: flex; align-items: center; gap: 10px; width: 100%; padding: 6px 8px; text-align: left;
  background: none; border: 0; border-radius: 4px; color: var(--text); font: inherit; cursor: pointer;
}
.ctxmenu button:hover:not(:disabled) { background: var(--panel); }
.ctxmenu button.current { color: var(--muted); cursor: default; }
.ctxmenu button small { margin-left: auto; color: var(--muted); font-size: 11px; }
/* Anchored to the pane rather than flowing in it, so the video's own aspect
   never dictates the pane's (and the page's) height. The zoomer wrapper takes
   the digital zoom transform, so the video keeps its own rotation transform. */
.pane .zoomer { position: absolute; inset: 0; transform-origin: 0 0; }
.pane video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; display: block; }
/* The last decoded frame, held over the video while the next chunk loads. */
.pane .pane-hold { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; display: block; }
/* Spinner over the held frame while the next chunk is fetched or the picture waits for data. */
.pane-loading {
  position: absolute; top: 50%; left: 50%; width: 30px; height: 30px; margin: -15px 0 0 -15px; z-index: 2;
  border: 3px solid rgba(255,255,255,.22); border-top-color: #fff; border-radius: 50%;
  animation: pane-spin .8s linear infinite; pointer-events: none;
}
@keyframes pane-spin { to { transform: rotate(360deg); } }
/* One camera filling the stage: pinch and drag are ours (no page scroll over
   the picture), the cursor says the picture can be dragged once zoomed. */
.video-area.grid-1 .pane { touch-action: none; }
.pane.zoomed { cursor: grab; }
.pane-zoom {
  position: absolute; top: 8px; right: 8px; z-index: 3; padding: 3px 9px;
  font: 11px system-ui, sans-serif; color: var(--text); background: rgba(0,0,0,.6);
  border: 1px solid var(--line); border-radius: 10px; cursor: pointer;
}
/* The active pane: the one the timeline, scrub preview and frame tools work on. */
.pane.focus { outline: 2px solid var(--accent); outline-offset: -2px; }
.pane.focus .pane-name { color: var(--accent); }
/* Orientation correction for a camera mounted sideways or inverted. The stored
   stream is never re-encoded — the picture is rotated here, on display. object-fit
   keeps the whole frame visible at 90°/270° (letterboxed rather than clipped),
   which is the safe trade for a rare portrait mount. */
.pane[data-rot="90"]  video, .pane[data-rot="90"]  .pane-hold { transform: rotate(90deg); }
.pane[data-rot="180"] video, .pane[data-rot="180"] .pane-hold { transform: rotate(180deg); }
.pane[data-rot="270"] video, .pane[data-rot="270"] .pane-hold { transform: rotate(270deg); }
/* Mirrors the camera's real IR-cut state, so the day/night feature is
   visible in the picture rather than only in a settings panel. */
.pane.bw video, .pane.bw .pane-hold { filter: grayscale(1) contrast(1.08); }

.pane-overlay {
  position: absolute; inset: 0; pointer-events: none;
  display: flex; flex-direction: column; justify-content: space-between;
  padding: 7px 9px;
  background: linear-gradient(180deg, rgba(0,0,0,.5), transparent 22%, transparent 78%, rgba(0,0,0,.5));
}
.pane-top { display: flex; align-items: center; gap: 7px; }
.pane-name { font-weight: 600; font-size: 13px; text-shadow: 0 1px 3px #000; }
.pane-ch { font-size: 11px; color: var(--muted); }
.pane-bottom { display: flex; justify-content: flex-end; }
.pane-time {
  font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 12px;
  background: rgba(0,0,0,.55); padding: 2px 6px; border-radius: 3px;
}
/* Lens zoom on a motorised camera: hold + or − to move the lens, ⌂ for the home framing. */
.pane-lens {
  position: absolute; right: 8px; top: 50%; transform: translateY(-50%); z-index: 3;
  display: flex; flex-direction: column; gap: 6px;
}
.pane-lens button {
  width: 32px; height: 32px; padding: 0; border-radius: 50%;
  border: 1px solid var(--line); background: rgba(0,0,0,.55); color: var(--text);
  font: 600 17px/1 system-ui, sans-serif; cursor: pointer; opacity: .7; touch-action: none;
}
.pane:hover .pane-lens button, .pane-lens button:focus-visible { opacity: 1; }
.pane-lens button.on { background: var(--accent); border-color: var(--accent); color: #04111f; opacity: 1; }
/* "Returning to Home": a lens on its way back to the saved framing. */
.pane-note {
  position: absolute; top: 34px; left: 50%; transform: translateX(-50%); z-index: 3;
  padding: 3px 11px; border-radius: 10px; white-space: nowrap; pointer-events: none;
  font: 12px system-ui, sans-serif; color: var(--text);
  background: rgba(0,0,0,.7); border: 1px solid var(--line);
}
/* The camera dialog's lens and SD card lines: what it has, and what can be done with it. */
.camrow { display: flex; align-items: center; gap: 8px; font-size: 12px; flex-wrap: wrap; }
.camrow select { width: auto; }
.pane-nosignal {
  position: absolute; inset: 0; display: grid; place-content: center; gap: 4px;
  text-align: center; background: repeating-linear-gradient(45deg, #180a0b, #180a0b 8px, #1e0d0e 8px, #1e0d0e 16px);
}
.pane-nosignal span { color: #ff8f8f; font-weight: 700; letter-spacing: .08em; font-size: 13px; }
.pane-nosignal small { color: #a97b7b; }

/* ---------- contact sheet ---------------------------------------------- */

.sheet-area { flex: 1; min-height: 0; display: flex; flex-direction: column; background: #05070a; }
.sheet-bar {
  display: flex; align-items: center; gap: 8px; padding: 7px 10px;
  background: var(--panel); border-bottom: 1px solid var(--line); font-size: 12px;
}
.sheet-bar select {
  background: var(--panel-2); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 4px 7px; font-size: 12px;
}
.sheet-hint { margin-left: auto; }

.sheet-grid { --sheet-pad: 6px; flex: 1; overflow-y: auto; padding: var(--sheet-pad); }

/* One section per calendar day. The header is sticky INSIDE its section (a
   sticky grid item could not travel — its containing block is its own grid
   area), so it holds at the top while that day's stills scroll under it and
   the next day's header pushes it off. */
.sheet-day { position: relative; }
.sheet-day + .sheet-day { margin-top: 8px; }
.sheet-day-head {
  /* The sticky edge is inset by the scroll pane's padding; cancel it so the
     header hugs the visible top edge. */
  position: sticky; top: calc(-1 * var(--sheet-pad)); z-index: 1; margin: 0 0 4px;
  padding: 5px 6px 4px; font-size: 12px; font-weight: 600; letter-spacing: .02em;
  color: var(--text); background: rgba(5, 7, 10, .94); border-bottom: 1px solid var(--line);
}
.sheet-day-head small { font-weight: 400; color: var(--muted); margin-left: 8px; }
.sheet-day-tiles {
  display: grid; gap: 4px;
  /* Tiles size themselves to the pane so the whole window stays glanceable
     rather than forcing a horizontal scroll nobody will use. */
  grid-template-columns: repeat(auto-fill, minmax(116px, 1fr));
}

.tile {
  position: relative; aspect-ratio: 16 / 9; background: #0b0e13;
  border: 1px solid transparent; border-radius: 3px; overflow: hidden; cursor: pointer;
}
.tile img { width: 100%; height: 100%; object-fit: cover; display: block; }
.tile b {
  position: absolute; left: 0; bottom: 0; padding: 1px 4px;
  background: rgba(0,0,0,.6); font: 10px/1.4 ui-monospace, Menlo, Consolas, monospace;
  font-weight: 400; color: #cfd6df;
}
.tile:hover { border-color: var(--accent); }
/* A tile with something in it gets a ring, so a block of activity reads as a
   block even when the subject itself is small at this size. */
.tile.hasevent { border-color: #6b5a2a; }
.tile.now { border-color: #ff4d4d; box-shadow: 0 0 0 1px #ff4d4d; }
.tile-kinds { position: absolute; top: 0; left: 0; right: 0; height: 3px; display: flex; }
.tile-kinds i { flex: 1; }

.tile.gap {
  display: grid; place-content: center; gap: 2px; text-align: center;
  background: repeating-linear-gradient(45deg, #180a0b, #180a0b 6px, #1e0d0e 6px, #1e0d0e 12px);
  border-color: #4a1c1f;
}
.tile.gap span { color: #ff8f8f; font-size: 10px; letter-spacing: .04em; }
.tile.gap b { position: static; background: none; color: #a97b7b; }

/* ---------- transport -------------------------------------------------- */

.transport {
  flex: none;   /* the video gives way, never the controls or the timeline */
  display: flex; align-items: center; gap: 3px; flex-wrap: wrap;
  padding: 5px 6px; background: var(--panel); border-top: 1px solid var(--line);
}
/* Compact on purpose: nine controls, the playhead readout and the hint fit a
   ~960px stage on ONE line (a 1920 screen at 135% zoom). Wrapping stays as
   the fallback so nothing is ever clipped — the hint gives way first. */
.transport .tbtn { font-size: 12px; padding: 5px 7px; flex: none; }
.transport .tbtn.wide { min-width: 0; }
.transport .tbtn.play { min-width: 38px; font-size: 13px; }
.transport select { font-size: 12px; padding: 4px 5px; flex: none; }
.tbtn.play { min-width: 46px; font-size: 15px; }
.tbtn.small { padding: 4px 9px; font-size: 12px; }
.tbtn.primary { background: var(--accent); color: #04121f; border-color: transparent; font-weight: 600; }
.tbtn.danger { color: #ff9a9a; border-color: #5c2a2e; }
.tbtn[disabled] { opacity: .45; cursor: not-allowed; }
.tbtn:is(a) { text-decoration: none; display: inline-block; }

.transport select {
  background: var(--panel-2); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 6px 8px;
}
.playhead-readout {
  font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 12px;
  display: flex; gap: 6px; align-items: baseline; margin-left: 2px;
  white-space: nowrap; flex: none;
}
.selection-box {
  margin-left: auto; display: flex; align-items: center; gap: 4px;
  background: #10222f; border: 1px solid #1d4a6b; border-radius: var(--radius);
  padding: 2px 3px 2px 8px; font-size: 12px;
}
.sel-hint {
  margin-left: auto; font-size: 12px; text-align: right;
  flex: 1 1 0; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ---------- timeline --------------------------------------------------- */

.timeline-wrap { flex: none; position: relative; background: #0e1116; border-top: 1px solid var(--line); }
/* − / + in the ruler's empty corner (the name gutter has no header). The
   timeline zooms from here and the −/+ keys — never from the wheel, which
   scrolls things. */
.tlzoom { position: absolute; left: 8px; top: 3px; display: flex; gap: 3px; z-index: 2; }
.tlzoom button {
  width: 20px; height: 20px; padding: 0; line-height: 18px; font-size: 14px; font-weight: 600;
  color: var(--muted); background: var(--panel-2); border: 1px solid var(--line);
  border-radius: 4px; cursor: pointer;
}
.tlzoom button:hover:not(:disabled) { color: var(--text); border-color: var(--accent); }
.tlzoom button:disabled { opacity: .35; cursor: default; }

/* Scroll focus ring (from OurTownTrades' scroll-focus.js): frames the nested
   panel the wheel will scroll while the pointer is over it. No ring = nothing
   under the pointer scrolls on its own. A fixed overlay that never catches
   the pointer; above dialogs (50) and the frame viewer (60), under tooltips. */
.scroll-focus {
  position: fixed; pointer-events: none; z-index: 65; border-radius: 6px; opacity: 0;
  box-shadow: inset 0 0 0 2px var(--accent, #58a6ff), inset 0 0 12px var(--accent, #58a6ff);
  transition: opacity .12s ease, top .09s ease, left .09s ease, width .09s ease, height .09s ease;
}
.scroll-focus.is-on { opacity: .55; }
/* The tape: drag it under the pinned playhead. Horizontal drags are ours;
   vertical ones stay the page's (a phone still scrolls past it). */
#timeline { display: block; cursor: grab; user-select: none; touch-action: pan-y pinch-zoom; }
#timeline.dragging { cursor: grabbing; }
/* Scrub preview. Fixed to the viewport (positioned by hoverTip above the
   pointer and clamped on-screen), so it can never be clipped by the timeline
   box it is drawn from. */
.tltip {
  position: fixed; z-index: 40; pointer-events: none;
  /* One size, always: fixed width, and the text below is always two lines. */
  width: min(158px, 46vw); box-sizing: border-box;
  background: #1b222c; border: 1px solid var(--line); border-radius: 4px;
  padding: 5px 8px; font-size: 12px; white-space: nowrap;
  box-shadow: 0 4px 14px rgba(0,0,0,.5);
}
/* The still under the cursor while hovering or dragging the timeline. Fills
   the tip's fixed width at a fixed aspect, so frames swap without a resize. */
/* Small on purpose: it shows the shape of the moment, not the detail. */
#tlThumb {
  display: block; width: 60%; aspect-ratio: 16 / 9; margin: 0 auto 5px;
  object-fit: cover; border-radius: 3px; background: #000;
}
#tlTipText { height: 30px; line-height: 15px; overflow: hidden; }
#tlTipText b, #tlTipText small { display: block; overflow: hidden; text-overflow: ellipsis; }
#tlTipText b { font-size: 12px; }
#tlTipText small { font-size: 11px; color: var(--muted); }

/* In-modal clip player. The clip plays inside our own card with an obvious
   Close, never handed off to the browser's native full-window video viewer. */
.clip-preview { background: #000; border-radius: 3px; margin: 4px 0 10px; }
.clip-preview video { display: block; width: 100%; max-height: 80vh; border-radius: 3px; }
/* A wide card for things that are looked at rather than read: the clip player. */
.modal-card.wide { width: min(1280px, 96vw); }

/* ---------- side panel ------------------------------------------------- */

.tabs { display: flex; border-bottom: 1px solid var(--line); }
.tabs button {
  position: relative;
  flex: 1; background: none; border: 0; border-bottom: 2px solid transparent;
  /* Top padding reserves the strip where a counter badge sits, so labels line
     up across tabs whether or not one carries a badge. */
  padding: 16px 8px 8px; color: var(--muted); cursor: pointer; font-size: 13px;
  white-space: nowrap;
}
.tabs button:hover { color: var(--text); }
.tabs button.active { color: var(--text); border-bottom-color: var(--accent); }

/* Counter badge ABOVE the label, centred — it never pushes the text around. */
.tabbadge {
  position: absolute; top: 2px; left: 50%; transform: translateX(-50%);
  min-width: 16px; padding: 0 4px;
  background: var(--err); color: #2a0b0b; border-radius: 9px;
  font-size: 10px; font-weight: 700; line-height: 14px; text-align: center;
}

/* Section captions inside the filters block. */
.filter-label {
  font-size: 11px; letter-spacing: .05em; text-transform: uppercase; color: var(--muted);
  margin-bottom: -3px;
}

/* Hover tooltip: fixed, above the pointer, never under it. */
.tip {
  position: fixed; z-index: 70; pointer-events: none;
  background: #1b222c; border: 1px solid var(--line); border-radius: 5px;
  padding: 6px 9px; font-size: 12px; line-height: 1.4; color: var(--text);
  max-width: 300px; box-shadow: 0 6px 20px rgba(0,0,0,.55);
}
.alerthead {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 8px 10px; border-bottom: 1px solid var(--line); font-size: 12px;
}
.card.alert.unread { box-shadow: inset 3px 0 0 var(--err); background: #14181f; }
.card.alert.hi .card-head strong { color: #ffb3b3; }
label.inline { display: flex; flex-direction: row; align-items: center; gap: 7px; color: var(--text); }
label.inline input { width: auto; }
textarea.curl {
  width: 100%; background: var(--panel-2); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 8px; font-family: ui-monospace, Menlo, Consolas, monospace;
  font-size: 11.5px; color: var(--text); resize: vertical; white-space: pre;
}

.tabpanel { flex: 1; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
.list, .scroll { flex: 1; min-height: 0; overflow-y: auto; }

/* Settings sections */
#settings h3 { margin: 2px 0 6px; font-size: 14px; }
#settings .pad + .pad { border-top: 1px solid var(--line); }
#settings .card.off { opacity: .55; }
button.link {
  background: none; border: 0; padding: 0; color: var(--accent); cursor: pointer; font-size: 12px;
}
button.link:hover { text-decoration: underline; }

.filters { padding: 9px 10px; border-bottom: 1px solid var(--line); display: grid; gap: 8px; }
.chips { display: flex; flex-wrap: wrap; gap: 5px; }
.chip {
  background: var(--panel-2); border: 1px solid var(--line); color: var(--muted);
  border-radius: 20px; padding: 2px 9px; font-size: 11.5px; cursor: pointer;
}
.chip:hover { color: var(--text); }
.chip.on { color: #04121f; background: var(--c, var(--accent)); border-color: transparent; font-weight: 600; }
.chip.cam.on { background: var(--accent); }
.score { display: grid; grid-template-columns: auto auto 1fr; gap: 8px; align-items: center; font-size: 12px; color: var(--muted); }
.score input { width: 100%; }
.score output { font-family: ui-monospace, monospace; color: var(--text); }

.daysep {
  position: sticky; top: 0; background: var(--panel-2); border-bottom: 1px solid var(--line);
  padding: 4px 10px; font-size: 11px; color: var(--muted); letter-spacing: .04em;
}
.row {
  display: grid; grid-template-columns: 10px 62px 62px 1fr auto;
  gap: 7px; align-items: center; padding: 5px 10px; cursor: pointer;
  border-bottom: 1px solid #161a21; font-size: 12.5px;
}
.row:hover { background: var(--panel-2); }
.row.sel { background: #16283a; box-shadow: inset 2px 0 0 var(--accent); }
/* The event the playhead is in (followEvents): green, so it reads apart from
   the selected event's blue, and both can show at once. */
.row.now { background: #15291f; box-shadow: inset 3px 0 0 var(--ok); }
.row.sel.now { background: #16283a; box-shadow: inset 3px 0 0 var(--ok); }
.followbar { display: flex; align-items: center; justify-content: space-between; gap: 8px; font-size: 12px; }
.row .dot { width: 8px; height: 8px; border-radius: 2px; }
.row .t { font-family: ui-monospace, monospace; color: var(--muted); }
.row .c { color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.row .note { grid-column: 2 / -1; color: var(--warn); font-size: 11.5px; }

/* ---------- cards ------------------------------------------------------ */

.card {
  border-bottom: 1px solid var(--line); padding: 10px 12px; display: grid; gap: 7px;
}
.card-head { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; }
.card-head strong { font-size: 13px; }
.card-label { font-size: 12.5px; }
.card-meta { display: flex; gap: 10px; flex-wrap: wrap; font-size: 12px; color: var(--muted); }
.card-actions { display: flex; gap: 6px; flex-wrap: wrap; }
.hash { color: #6f7f92; }
.urlfield, .card input, .modal-card input, .modal-card select {
  width: 100%; background: var(--panel-2); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 6px 8px; font-size: 12px;
}
.card label, .modal-card label { display: grid; gap: 4px; font-size: 12px; color: var(--muted); }
/* A checkbox sits BEFORE its words on one line — the stacked field layout
   above is for text inputs. Declared after it so it wins inside cards and
   dialogs (same specificity, later rule). */
.card label.inline, .modal-card label.inline {
  display: flex; flex-direction: row; align-items: center; gap: 7px; color: var(--text);
}
.card label.inline input, .modal-card label.inline input { width: auto; margin: 0; flex: none; }
.two { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
/* A checkbox beside a select/input field: line it up with the control, not the field's caption. */
.two > label.inline { align-self: end; padding-bottom: 6px; }

/* enhancement: region select + adjust */
.selwrap {
  position: relative; aspect-ratio: 16/9; background: #000; overflow: hidden;
  border: 1px solid var(--line); border-radius: var(--radius);
  cursor: crosshair; touch-action: none;
}
.selwrap img { width: 100%; height: 100%; object-fit: cover; display: block; }
.selbox {
  position: absolute; border: 1px solid var(--accent);
  background: rgba(77,163,255,.16); box-shadow: 0 0 0 9999px rgba(0,0,0,.45);
  pointer-events: none;
}
.adjcanvaswrap {
  background: #000; border: 1px solid var(--line); border-radius: var(--radius);
  overflow: auto; max-height: 46vh; display: grid; place-items: center;
}
.adjcanvaswrap canvas {
  max-width: 100%; image-rendering: pixelated;   /* show the actual pixels, not a browser guess */
}
.adjgrid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px 12px; }
.adjgrid label { font-size: 12px; }
.adjgrid input[type=range] { width: 100%; }
.adjgrid output { font-family: ui-monospace, monospace; color: var(--text); }
.stillimg {
  width: 100%; max-height: 260px; object-fit: contain; background: #000;
  border: 1px solid var(--line); border-radius: var(--radius); image-rendering: pixelated;
}

.refslot { display: grid; gap: 6px; align-content: start; }
.refimg {
  width: 100%; aspect-ratio: 16/9; object-fit: cover;
  border: 1px solid var(--line); border-radius: var(--radius); background: #000;
}

/* watch zone grid editor */
.zonewrap {
  position: relative; aspect-ratio: 16 / 9; background: #000;
  border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden;
  touch-action: none;   /* drag-paint must not scroll the page */
}
.zonewrap img { width: 100%; height: 100%; object-fit: cover; display: block; }
.zonegrid {
  position: absolute; inset: 0;
  display: grid; grid-template-columns: repeat(32, 1fr); grid-template-rows: repeat(18, 1fr);
}
.zonegrid i {
  border: 1px solid rgba(255,255,255,.06); cursor: crosshair;
}
.zonegrid i.on { background: rgba(77,163,255,.34); border-color: rgba(77,163,255,.6); }
/* Cells already in this camera's other zones: visible, not paintable. */
.zonegrid i.taken { background: rgba(240,192,74,.22); border-color: rgba(240,192,74,.45); cursor: not-allowed; }
.zonegrid i:hover { border-color: var(--accent); }

/* connection test */
.probe { display: grid; gap: 7px; margin-top: 4px;
         border-top: 1px solid var(--line); padding-top: 9px; }
.probe table.log { margin: 0; }
.probe table.log th { width: 96px; text-transform: capitalize; }
.probe details summary { cursor: pointer; color: var(--muted); font-size: 12px; }
.probe .hash { word-break: break-all; }
.chip.openport { background: #10281a; border-color: #1d5233; color: #6fd48a;
                 font-family: ui-monospace, monospace; }
.chip.shutport { opacity: .4; font-family: ui-monospace, monospace; }
.clockbox { border-radius: var(--radius); padding: 9px 11px; display: grid; gap: 6px; }
.clockbox.good { background: #10281a; border: 1px solid #1d5233; }
.clockbox.bad  { background: #2a2010; border: 1px solid #5a4416; }
.clockmsg { margin: 0; font-size: 12.5px; color: #e8c46a; }
.clockbox.good .clockmsg { color: #8fd9a5; }

.probe-ok   { color: #6fd48a; font-size: 13px; }
.probe-warn { color: #ff9a9a; font-size: 13px; }

.sun { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-top: 10px; font-size: 13px; }
.caps { display: grid; gap: 6px; margin-top: 8px; }
.cap { display: grid; grid-template-columns: 16px 1fr; gap: 8px; font-size: 12.5px; align-items: start; }
.cap.yes > span { color: var(--ok); }
.cap.no  > span { color: var(--err); }

/* ---------- full-res frame viewer -------------------------------------- */
.frameviewer {
  position: fixed; inset: 0; z-index: 60; background: rgba(0,0,0,.9);
  display: grid; grid-template-rows: auto 1fr auto;
}
.fv-bar {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 12px; background: var(--panel); border-bottom: 1px solid var(--line);
}
.fv-meta { font-size: 13px; color: var(--text); }
.fv-spacer { flex: 1; }
.fv-zoom { font-size: 12px; color: var(--muted); min-width: 46px; text-align: right; }
#frameDownload { text-decoration: none; }
.fv-stage {
  position: relative; overflow: hidden; display: grid; place-items: center;
  cursor: grab; touch-action: none; user-select: none;
}
.fv-stage.grabbing { cursor: grabbing; }
#frameImg {
  max-width: 100%; max-height: 100%; transform-origin: center center;
  will-change: transform; image-rendering: auto;
}
.fv-status { position: absolute; color: var(--muted); font-size: 13px; }
.fv-hint { padding: 6px 12px; font-size: 11px; color: var(--muted); text-align: center;
           background: var(--panel); border-top: 1px solid var(--line); }

/* ---------- modal / toast ---------------------------------------------- */

.modal {
  position: fixed; inset: 0; background: rgba(0,0,0,.6);
  display: grid; place-items: center; z-index: 50; padding: 20px;
}
.modal-card {
  background: var(--panel); border: 1px solid var(--line); border-radius: 10px;
  padding: 20px 22px; width: min(560px, 96vw); max-height: 86vh; overflow: auto;
  display: flex; flex-direction: column; gap: 12px; box-shadow: 0 20px 60px rgba(0,0,0,.6);
}
/* Nothing in a dialog squeezes to fit (a clipped image box would): a tall one scrolls. */
.modal-card > * { flex-shrink: 0; }
.modal-card h2 { margin: 0; font-size: 16px; }
.modal-card h2 ~ h2 { margin-top: 8px; }
.modal-card p { margin: 0; font-size: 13px; }
.modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 4px; }
/* A dialog's own buttons sit under a rule, pinned to the foot of the card:
   they read as belonging to the whole dialog, not to its last section, and
   stay in reach when the dialog is taller than the screen. */
.modal-card:has(> .modal-actions:last-child) { padding-bottom: 0; }
.modal-card > .modal-actions:last-child {
  position: sticky; bottom: 0; z-index: 2;
  margin: 4px -22px 0; padding: 12px 22px 16px;
  background: var(--panel); border-top: 1px solid var(--line);
}
/* A dialog section: a titled box, so which part a control belongs to is plain. */
.msec {
  display: grid; gap: 10px;
  border: 1px solid var(--line); border-radius: 8px; padding: 12px 14px 14px;
}
.msec > h3 {
  margin: 0; font-size: 11px; font-weight: 600; letter-spacing: .06em;
  text-transform: uppercase; color: var(--muted);
}
.zrows { display: grid; gap: 6px; }
.zrow { display: flex; align-items: center; gap: 8px; font-size: 12px; }
.zrow-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.log { width: 100%; border-collapse: collapse; font-size: 12px; }
/* The activity log viewer. */
.logbar { display: flex; gap: 8px; flex-wrap: wrap; }
.logbar input[type=search] { flex: 1 1 220px; }
.logbar select { width: auto; }
.logwrap { max-height: 60vh; overflow: auto; border: 1px solid var(--line); border-radius: 6px; }
.lglog thead th { position: sticky; top: 0; background: var(--panel-2); z-index: 1; }
.lglog .lgt { white-space: nowrap; color: var(--muted); font-family: ui-monospace, monospace; }
.lglog .lgrow { cursor: pointer; }
.lglog .lgrow:hover td { background: var(--panel-2); }
.lglog .lv-warn td:first-child { box-shadow: inset 3px 0 0 var(--warn); }
.lglog .lv-error td:first-child { box-shadow: inset 3px 0 0 var(--err); }
.lglog .lv-error b { color: var(--err); }
.lgdetail td { background: #0d1117; }
.lgfacts { display: flex; flex-wrap: wrap; gap: 4px 14px; font-size: 11.5px; color: var(--muted); margin-bottom: 6px; }
.lgfacts b { color: var(--text); font-weight: 600; }
.lgdetail pre { margin: 0; max-height: 320px; overflow: auto; white-space: pre-wrap; word-break: break-word; font-size: 11px; }
.log th, .log td { text-align: left; padding: 4px 8px 4px 0; border-bottom: 1px solid #1a1f27; vertical-align: top; }
.log th { color: var(--muted); font-weight: 500; }

.toast {
  position: fixed; left: 50%; bottom: 22px; transform: translateX(-50%);
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 8px;
  padding: 9px 16px; z-index: 60; font-size: 13px; max-width: 80vw;
  box-shadow: 0 8px 30px rgba(0,0,0,.55);
}
.toast.ok  { border-color: #1d5233; background: #10281a; }
.toast.err { border-color: #6b2429; background: #2c1416; }

@media (max-width: 1000px) {
  /* Narrow screens stack the stage over the side panel and let the PAGE
     scroll when it must (a landscape phone cannot fit both). The video area
     takes an explicit 16:9 of the width — its videos no longer size it — and
     the side panel gets whatever the viewport has left, never under 300px. */
  /* The body grows with its content and the VIEWPORT scrolls (not an
     overflow:auto body, which would keep a phone's address bar from hiding). */
  body { height: auto; min-height: 100%; overflow: visible; }
  .layout {
    flex: none;
    grid-template-columns: 1fr;
    grid-template-rows: auto max(300px, calc(100vh - 56.25vw - 180px));
  }
  .stage { min-height: auto; }
  .video-area { flex: none; height: 56.25vw; }
  .side { border-left: 0; border-top: 1px solid var(--line); }
  /* The full-screen toggle sits at the right end of the TITLE line (the one
     with the site name) rather than in the wrapped control rows; the brand
     row keeps clear of it so a long name or the badge cannot run under it. */
  .topbar { position: relative; overflow-x: clip; }   /* nothing in the bar may widen the page */
  .topbar .brand { padding-right: 52px; }
  #btnFull { position: absolute; top: 8px; right: 12px; }
  /* LIVE · date · presets cannot shrink below their words: let them wrap. */
  .topbar-mid { flex-wrap: wrap; }
  /* Six tabs at phone width: the strip scrolls sideways rather than pushing
     the page wider (a swipe reaches the rest). */
  .tabs { overflow-x: auto; scrollbar-width: none; }
  .tabs::-webkit-scrollbar { display: none; }
  /* Transport goes icon-only (±10s keeps its text — it is the icon), with
     finger-sized targets; there is no Shift key to drag with, so no hint. */
  .transport .lbl, .transport .sel-hint { display: none; }
  .transport .tbtn, .transport .tbtn.wide { min-width: 40px; padding: 7px 10px; font-size: 15px; }
  .transport .tbtn.play { min-width: 44px; }
  .topbar { flex-wrap: wrap; }
}

/* ---------- TV mode (tv.php) ---------------------------------------------
   A camera wall on a television, driven by its remote: the grid and a
   scrubber, nothing else. The top bar keeps the site and who is signed in.
   Rules are body.tv-scoped and win over the narrow-screen layout too, because
   a TV browser can report a small CSS width (a 1920 panel at 2x is 960). */
body.tv .side,
body.tv .topbar-mid,
body.tv #driverBadge,
body.tv .topbar-right > :not(#whoami),
body.tv #btnSheet, body.tv #btnEnhance, body.tv #btnFrame,
body.tv #btnPrevEvent, body.tv #btnNextEvent, body.tv #rate,
body.tv .selection-box, body.tv .sel-hint, body.tv .tlzoom, body.tv .tltip { display: none !important; }
body.tv { height: 100%; overflow: hidden; cursor: none; }
body.tv .layout { display: flex; flex: 1; min-height: 0; grid-template-columns: none; grid-template-rows: none; }
body.tv .stage { flex: 1; min-height: 0; }
body.tv .video-area { flex: 1; height: auto; min-height: 0; }
body.tv .topbar { justify-content: space-between; }
body.tv #whoami { pointer-events: none; }
body.tv .transport { justify-content: center; }
body.tv .timeline-wrap { height: 92px; }
/* Where the remote is: the active pane (grid) or the scrubber. Thick and
   bright -- it is read from across a room. */
/* Drawn on its own top layer: the pane's overlays (no signal, the picture)
   would otherwise paint over an outline. */
body.tv .pane.focus { outline: none; }
body.tv .pane.focus::after { content: ''; position: absolute; inset: 0; z-index: 9; pointer-events: none;
  border: 5px solid var(--accent); }
body.tv.tv-scrub .pane.focus::after { border-color: #3d6b99; border-width: 3px; }
body.tv.tv-scrub .transport,
body.tv.tv-scrub .timeline-wrap { box-shadow: inset 0 0 0 4px var(--accent); }
body.tv .ctxmenu button:focus { outline: 3px solid var(--accent); outline-offset: -3px; background: #1d2430; }
body.tv .ctxmenu { font-size: 18px; }
/* The remote's key help, shown briefly on load and on demand (the Info / ? key). */
.tvhelp { position: fixed; left: 50%; bottom: 150px; transform: translateX(-50%); z-index: 60;
  background: rgba(17, 21, 27, .94); border: 1px solid var(--line); border-radius: 10px;
  padding: 14px 20px; font-size: 17px; line-height: 1.6; color: var(--text); pointer-events: none; }
.tvhelp b { color: var(--accent); }
.userform label.check { display: flex; align-items: center; gap: 8px; font-size: 12px; }
.userform label.check input { width: auto; }

/* Installed on a phone's home screen (standalone): keep the top bar out of the
   notch and the timeline above the home indicator. In a browser tab the
   browser's own chrome already does this. */
@media (display-mode: standalone) {
  body { padding-top: env(safe-area-inset-top, 0px); padding-bottom: env(safe-area-inset-bottom, 0px); }
}
