:root {
  --bg: #0d0f12;
  --panel: #161a1f;
  --tile: #000;
  --text: #e6e8eb;
  --muted: #8a93a0;
  --accent: #3ba3ff;
  --live: #e5484d;
  --offline: #5b6470;
  /* The camera states the map and its list are coloured by (public/map-cameras.js). */
  --rec-ok: #3fb950; /* online and recording */
  --rec-idle: #e0a030; /* online, recording nothing: a fault of its own, not a healthy camera */
  --rec-alert: #e5484d; /* an alert is open about it */
  --rec-unknown: #6b7480; /* nobody has told us: never painted as well */
  --gap: 4px;
  --tl-track: #1d232b;
  --tl-rec: #2f6f9f;
  --tl-evt: #e0a030;
  --tl-tick: #3a4450;
  --tl-hit: #b36bff;
  --tl-nvr: #3f8a78; /* stretches only the NVR has (server playback plays them from the NVR) */
  --tl-bm: #7cc4ff; /* bookmarks: a colour the timeline uses for nothing else */
  --tl-gap: #d9534f; /* the server's recorder missed this (a thin strip; the reason on hover) */
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  background: var(--bg);
  color: var(--text);
  font: 14px/1.4 system-ui, -apple-system, "Segoe UI", sans-serif;
}

body { display: flex; flex-direction: column; }

header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 8px 16px;
  background: var(--panel);
  flex-wrap: wrap;
}

h1 { margin: 0; font-size: 16px; font-weight: 600; }

.controls { display: flex; align-items: center; gap: 8px; }
/* narrow screens (phones): the header's controls wrap, so the pager arrows stay on screen */
@media (max-width: 700px), (pointer: coarse) and (max-height: 500px) { .controls { flex-wrap: wrap; } }

select, button {
  background: var(--surface-2);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 4px 10px;
  font: inherit;
  cursor: pointer;
}
button:disabled { opacity: 0.4; cursor: default; }
label { color: var(--muted); display: flex; gap: 6px; align-items: center; }
#page { color: var(--muted); min-width: 48px; text-align: center; }

#grid {
  flex: 1;
  display: grid;
  gap: var(--gap);
  padding: var(--gap);
  min-height: 0;
}

.tile {
  position: relative;
  background: var(--tile);
  border-radius: 4px;
  overflow: hidden;
  min-height: 0;
  cursor: pointer;
}
.tile canvas { width: 100%; height: 100%; object-fit: contain; display: block; }

.tile .label {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  display: flex;
  justify-content: space-between;
  gap: 8px;
  padding: 4px 8px;
  background: linear-gradient(transparent, rgba(0, 0, 0, 0.75));
  font-size: 12px;
  pointer-events: none;
}
.tile .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* the state badge, top left of every picture: a red LIVE while frames arrive, otherwise what it is doing */
.tile .status {
  position: absolute; top: 8px; left: 8px; z-index: 3; pointer-events: none;
  padding: 3px 8px; border-radius: 999px; white-space: nowrap;
  font-size: 11px; font-weight: 700; letter-spacing: .04em; line-height: 1.3; color: #fff;
  background: rgba(20, 22, 26, .55); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
}
.tile .status:empty { display: none; }
.tile .status.live { background: rgba(220, 38, 38, .88); }
.single-overlay > .status { z-index: 4; }
.tile .status.live::before {
  content: ""; background: #fff !important;
  display: inline-block;
  width: 7px; height: 7px;
  margin-right: 5px;
  border-radius: 50%;
  background: var(--live);
  vertical-align: 1px;
}
.tile.offline { cursor: default; }
/* readable muted grey, not the faint --offline (~3:1 on the dark pill, under AA for this small text);
   the offline state still reads from the word itself, the dimmed tile and the dot. */
.tile.offline .status { color: var(--muted); }
.tile.single { cursor: zoom-out; }

#notice {
  margin: 0;
  padding: 8px 16px;
  background: #3a2a10;
  color: #ffcf87;
}
/* A benign "no cameras yet / none shared with you" message is not a fault: neutral, not amber. */
#notice.as-info { background: transparent; color: var(--muted); border-bottom: 1px solid var(--border); }

.account { display: flex; align-items: center; gap: 8px; color: var(--muted); }

/* ---- sign-in page ---- */
.login-page { align-items: center; justify-content: center; padding: 16px; }
.login-card {
  width: 100%;
  max-width: 340px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 24px;
  background: var(--panel);
  border-radius: 10px;
}
.login-card h1 { margin-bottom: 8px; font-size: 18px; }
.login-card label { color: var(--muted); font-size: 13px; }
.login-card input {
  background: #0f1216;
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 8px 10px;
  font: inherit;
}
.login-card input:focus, select:focus, button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.login-card button { margin-top: 8px; padding: 8px; background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600; }
#error { margin: 0; color: #ff8a8a; font-size: 13px; }

/* ---- stats overlay (press D) ---- */
.tile .stats {
  display: none;
  position: absolute;
  top: 34px; left: 4px;
  margin: 0;
  padding: 4px 6px;
  background: rgba(0, 0, 0, 0.7);
  border-radius: 4px;
  font: 11px/1.35 ui-monospace, Consolas, monospace;
  color: #b9f5c9;
  pointer-events: none;
}
.show-stats .tile .stats { display: block; }

/* ---- header tabs ---- */
.brand { display: flex; align-items: center; gap: 16px; }
/* Ten tabs no longer fit a narrow window, and a flex row simply pushes the last ones off the edge
   with no sign they are there -- on a laptop that silently loses Sites and Settings, which are the
   only way to reach those pages. It scrolls sideways instead, and each tab keeps its full label
   rather than being squeezed to an unreadable stub. */
.tabs { display: flex; gap: 4px; overflow-x: auto; scrollbar-width: thin; }
.tabs a {
  flex: 0 0 auto;
  color: var(--muted);
  text-decoration: none;
  padding: 4px 10px;
  border-radius: 6px;
}
.tabs a:hover { color: var(--text); background: var(--surface-2); }
.tabs a[aria-current="page"] { color: var(--text); background: var(--surface-2); }
.tile .pb-link {
  pointer-events: auto;
  color: var(--accent);
  text-decoration: none;
  margin-left: 8px;
}

/* ---- playback page ---- */
.pb-main { flex: 1; min-height: 0; padding: var(--gap); display: flex; }
.pb-video { flex: 1; cursor: default; }
.pb-message {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  text-align: center;
  color: var(--muted);
  background: rgba(0, 0, 0, 0.55);
}
.pb-message[hidden] { display: none; }
.show-stats .pb-video .stats { display: block; }
.pb-bar { background: var(--panel); padding: 8px 16px 10px; display: flex; flex-direction: column; gap: 6px; }
.pb-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.pb-play { min-width: 44px; font-size: 16px; }

/* ---- the when row: date, clock, span, zoom presets, follow ---- */
.pb-date { color: var(--muted); font-variant-numeric: tabular-nums; }
.pb-clock { font-size: 20px; font-variant-numeric: tabular-nums; color: var(--text); padding: 2px 10px; }
.pb-clock-input {
  width: 9.5em;
  background: var(--surface-2);
  color: var(--text);
  border: 1px solid var(--accent);
  border-radius: 6px;
  padding: 3px 8px;
  font: 20px/1.2 inherit;
  font-variant-numeric: tabular-nums;
}
.pb-clock-input[hidden] { display: none; }
.pb-span { color: var(--muted); min-width: 4em; }
.pb-zoom { display: flex; gap: 2px; }
.pb-zoom button { padding: 3px 8px; font-size: 12px; }
.pb-zoom button[aria-pressed="true"], .pb-speeds button[aria-pressed="true"],
#followBtn[aria-pressed="true"] { border-color: var(--accent); color: var(--accent); }

/* ---- the overview strip: the whole day, with the zoomed part marked ---- */
.pb-overview {
  position: relative;
  height: 14px;
  background: var(--tl-track);
  border-radius: 3px;
  overflow: hidden;
  touch-action: none;
  cursor: pointer;
}
.pb-ov-boxes { position: absolute; inset: 0; }
.pb-ov-boxes div { position: absolute; top: 0; bottom: 0; background: var(--tl-rec); }
.pb-ov-now { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--muted); }
.pb-ov-window {
  position: absolute;
  top: 0; bottom: 0;
  background: rgba(59, 163, 255, 0.18);
  border: 1px solid var(--accent);
  border-radius: 3px;
  cursor: grab;
}
.pb-ov-window:active { cursor: grabbing; }

/* ---- the timeline: ordinary elements, not a canvas, so each box can carry a title and a click ---- */
.pb-timeline {
  position: relative;
  height: 62px;
  background: var(--tl-track);
  border-radius: 4px;
  overflow: hidden;
  touch-action: none;
  cursor: pointer;
  user-select: none;
}
.pb-lane { position: absolute; left: 0; right: 0; }
.pb-lane-rec { top: 4px; height: 20px; }
.pb-lane-motion { top: 28px; height: 30px; }
.pb-lane div { position: absolute; top: 0; bottom: 0; border-radius: 2px; }
.pb-lane-rec div[data-kind="server"], .pb-lane-rec div[data-kind="rec"] { background: var(--tl-rec); }
/* NVR stretches are striped as well as coloured, so they read without relying on colour */
.pb-lane-rec div[data-kind="nvr"] {
  background: repeating-linear-gradient(45deg, var(--tl-nvr) 0 5px, #2b6559 5px 10px);
}
.pb-lane-rec div[data-kind="gap"] { background: var(--tl-gap); opacity: 0.85; }
.pb-lane-motion div[data-kind="event"] { background: var(--tl-evt); cursor: pointer; }
.pb-lane-motion div[data-kind="hit"] { background: var(--tl-hit); top: 0; bottom: 50%; cursor: pointer; }
.pb-now { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--muted); }
.pb-hoverline { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--text); pointer-events: none; }
.pb-hoverlabel {
  position: absolute;
  top: 2px;
  padding: 1px 5px;
  border-radius: 3px;
  background: var(--panel);
  color: var(--text);
  font-size: 11px;
  white-space: nowrap;
  pointer-events: none;
}
.pb-playhead { position: absolute; top: 0; bottom: 0; width: 2px; margin-left: -1px; background: var(--accent); pointer-events: none; }
.pb-playhead::before {
  content: "";
  position: absolute;
  top: 0; left: -4px;
  border-left: 5px solid transparent;
  border-right: 5px solid transparent;
  border-top: 6px solid var(--accent);
}
.pb-now[hidden], .pb-playhead[hidden], .pb-hoverline[hidden], .pb-hoverlabel[hidden] { display: none; }
.pb-ticks { position: relative; height: 14px; color: var(--muted); font-size: 11px; }
.pb-ticks span { position: absolute; top: 0; transform: translateX(-50%); font-variant-numeric: tabular-nums; }

/* ---- controls: speed left, transport centred, tools right ---- */
.pb-controls { gap: 12px; }
.pb-speeds { display: flex; gap: 2px; flex: 1; min-width: 0; flex-wrap: wrap; }
.pb-speeds button { padding: 3px 7px; font-size: 12px; }
.pb-transport { display: flex; gap: 6px; align-items: center; }
.pb-tools { display: flex; gap: 6px; align-items: center; flex: 1; min-width: 0; justify-content: flex-end; flex-wrap: wrap; }

/* ---- the shuttle ---- */
.pb-shuttle-row { display: flex; align-items: center; justify-content: center; gap: 10px; }
.pb-shuttle { width: min(420px, 60%); accent-color: var(--accent); }
.pb-shuttle-label { color: var(--muted); min-width: 5em; font-variant-numeric: tabular-nums; }

/* ---- spinner while seeking, switching camera or changing quality ---- */
.pb-spinner {
  position: absolute;
  top: 50%; left: 50%;
  width: 34px; height: 34px;
  margin: -17px 0 0 -17px;
  border: 3px solid rgba(255, 255, 255, 0.2);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: pb-spin 0.9s linear infinite;
  pointer-events: none;
}
.pb-spinner[hidden] { display: none; }
@keyframes pb-spin { to { transform: rotate(360deg); } }

.pb-shortcuts { background: var(--panel); color: var(--text); border: 1px solid var(--border); border-radius: 8px; }
.pb-shortcuts h2 { margin: 0 0 8px; font-size: 15px; }
.pb-shortcuts dl { display: grid; grid-template-columns: auto 1fr; gap: 4px 16px; margin: 0 0 12px; }
.pb-shortcuts dt { color: var(--accent); font-variant-numeric: tabular-nums; }
.pb-shortcuts dd { margin: 0; color: var(--muted); }
.pb-shortcuts-btn { padding: 2px 8px; font-size: 12px; }
.pb-legend { display: flex; align-items: center; gap: 6px; color: var(--muted); font-size: 12px; flex-wrap: wrap; }
.pb-legend span[class^="lg-"] { display: inline-block; width: 12px; height: 8px; border-radius: 2px; margin-left: 8px; }
.lg-rec { background: var(--tl-rec); }
.lg-evt { background: var(--tl-evt); }
.lg-nvr { background: var(--tl-nvr); }
.pb-legend span.lg-gap { background: var(--tl-gap); height: 3px; }
.pb-legend-server[hidden], .pb-skew[hidden] { display: none; }
.pb-skew { margin-left: 8px; color: var(--tl-evt); }
.pb-hint { margin-left: auto; }
/* server playback: dragging the playhead scrubs */
.pb-timeline.on-playhead, .pb-timeline.scrubbing { cursor: ew-resize; }
.pb-badge {
  position: absolute;
  top: 6px; right: 6px;
  padding: 2px 8px;
  border-radius: 10px;
  background: var(--tl-nvr);
  color: #fff;
  font-size: 12px;
  pointer-events: none;
}
.pb-notice {
  position: absolute;
  top: 8px; left: 50%;
  transform: translateX(-50%);
  max-width: calc(100% - 140px);
  padding: 4px 12px;
  border-radius: 6px;
  background: rgba(0, 0, 0, 0.72);
  color: var(--text);
  font-size: 13px;
  text-align: center;
  pointer-events: none;
  transition: opacity 0.6s;
}
.pb-notice.fading { opacity: 0; }
.pb-badge[hidden], .pb-notice[hidden] { display: none; }
input[type="date"] {
  background: var(--surface-2);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 3px 8px;
  font: inherit;
  color-scheme: light dark;
}
@media (max-width: 640px) {
  .pb-hint { display: none; }
  .pb-bar { padding: 6px 8px 8px; }
  /* on a phone the transport is what the thumb wants first, so it comes before speeds and tools */
  .pb-controls { flex-direction: column; align-items: stretch; gap: 6px; }
  .pb-transport { order: 1; justify-content: center; }
  .pb-speeds { order: 2; justify-content: center; }
  .pb-tools { order: 3; justify-content: center; }
  .pb-shuttle { width: 100%; }
  .pb-when { gap: 6px; }
  .pb-clock { font-size: 17px; }
}

/* dense grids: smaller labels so they don't cover the picture */
#grid[data-size="5"] .tile .label,
#grid[data-size="6"] .tile .label { font-size: 11px; padding: 2px 6px; }
#grid[data-size="8"] .tile .label { font-size: 10px; padding: 1px 4px; }
#grid[data-size="8"] { --gap: 2px; }
label input[type="checkbox"] { accent-color: var(--accent); margin: 0; }

/* ---- motion search ---- */
.pb-main { gap: var(--gap); }
.pb-box-layer { position: absolute; inset: 0; cursor: crosshair; touch-action: none; }
.pb-box {
  position: absolute;
  border: 2px solid var(--tl-hit);
  background: rgba(179, 107, 255, 0.12);
  box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.35);
  pointer-events: none;
}
.pb-video.box-kept .pb-box-layer { cursor: default; pointer-events: none; }
.pb-search {
  width: 280px;
  flex: none;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 12px;
  background: var(--panel);
  border-radius: 4px;
  overflow-y: auto;
}
/* display: flex above would otherwise beat the hidden attribute and leave the panel always open */
.pb-search[hidden] { display: none; }
.pb-search h2 { margin: 0; font-size: 14px; }
.pb-search label { flex-direction: column; align-items: stretch; gap: 4px; font-size: 12px; }
.pb-search-help, .pb-search-status { margin: 0; color: var(--muted); font-size: 12px; }
.pb-search-actions { display: flex; gap: 6px; }
.pb-primary { background: var(--tl-hit); border-color: var(--tl-hit); color: #fff; font-weight: 600; }
.pb-primary:disabled { background: var(--surface-2); border-color: var(--border); color: var(--muted); }
.pb-progress { height: 4px; background: var(--tl-track); border-radius: 2px; overflow: hidden; }
.pb-progress div { height: 100%; width: 0; background: var(--tl-hit); transition: width 0.3s; }
.pb-hits { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.pb-hits button {
  width: 100%;
  display: flex;
  justify-content: space-between;
  text-align: left;
  font-variant-numeric: tabular-nums;
}
.pb-hits button span { color: var(--muted); }
.lg-hit { background: var(--tl-hit); }
/* rotated, so the legend shows the same diamond the timeline draws */
.lg-bm { background: var(--tl-bm); transform: rotate(45deg); }
#searchToggle[aria-pressed="true"] { border-color: var(--tl-hit); color: var(--tl-hit); }
@media (max-width: 800px) {
  .pb-main { flex-direction: column; }
  .pb-search, .pb-bookmarks { width: auto; max-height: 40vh; }
}

/* ---- sites page ---- */
.tabs a[hidden] { display: none; }
.st-primary { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600; }
.st-main { flex: 1; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 20px; max-width: 1100px; width: 100%; margin: 0 auto; }
.st-notice { margin: 0; padding: 10px 14px; background: var(--warn-soft); color: var(--text); border-left: 3px solid var(--warn); border-radius: 6px; }
.st-empty { color: var(--muted); }
.st-site-head { display: flex; align-items: baseline; gap: 12px; margin-bottom: 8px; }
.st-site-head h2 { margin: 0; font-size: 16px; }
.st-link { background: none; border: none; color: var(--accent); padding: 0; font-size: 13px; }
.st-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 10px; }
.st-card { background: var(--panel); border-radius: 8px; padding: 12px 14px; display: flex; flex-direction: column; gap: 6px; }
.st-card-head { display: flex; justify-content: space-between; align-items: center; gap: 8px; }
.st-card-head h3 { margin: 0; font-size: 15px; }
.st-meta { margin: 0; color: var(--muted); font-size: 13px; }
.st-error-text { margin: 0; color: #ff8a8a; font-size: 13px; }
.st-status { font-size: 12px; white-space: nowrap; }
.st-status::before { content: ""; display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; vertical-align: 0; background: var(--muted); }
.st-online::before { background: #3fb950; }
.st-offline::before { background: var(--live); }
.st-connecting::before { background: #d29922; }
.st-card-actions { display: flex; gap: 6px; margin-top: 4px; }
.st-danger { color: #ff8a8a; }
.st-dialog {
  background: var(--panel);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 20px;
  width: min(420px, calc(100vw - 32px));
}
.st-dialog::backdrop { background: rgba(0, 0, 0, 0.6); }
.st-dialog h2 { margin: 0 0 12px; font-size: 16px; }
.st-dialog form { display: flex; flex-direction: column; gap: 10px; }
.st-dialog label { flex-direction: column; align-items: stretch; gap: 4px; font-size: 13px; }
.st-dialog input:not([type="checkbox"]) {
  background: #0f1216;
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 7px 10px;
  font: inherit;
}
.st-dialog input:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
.st-dialog small { color: var(--muted); font-size: 12px; }
.st-row { display: flex; gap: 8px; }
.st-grow { flex: 1; }
.st-port { width: 90px; }
.st-dialog .st-check { flex-direction: row; align-items: center; gap: 8px; }
.st-error { margin: 0; color: #ff8a8a; font-size: 13px; }
.st-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 4px; }

/* a tile whose stream the browser can't play */
.tile-msg {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 12px;
  text-align: center;
  font-size: 12px;
  color: var(--muted);
  pointer-events: none;
}
.st-warn-text { margin: 0; color: #ffcf87; font-size: 13px; }
/* the full-size view of a camera reported offline, kept open for its Picture panel (a restart) */
.single-offline {
  position: absolute;
  left: 50%;
  top: 44px;
  transform: translateX(-50%);
  max-width: calc(100% - 32px);
  padding: 6px 10px;
  border-radius: 6px;
  background: rgba(0, 0, 0, 0.72);
  color: #ffcf87;
  font-size: 13px;
  text-align: center;
  pointer-events: none;
  z-index: 3;
}

/* ---- map page ---- */
.map-main { flex: 1; min-height: 0; display: flex; gap: var(--gap); padding: var(--gap); }
.map-view {
  position: relative;
  flex: 1;
  min-width: 0;
  overflow: hidden;
  background: #0a0c0f;
  border-radius: 4px;
  touch-action: none;
  user-select: none;
  cursor: grab;
}
.map-view:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.map-view.panning { cursor: grabbing; }
.map-view.placing, .map-view.placing .cam { cursor: crosshair; }
.map-tiles { position: absolute; inset: 0; }
.map-tile, .map-plan {
  position: absolute;
  left: 0;
  top: 0;
  max-width: none;
  transform-origin: 0 0;
  pointer-events: none;
}
.map-overlay { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; overflow: visible; z-index: 2; }
.map-overlay .cam, .map-overlay .handle { pointer-events: auto; cursor: pointer; }
/* What a camera is doing, in one colour each. "Online but recording nothing" is amber and its own
   colour on purpose: at this site that is a real failure, and painting it the same green as a
   recording camera is how it goes unnoticed. Grey is "we have not been told", never "fine". */
.map-overlay .cone { fill: rgba(59, 163, 255, 0.2); stroke: rgba(59, 163, 255, 0.85); stroke-width: 1.5; }
.map-overlay .cone.st-offline, .map-overlay .cone.st-unknown { fill: rgba(138, 147, 160, 0.16); stroke: rgba(138, 147, 160, 0.7); }
.map-overlay .cone.st-idle { fill: rgba(224, 160, 48, 0.18); stroke: rgba(224, 160, 48, 0.8); }
.map-overlay .cone.st-alert { fill: rgba(229, 72, 77, 0.18); stroke: rgba(229, 72, 77, 0.85); }
.map-overlay .cone.sel { fill: rgba(59, 163, 255, 0.3); stroke-width: 2.5; }
.map-overlay .cam-dot { fill: var(--accent); stroke: #fff; stroke-width: 2; }
.map-overlay .cam.st-recording .cam-dot { fill: var(--rec-ok); }
.map-overlay .cam.st-idle .cam-dot { fill: var(--rec-idle); }
.map-overlay .cam.st-alert .cam-dot { fill: var(--rec-alert); }
.map-overlay .cam.st-offline .cam-dot { fill: var(--offline); }
/* Dashed edge: unknown must not be mistaken for a state we actually checked. */
.map-overlay .cam.st-unknown .cam-dot { fill: var(--rec-unknown); stroke-dasharray: 3 3; }
.map-overlay .cam.cluster .cam-dot { stroke-width: 3; }
.map-overlay .cam.sel .cam-dot { stroke: #ffcf87; stroke-width: 3; }
/* Live lighting. The status colour stays on the dot; these rings sit behind it and say that
   something is happening right now. A brief green pulse for motion; a steady red ring for an alarm
   (or a camera the server cannot reach) that lingers until it clears. */
.map-overlay .cam-pulse { fill: none; stroke: var(--rec-ok); stroke-width: 2.5; transform-box: fill-box; transform-origin: center; animation: map-motion 1.1s ease-out 1; pointer-events: none; }
.map-overlay .cam-ring { fill: none; stroke: var(--rec-alert); stroke-width: 3; transform-box: fill-box; transform-origin: center; animation: map-alarm 1.6s ease-in-out infinite; pointer-events: none; }
@keyframes map-motion { from { transform: scale(1); opacity: 0.85; } to { transform: scale(2.4); opacity: 0; } }
@keyframes map-alarm { 0%, 100% { transform: scale(1.3); opacity: 0.4; } 50% { transform: scale(1.65); opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  /* no movement, but the alarm highlight must still be seen */
  .map-overlay .cam-pulse { animation: none; opacity: 0; }
  .map-overlay .cam-ring { animation: none; opacity: 0.9; transform: scale(1.5); }
}
.map-overlay text { fill: #fff; pointer-events: none; dominant-baseline: central; }
.map-overlay .cam-num { font: 600 11px system-ui, sans-serif; text-anchor: middle; }
.map-overlay .cam-name {
  font: 500 12px system-ui, sans-serif;
  paint-order: stroke;
  stroke: rgba(0, 0, 0, 0.85);
  stroke-width: 3px;
  stroke-linejoin: round;
}
.map-overlay .handle { fill: #fff; stroke: var(--accent); stroke-width: 2; cursor: grab; }
.map-overlay .handle.edge { fill: var(--accent); stroke: #fff; }
/* A whole site seen from too far away to tell its cameras apart: one pill with its name and camera
   count. The dot is the worst state among its cameras, in the markers' own colours, and the
   offline count is in the alert colour so a site with a camera down is seen from any distance. */
.map-overlay .site-badge { pointer-events: auto; cursor: pointer; }
.map-overlay .site-badge-pill { fill: #14181d; fill-opacity: 0.94; stroke: #fff; stroke-width: 1.5; }
.map-overlay .site-badge:hover .site-badge-pill { stroke: var(--accent); stroke-width: 2.5; }
.map-overlay .site-badge-text { font: 600 12px system-ui, sans-serif; }
.map-overlay .site-badge-off { fill: var(--rec-alert); }
.map-overlay .site-badge-dot { fill: var(--rec-unknown); }
.map-overlay .site-badge.st-recording .site-badge-dot { fill: var(--rec-ok); }
.map-overlay .site-badge.st-idle .site-badge-dot { fill: var(--rec-idle); }
.map-overlay .site-badge.st-alert .site-badge-dot, .map-overlay .site-badge.st-offline .site-badge-dot { fill: var(--rec-alert); }
.map-overlay .site-badge.st-offline .site-badge-pill { stroke: var(--rec-alert); }
/* Edit mode: where each site has been placed. Not a handle: a site is moved from the panel. */
.map-overlay .site-pin-mark { fill: none; stroke: #fff; stroke-width: 2; stroke-dasharray: 4 3; }
.map-overlay .site-pin.sel .site-pin-mark { stroke: #ffcf87; }
.map-overlay .site-pin-name { font: 600 11px system-ui, sans-serif; text-anchor: middle; paint-order: stroke; stroke: rgba(0, 0, 0, 0.75); stroke-width: 3px; }
.map-zoom { position: absolute; top: 8px; right: 8px; display: flex; flex-direction: column; gap: 4px; z-index: 3; }
.map-zoom button { width: 34px; height: 34px; padding: 0; font-size: 17px; line-height: 1; }
.map-credit {
  position: absolute;
  right: 0;
  bottom: 0;
  z-index: 3;
  padding: 2px 6px;
  font-size: 11px;
  color: #c9cfd6;
  background: rgba(0, 0, 0, 0.6);
  border-top-left-radius: 4px;
}
.map-credit a { color: inherit; }
.map-credit[hidden] { display: none; }
/* A notice over the map (nothing placed yet). Only the card is solid and takes the pointer: the
   map underneath is a live map and must still drag, and the zoom buttons must still press. */
.map-empty {
  position: absolute;
  inset: 0;
  z-index: 3;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  pointer-events: none;
}
.map-empty[hidden] { display: none; }
.map-empty-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  max-width: 360px;
  padding: 16px 20px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45);
  color: var(--text);
  text-align: center;
  cursor: default;
  pointer-events: auto;
}
.map-empty p { margin: 0; }
.map-empty p + p { color: var(--muted); font-size: 13px; }
.map-live {
  position: absolute;
  right: 12px;
  bottom: 28px;
  z-index: 4;
  width: min(440px, calc(100% - 24px));
  max-height: calc(100% - 40px); /* a short map: the video shrinks, the header stays visible */
  display: flex;
  flex-direction: column;
  background: var(--panel);
  border-radius: 6px;
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.55);
  overflow: hidden;
  cursor: default;
}
.map-live[hidden] { display: none; }
.map-live-head { display: flex; align-items: center; gap: 10px; padding: 6px 6px 6px 10px; font-size: 13px; }
.map-live-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.map-live-head a { color: var(--accent); text-decoration: none; }
.map-live-close { padding: 0 9px; font-size: 16px; line-height: 24px; }
.map-live .tile { aspect-ratio: 16 / 9; flex: 0 1 auto; min-height: 0; border-radius: 0; cursor: default; }
.map-side {
  width: 300px;
  flex: none;
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 12px;
  overflow-y: auto;
  background: var(--panel);
  border-radius: 4px;
}
.map-side h2 { margin: 0; font-size: 14px; }
.map-side section { display: flex; flex-direction: column; gap: 8px; }
.map-side label { flex-direction: column; align-items: stretch; gap: 4px; font-size: 12px; }
.map-side input[type="text"], .map-side input[type="number"] {
  width: 100%;
  min-width: 0;
  background: #0f1216;
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 6px 8px;
  font: inherit;
}
.map-side input[type="range"] { accent-color: var(--accent); width: 100%; }
.map-side input:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
.map-field { display: flex; justify-content: space-between; }
.map-field output { color: var(--text); font-variant-numeric: tabular-nums; }
.map-find { display: flex; align-items: flex-end; gap: 6px; }
.map-find label { flex: 1; min-width: 0; }
.map-help { margin: 0; color: var(--muted); font-size: 12px; }
.map-lens { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.map-lens-lbl { color: var(--muted); font-size: 12px; margin-right: 2px; }
.map-lens-btn { padding: 2px 8px; font-size: 12px; border-radius: 6px; border: 1px solid var(--border); background: transparent; color: var(--text); cursor: pointer; font-variant-numeric: tabular-nums; }
.map-lens-btn:hover { border-color: var(--accent); }
.map-note { color: var(--muted); font-size: 12px; white-space: nowrap; }
.map-cams { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.map-cams li { display: flex; align-items: center; gap: 8px; padding: 4px 6px; border-radius: 4px; font-size: 13px; }
.map-cams li.placed { cursor: pointer; }
.map-cams li.placed:hover, .map-cams li.sel { background: var(--surface-2); }
.map-cams li[draggable="true"] { cursor: grab; }
.map-cams li.unplaced .map-cam-name { color: var(--muted); }
.map-cam-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.map-dot { width: 8px; height: 8px; flex: none; border-radius: 50%; background: var(--rec-unknown); }
.map-dot.off, .map-dot.st-offline { background: var(--offline); }
.map-dot.st-recording { background: var(--rec-ok); }
.map-dot.st-idle { background: var(--rec-idle); }
.map-dot.st-alert { background: var(--rec-alert); }
/* A ring rather than a solid dot: unknown should not read as one of the states we did check. */
.map-dot.st-unknown { background: transparent; box-shadow: inset 0 0 0 2px var(--rec-unknown); }
.map-legend { list-style: none; margin: 0 0 4px; padding: 0; display: flex; flex-wrap: wrap; gap: 4px 12px; font-size: 12px; color: var(--muted); }
/* The cone-colour groups, as a legend for everyone and as a small editor for an admin. */
.map-cone-legend li { display: flex; align-items: center; gap: 6px; }
.map-swatch { width: 12px; height: 12px; flex: none; border-radius: 3px; box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.35); }
.map-groups { list-style: none; margin: 0 0 8px; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.map-groups li { display: flex; align-items: center; gap: 8px; }
.map-groups input[type="color"] { width: 30px; height: 26px; padding: 0; border: 1px solid var(--border); border-radius: 4px; background: none; flex: none; }
.map-group-name { flex: 1; min-width: 0; }
.map-cone-colour { display: flex; flex-direction: column; gap: 4px; }
.map-cone-row { display: flex; align-items: center; gap: 8px; }
.map-cone-select { flex: 1; min-width: 0; }
.map-cone-row input[type="color"] { width: 30px; height: 28px; padding: 0; border: 1px solid var(--border); border-radius: 4px; background: none; flex: none; }
/* The hover snapshot bubble: a small picture above the marker, no pointer of its own. */
.map-hover {
  position: absolute;
  z-index: 4;
  width: 180px;
  transform: translate(-50%, calc(-100% - 10px));
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 6px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
  overflow: hidden;
  pointer-events: none;
}
.map-hover[hidden] { display: none; }
.map-hover-name { font-size: 12px; padding: 4px 8px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--text); }
.map-hover .tile { aspect-ratio: 16 / 9; border-radius: 0; cursor: default; }
.map-legend li { display: flex; align-items: center; gap: 6px; }
.map-live-state { font-size: 12px; color: var(--muted); }
.map-live-state.st-recording { color: var(--rec-ok); }
.map-live-state.st-idle { color: var(--rec-idle); }
.map-live-state.st-alert { color: var(--rec-alert); }
.map-selected { padding-top: 10px; border-top: 1px solid var(--border); }
.map-actions { display: flex; gap: 8px; }
.map-actions button { flex: 1; padding: 6px 10px; }
/* The row of sites above the map. One line that scrolls sideways when the sites do not fit, which
   on a phone is always: wrapping would push the map itself down the page. */
.map-sites {
  flex: none;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: var(--gap) var(--gap) 0;
  overflow-x: auto;
  white-space: nowrap;
  scrollbar-width: thin;
  -webkit-overflow-scrolling: touch;
}
.map-sites:empty { display: none; }
.map-site { flex: none; display: inline-flex; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; background: var(--panel); }
.map-site.sel { border-color: var(--accent); }
.map-sites button { flex: none; height: 32px; padding: 0 12px; font-size: 13px; white-space: nowrap; }
.map-site button { border: 0; border-radius: 0; background: none; }
.map-site button + button { border-left: 1px solid var(--border); }
.map-site button:hover { background: var(--surface-2); }
.map-site button[aria-pressed="true"] { background: var(--accent); color: #fff; font-weight: 600; }
.map-sites button:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.map-site button[aria-pressed="true"]:focus-visible { outline-color: #fff; }
.map-site-go { display: inline-flex; align-items: center; gap: 8px; }
.map-site-off { color: var(--rec-alert); font-weight: 600; }
.map-site button[aria-pressed="true"] .map-site-off { color: #fff; text-decoration: underline; }
.map-site-hint { color: var(--muted); font-size: 12px; font-style: italic; }
.map-site button[aria-pressed="true"] .map-site-hint { color: #fff; }
.map-site-place { color: var(--accent); }
.map-cams li.map-cams-site { padding: 10px 6px 2px; font-size: 12px; font-weight: 600; color: var(--muted); }
@media (max-width: 800px) {
  .map-sites button { height: 44px; } /* a finger, not a mouse pointer */
  .map-main { flex-direction: column; }
  .map-view { min-height: 55vh; }
  .map-side { width: auto; max-height: 45vh; }
}

/* ---- Find NVRs ---- */
.st-discover { width: min(680px, calc(100vw - 32px)); }
.st-help { margin: 0; color: var(--muted); font-size: 13px; }
.st-actions-start { justify-content: flex-start; align-items: center; }
.st-found { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; max-height: 45vh; overflow-y: auto; }
.st-found-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px 10px;
  background: #0f1216;
  border: 1px solid var(--border);
  border-radius: 6px;
}
.st-found-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.st-found-main .st-meta { overflow-wrap: anywhere; }
.st-found-by { color: var(--muted); font-size: 12px; }
.st-found-added { opacity: 0.75; }

/* ---- sub-streams ---- */
.st-subs .st-found-item input[type="checkbox"] { width: 18px; height: 18px; accent-color: var(--accent); flex: none; }
.st-bitrate { border: 1px solid var(--border); border-radius: 6px; padding: 8px 12px; display: flex; flex-direction: column; gap: 6px; margin: 0; }
.st-bitrate legend { color: var(--muted); font-size: 12px; padding: 0 4px; }
.st-dialog .st-bitrate .st-check { font-size: 13px; }
.st-step { font-size: 12px; }
.st-step-done { color: #3fb950; }
.st-step-failed { color: #ff8a8a; }
.st-step-skipped, .st-step-waiting { color: var(--muted); }
.st-step-working { color: #d29922; }

/* ---- full size: main stream layer over the sub stream ---- */
.tile-upgrade { position: absolute; inset: 0; background: var(--tile); }
.tile-upgrade.pending { visibility: hidden; }
.tile-upgrade canvas { width: 100%; height: 100%; object-fit: contain; display: block; }

/* ---- browser full screen of the grid ---- */
#grid:fullscreen { background: #000; padding: 0; --gap: 2px; width: 100vw; height: 100vh; }
#fullscreen { font-size: 15px; line-height: 1; padding: 4px 9px; }
#fullscreen[hidden] { display: none; }

/* ---- full-size view laid over the (suspended) grid ---- */
#grid { position: relative; }
.tile.single-overlay { position: absolute; inset: var(--gap); z-index: 3; }
/* the full-size view zooms (pinch-zoom.js via viewer.js): only the pictures move, not the name,
   the LIVE badge or the buttons; the overlay text (.osd) stays where it is too */
.single-overlay { overflow: hidden; touch-action: none; }
.single-overlay canvas:not(.osd) { transform-origin: 0 0; transform: translate(var(--zx, 0px), var(--zy, 0px)) scale(var(--zs, 1)); }
.single-overlay.zoomed { cursor: grab; }
.single-overlay.zoomed:active { cursor: grabbing; }
.single-overlay > .zoom-badge { position: absolute; top: 8px; left: 50%; transform: translateX(-50%); z-index: 5; padding: 2px 8px; border-radius: 999px; background: rgba(20, 22, 26, .6); color: #fff; font-size: 12px; font-variant-numeric: tabular-nums; pointer-events: none; }
.single-overlay > .zoom-badge[hidden] { display: none; }
body.phone-full .single-overlay > .zoom-badge { top: calc(8px + env(safe-area-inset-top)); }

/* ---- picture settings panel (admins), over the full-size view ---- */
/* the links (Recordings, Picture) never shrink; a long camera name is cut short instead */
.tile .label .name { min-width: 0; }
.tile .label .links { flex: none; display: flex; align-items: center; margin: -6px auto -6px 0; pointer-events: auto; }
.tile .label .links .pb-link { margin-left: 4px; }
.tile button.pb-link { background: none; border: 0; padding: 6px 8px; font: inherit; color: var(--accent); cursor: pointer; }
.tile a.pb-link { padding: 6px 8px; }
/* Full-size: lift the control strip off the very bottom edge and give its buttons a bigger tap target,
   so a near-miss has somewhere to land (viewer.js also stops a tap in the strip from closing the view). */
.single-overlay > .label { padding-bottom: calc(10px + env(safe-area-inset-bottom)); }
.single-overlay .label button.pb-link, .single-overlay .label a.pb-link { padding: 9px 12px; }
/* the full-size view without Live HD stays on the sub-stream, and says so (viewer.js sdBadge); it takes
   the pointer (the label does not) so its title shows, as the dot's does */
.tile .label .sd-badge { flex: none; margin-left: 6px; padding: 1px 6px; border-radius: 4px; background: rgba(255, 255, 255, .18); color: #fff; font-size: 11px; font-weight: 600; letter-spacing: .04em; pointer-events: auto; }
.img-panel {
  position: absolute;
  top: calc(var(--gap) + 8px);
  right: calc(var(--gap) + 8px);
  bottom: calc(var(--gap) + 8px);
  width: min(340px, calc(100% - 16px));
  z-index: 5;
  overflow: auto;
  overscroll-behavior: contain;
  background: rgba(22, 26, 31, 0.95);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 12px 14px;
  font-size: 13px;
  cursor: default;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.img-panel [hidden] { display: none !important; }
.ip-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.ip-head h2 { margin: 0; font-size: 15px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ip-head h2:focus { outline: none; }
.ip-head h2:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.ip-cam { color: var(--muted); font-weight: 400; }
.ip-close { font-size: 18px; line-height: 1; padding: 2px 9px; }
.ip-using { margin: 0; font-size: 12.5px; }
.ip-profile, .ip-location { flex-direction: row; justify-content: space-between; font-size: 12.5px; }
.ip-note { margin: 0; color: var(--muted); font-size: 12px; }
.ip-muted { color: var(--muted); }
.ip-warn-text { color: #ffcf87; }
.ip-auto-buttons { display: flex; gap: 6px; }
.ip-measure { flex: 1; padding: 7px 10px; background: #1d3a57; border-color: #2c5a85; font-weight: 600; }
.ip-cancel { flex: 1; padding: 7px 10px; }
.ip-colour { padding: 7px 10px; }
.ip-report, .ip-box { margin-top: 8px; padding: 8px 10px; border-radius: 8px; background: #0f1216; }
.ip-box { margin-top: 0; border: 1px solid #2a313b; }
.ip-report h3, .ip-box h3 { margin: 0 0 6px; font-size: 12px; color: var(--muted); font-weight: 600; }
.ip-report h4 { margin: 10px 0 4px; font-size: 12px; font-weight: 600; }
.ip-report p, .ip-box p { margin: 0 0 4px; font-size: 12px; }
.ip-report ul, .ip-box ul { list-style: none; margin: 4px 0 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.ip-report ul.ip-plain, .ip-report ul.ip-verdicts { gap: 3px; font-size: 12px; }
.ip-plain li::before, .ip-verdicts li::before { content: "– "; color: var(--muted); }
.ip-v-helped { color: #3fb950; }
.ip-v-worse { color: #ff8a8a; }
.ip-items li label { color: var(--text); align-items: flex-start; gap: 8px; font-size: 12.5px; }
.ip-items li label input { margin-top: 2px; }
.ip-rule { color: var(--muted); font-size: 10.5px; }
.ip-why { display: block; margin: 2px 0 0 22px; color: var(--muted); font-size: 11.5px; line-height: 1.35; }
.ip-box-buttons { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 6px; }
.ip-box .ip-field { margin-left: 0; padding-left: 0; }
.ip-split { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; font-size: 12px; }
.ip-good { color: #3fb950; }
.ip-fields { display: flex; flex-direction: column; gap: 10px; }
.ip-fields fieldset { margin: 0; padding: 8px 10px 10px; border: 1px solid #2a313b; border-radius: 8px; display: flex; flex-direction: column; gap: 8px; }
.ip-fields legend { color: var(--muted); font-size: 12px; padding: 0 4px; }
.ip-field { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 3px 8px; color: var(--text); border-left: 2px solid transparent; padding-left: 6px; margin-left: -8px; }
.ip-field .ip-label { color: var(--text); }
.ip-def { grid-column: 1 / span 2; color: var(--muted); font-size: 11px; }
.ip-range .ip-label { grid-column: 1; grid-row: 1; }
.ip-range .ip-num { grid-column: 2; grid-row: 1; width: 64px; }
.ip-range input[type="range"] { grid-column: 1 / span 2; width: 100%; accent-color: var(--accent); }
.ip-switch { display: flex; align-items: center; gap: 8px; }
.ip-switch .ip-def { margin-left: auto; }
.ip-fixed { color: var(--muted); }
.img-panel input[type="number"], .img-panel input[type="time"] { background: #0f1216; color: var(--text); border: 1px solid var(--border); border-radius: 6px; padding: 2px 6px; font: inherit; color-scheme: light dark; }
.img-panel input:focus-visible, .img-panel select:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.ip-changed { border-left-color: var(--accent); }
.ip-changed .ip-label { color: var(--accent); }
.ip-stale { border-left-color: #ffb454; background: rgba(255, 180, 84, 0.08); }
.ip-off { opacity: 0.45; }
.ip-result { padding: 8px 10px; border-radius: 8px; background: #0f1216; border: 1px solid #2a313b; font-size: 12px; }
.ip-result.ip-done { border-color: #2f6f3f; }
.ip-result.ip-partial, .ip-result.ip-unknown, .ip-result.ip-restart-needed { border-color: #8a6a2a; }
.ip-result.ip-failed { border-color: #8a2a2a; }
.ip-result p { margin: 0 0 4px; }
.ip-result ul { margin: 0 0 4px; padding-left: 16px; }
.ip-st-done { color: #3fb950; }
.ip-st-kept, .ip-st-refused, .ip-st-unknown, .ip-st-restart-needed { color: #ffcf87; }
.ip-side { color: #ffcf87; }
.ip-result-buttons { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
.ip-status, .ip-error { margin: 0; font-size: 12px; color: var(--muted); }
.ip-status:empty, .ip-error:empty { display: none; }
.ip-error { color: #ff8a8a; }
.ip-undo-note { margin: 0; color: var(--muted); font-size: 11.5px; }
.ip-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 6px; position: sticky; bottom: -12px; padding: 8px 0 2px; background: rgba(22, 26, 31, 0.98); }
.ip-undo { margin-right: auto; }
.ip-undo.ip-warn { border-color: #ffb454; color: #ffcf87; }
.ip-apply:not(:disabled) { background: var(--accent); border-color: var(--accent); color: #041018; font-weight: 600; }
.img-panel button.ip-danger:not(:disabled) { border-color: #b3542a; color: #ffb08a; }
/* confirmation dialogs (the server's own texts) */
.ip-dialog { max-width: min(520px, 92vw); max-height: 86vh; overflow: auto; background: var(--panel); color: var(--text); border: 1px solid var(--border); border-radius: 10px; padding: 14px 16px; font-size: 13px; }
.ip-dialog::backdrop { background: rgba(0, 0, 0, 0.6); }
.ip-dialog h3 { margin: 0 0 8px; font-size: 15px; }
.ip-dialog p { margin: 0 0 8px; }
.ip-dialog ul { margin: 0 0 8px; padding-left: 18px; display: flex; flex-direction: column; gap: 4px; }
.ip-dialog details { margin: 0 0 8px; color: var(--muted); font-size: 12px; }
.ip-dialog-buttons { display: flex; justify-content: flex-end; gap: 8px; margin-top: 8px; }
.ip-dialog .ip-go { background: var(--accent); border-color: var(--accent); color: #041018; font-weight: 600; }
.ip-dialog .ip-danger { background: #5a2412; border-color: #b3542a; color: #ffd8c8; font-weight: 600; }
/* input dialogs (alarms ack note / bookmark, in place of the browser's prompt) */
.ip-dialog.al-input label { display: block; margin: 0 0 6px; color: var(--muted); font-size: 12px; }
.ip-dialog.al-input input[type="text"], .ip-dialog.al-input textarea { width: 100%; box-sizing: border-box; background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: 8px; padding: 8px; font: inherit; }
.ip-dialog.al-input textarea { resize: vertical; min-height: 54px; }
.ip-dialog.al-input input[type="text"]:focus, .ip-dialog.al-input textarea:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
@media (max-width: 600px) {
  .img-panel { top: auto; left: 8px; right: 8px; width: auto; height: 62%; }
}

/* ---- line crossing (admins): lines-panel.js, over the full-size view ---- */
/* the panel is the Picture panel's box (.img-panel); Hide folds it to its title bar so the whole
   picture can be drawn on */
.lines-panel.ln-folded, .osd-panel.ln-folded { bottom: auto; height: auto; }
@media (max-width: 600px) {
  .lines-panel.ln-folded, .osd-panel.ln-folded { top: auto; bottom: calc(var(--gap) + 8px); }
}
.lines-panel.ln-folded > :not(.ip-head, dialog), .osd-panel.ln-folded > :not(.ip-head, dialog) { display: none !important; }
.ln-head-buttons { display: flex; align-items: center; gap: 6px; flex: none; }
.ln-fold { padding: 2px 9px; font-size: 12px; }
.ln-help, .ln-note { margin: 0; color: var(--muted); font-size: 12px; }
.ln-warn { margin: 0; padding: 6px 8px; border-radius: var(--r-sm); background: var(--warn-soft); color: var(--text); font-size: 12px; }
.ln-slots { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.ln-slot { display: grid; grid-template-columns: auto 1fr auto auto; align-items: center; gap: 6px; font-size: 12.5px; }
.ln-slot button { padding: 3px 8px; font-size: 12px; }
.ln-pick { display: flex; align-items: center; gap: 6px; }
.ln-current .ln-pick { border-color: var(--accent); font-weight: 600; }
.ln-swatch { width: 10px; height: 10px; border-radius: 50%; flex: none; }
.ln-state { color: var(--muted); font-size: 12px; }
.ln-changed .ln-state { color: var(--accent); }
.ln-dir { min-width: 58px; }
.ln-box { padding: 8px 10px; border: 1px solid var(--border); border-radius: var(--r-md); display: flex; flex-direction: column; gap: 8px; }
.ln-row { flex-direction: row; justify-content: space-between; font-size: 12.5px; }
.ln-range { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 3px 8px; color: var(--text); font-size: 12.5px; }
.ln-range input[type="range"] { grid-column: 1 / span 2; width: 100%; accent-color: var(--accent); }
.ln-range input[type="number"] { width: 64px; }
.ln-range:has(input:disabled) { opacity: 0.45; }
.ln-result ul { list-style: none; padding-left: 0; }
.ln-ok { color: var(--ok); }
.ln-no { color: var(--warn); }
.ln-ntfy { display: flex; flex-direction: column; gap: 4px; font-size: 12px; }
.ln-ntfy p, .ln-ntfy ol { margin: 0; }
.ln-ntfy ol { padding-left: 18px; }
.ln-topic code { user-select: all; padding: 2px 6px; border-radius: var(--r-sm); background: var(--surface-2); color: var(--text); }
/* over the picture. The full-size view's own canvas rules (raised a layer, moved by the zoom, sized
   to the tile) must not reach these: the drawing sits exactly on the picture, placed by the script */
.tile.single-overlay > .ln-shield { position: absolute; inset: 0; z-index: 3; }
.tile.single-overlay canvas.ln-overlay { position: absolute; z-index: 3; display: block; transform: none; object-fit: fill; touch-action: none; cursor: crosshair; }
.tile.single-overlay canvas.ln-overlay.ln-locked { cursor: default; }
.tile.single-overlay canvas.ln-loupe { position: absolute; z-index: 5; display: block; transform: none; object-fit: fill; border-radius: 50%; border: 2px solid var(--accent); background: var(--video-bg); pointer-events: none; }
.tile.single-overlay canvas.ln-overlay[hidden], .tile.single-overlay canvas.ln-loupe[hidden] { display: none; }
/* the on-picture control bar (OSD, Picture, Lines) sits above the editor's drag layer (z-index 3) so
   its buttons stay tappable while an OSD or Lines editor is open. The bar itself is click-through
   (pointer-events: none on .label); only its buttons take a tap, so a drag elsewhere still reaches the
   drawing canvas. The status badge, step arrows, zoom badge and fullscreen button are already above it. */
.tile.single-overlay > .label { z-index: 4; }
/* a phone's full-size camera covers the grid (z-index 91): the panel goes above it */
body.phone-full #grid > .lines-panel, body.phone-full #grid > .osd-panel { z-index: 92; }

/* ---- the camera's burnt-in OSD (admins): osd-panel.js, over the full-size view ---- */
/* the panel reuses the line-crossing box (.img-panel, Hide/fold, the .ln-overlay drawing canvas) */
/* each overlay (name, clock) is its own box; the selected one (what a corner button and a tap place) is ringed */
.osd-ov { padding: 8px 10px; border: 1px solid var(--border); border-radius: var(--r-md); display: flex; flex-direction: column; gap: 8px; }
.osd-ov.osd-current { border-color: var(--accent); }
.osd-select { align-self: flex-start; padding: 3px 9px; font-size: 12px; }
.osd-select[aria-pressed="true"] { border-color: var(--accent); color: var(--text); font-weight: 600; }
.osd-name-row { flex-direction: column; align-items: stretch; gap: 3px; font-size: 12.5px; }
.osd-name { background: #0f1216; color: var(--text); border: 1px solid var(--border); border-radius: 6px; padding: 4px 7px; font: inherit; color-scheme: light dark; }
.osd-name-err:empty { display: none; }
.osd-corners { display: flex; flex-direction: column; gap: 6px; }
.osd-corner-buttons { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
.osd-corner { padding: 5px 8px; font-size: 12px; }
.osd-corner[aria-pressed="true"] { border-color: var(--accent); color: var(--text); font-weight: 600; }
.osd-result ul { list-style: none; padding-left: 0; margin: 4px 0 0; }

/* ---- add NVR: connect by address or by serial number (TVT P2P) ---- */
.st-dialog [hidden] { display: none !important; }
.st-connect { border: 0; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.st-connect legend { color: var(--muted); font-size: 13px; padding: 0; margin-bottom: 4px; }
#f-p2p { display: flex; flex-direction: column; gap: 6px; }

/* ---- VPN sites (deploy/vpn hub) ---- */
.st-vpn { margin: 0 0 18px; padding: 12px 14px; border: 1px solid var(--border); border-radius: 10px; background: var(--panel); }
.st-vpn h2 { margin: 0 0 8px; font-size: 15px; }
.st-vpn-list { list-style: none; margin: 0 0 8px; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.st-vpn-site { display: grid; grid-template-columns: minmax(120px, auto) 1fr auto; gap: 2px 12px; align-items: center; }
.st-vpn-site .st-meta:last-of-type { grid-column: 2; }
.st-vpn-site button { grid-row: 1 / span 2; grid-column: 3; }
@media (max-width: 600px) { .st-vpn-site { grid-template-columns: 1fr; } .st-vpn-site button { grid-row: auto; grid-column: auto; justify-self: start; } .st-vpn-site .st-meta:last-of-type { grid-column: auto; } }

/* ---- Sites: per-NVR camera dropdown and the inline settings editor (camera-editor.js) ---- */
/* a live preview beside the Picture / OSD / Lines panels, re-hosted from the full-size view */
.st-cameras { margin: 2px 0 6px; }
.st-cameras-sum { cursor: pointer; font-size: 13px; color: var(--muted); padding: 4px 0; }
.st-cameras-body { display: flex; flex-wrap: wrap; gap: 6px; padding: 6px 0; }
.st-cam-chip { font-size: 13px; padding: 5px 10px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface-2); color: var(--text); cursor: pointer; }
.st-cam-chip[aria-pressed="true"] { border-color: var(--accent); color: var(--accent); }
.st-cam-off { color: var(--muted); }
.st-cameras-editor:empty { display: none; }
.ce-editor { display: flex; gap: 16px; flex-wrap: wrap; align-items: flex-start; padding: 8px 0 4px; }
.ce-preview { flex: 1 1 280px; min-width: 240px; }
.ce-tile { position: relative; aspect-ratio: 16 / 9; width: 100%; background: var(--video-bg); border-radius: 8px; overflow: hidden; }
.ce-panels { flex: 1 1 320px; min-width: 280px; }
.ce-tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--border); margin-bottom: 12px; }
.ce-tab { font-size: 14px; padding: 7px 12px; background: none; border: 0; border-bottom: 2px solid transparent; color: var(--muted); cursor: pointer; }
.ce-tab[aria-selected="true"] { color: var(--text); border-bottom-color: var(--accent); }
.ce-body[hidden] { display: none; }
/* the re-hosted panels overlaid the full-size picture (position:absolute); here they sit in normal flow */
.ce-body .img-panel { position: static; inset: auto; width: auto; height: auto; max-height: 70vh; z-index: auto; background: var(--surface-2); }
@media (max-width: 640px) { .ce-editor { flex-direction: column; } }

/* Settings page (settings.html) */
.se-section { background: var(--panel); border-radius: 8px; padding: 14px 16px; display: flex; flex-direction: column; gap: 10px; }
.se-section[hidden] { display: none; }
.se-section h2 { margin: 0; font-size: 16px; }
.se-section h3 { margin: 6px 0 0; font-size: 14px; }
.se-section .st-card { background: var(--bg); }
.se-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 10px 14px; align-items: end; }
.se-grid label { display: flex; flex-direction: column; gap: 4px; font-size: 13px; }
.se-grid label.st-check { flex-direction: row; align-items: center; gap: 8px; }
.se-grid input:not([type="checkbox"]), .se-table input, .st-dialog select { background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: 4px; padding: 6px 8px; font: inherit; }
.se-wide { grid-column: 1 / -1; }
.se-actions { display: flex; align-items: center; gap: 10px; grid-column: 1 / -1; }
.se-table-wrap { overflow-x: auto; }
.se-table { border-collapse: collapse; width: 100%; font-size: 13px; }
.se-table th { text-align: left; color: var(--muted); font-weight: 500; padding: 4px 8px; }
.se-table td { padding: 4px 8px; border-top: 1px solid #232a33; vertical-align: middle; }
.se-num { width: 80px; }
.se-danger { margin: 0; padding: 10px 12px; border-radius: 6px; background: #3a1214; color: #ff8a8a; font-weight: 600; }
/* Settings > Storage: the time-lapse and retention switch, and what the two jobs last did (storage.js) */
.sj-now { margin: 0; font-size: 13px; font-weight: 600; }
.sj-choices { margin: 0; padding: 8px 10px 10px; border: 1px solid var(--border); border-radius: 8px; display: flex; flex-direction: column; gap: 6px; font-size: 13px; }
.sj-choices legend { color: var(--muted); font-size: 12px; padding: 0 4px; }
.sj-choices label { display: flex; align-items: center; gap: 8px; }
.sj-runs { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; font-size: 13px; }
.sj-runs .warn { color: var(--warn); }
.sj-runs .bad { color: var(--bad); }
.sj-runs ul { margin: 2px 0 0; padding-left: 18px; color: var(--muted); font-size: 12px; }
.se-erase { background: #b3261e; border-color: #b3261e; color: #fff; font-weight: 600; }
.se-parts { margin: 0; padding-left: 18px; font-size: 13px; }
.se-path-row { display: flex; align-items: flex-end; gap: 8px; }
.se-path-row label { flex: 1; }
.se-folder-path { display: flex; align-items: center; gap: 8px; margin: 8px 0; }
.se-folder-path code { overflow-wrap: anywhere; }
.se-folder-list { list-style: none; margin: 0; padding: 0; max-height: 50vh; overflow: auto; border: 1px solid #30363d; border-radius: 4px; }
.se-folder-list li { border-bottom: 1px solid #21262d; }
.se-folder-list li.st-meta { padding: 8px; }
.se-folder { width: 100%; text-align: left; background: none; border: 0; padding: 8px; cursor: pointer; }
.se-folder:hover, .se-folder:focus-visible { background: #1d3a57; }
.se-folder-new { display: flex; gap: 8px; margin-top: 8px; }
.se-folder-new input { flex: 1; }
.se-progress { display: block; width: 100%; max-width: 420px; height: 10px; margin: 6px 0 8px; accent-color: var(--accent); }
.se-job { border: 1px solid var(--border); border-radius: 6px; padding: 10px 12px; }
.se-job h3 { margin: 0 0 6px; }

/* ---- Health page and the alert banner -------------------------------------------------------- */
/* The banner sits above everything on every page: a problem should be seen by whoever is looking
   at a camera, not only by someone who thinks to open Health. */
.alert-banner { background: #3a1416; color: #ffb3b5; padding: 8px 16px; display: flex; gap: 10px; align-items: center; border-bottom: 1px solid #5a1f22; }
.alert-banner a { color: #fff; margin-left: auto; }
.health { padding: 14px 16px; display: flex; flex-direction: column; gap: 14px; }
.hp-sending { margin: 0; color: var(--tl-evt); }
.hp-sending:empty { display: none; }
.hp-note { margin: 0; color: var(--muted); font-size: 12px; }
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; }
.card { background: var(--panel); border-radius: 8px; padding: 12px 14px; }
.card .lbl { color: var(--muted); font-size: 12px; }
.card .big { font-size: 22px; font-weight: 600; margin: 4px 0; }
.card .note { color: var(--muted); font-size: 12px; }
.card.bad .big { color: #e5484d; }
.card.warn .big { color: var(--tl-evt); }
.card.ok .big { color: #3fb27f; }
.hp-table { width: 100%; border-collapse: collapse; background: var(--panel); border-radius: 8px; overflow: hidden; }
.hp-table th, .hp-table td { padding: 8px 12px; text-align: left; font-size: 13px; border-bottom: 1px solid #262d36; }
.hp-table th { color: var(--muted); font-weight: 500; }
.hp-table tr:last-child td { border-bottom: 0; }
/* the make and serial under a disk's name: there to be copied down, not read at a glance */
.hp-sub { color: var(--muted); font-size: 11px; margin-top: 2px; }

/* the full SMART report, folded away under its disk */
.hp-smart-row td { padding: 0 !important; }
.hp-smart { padding: 0 12px 12px; }
.hp-smart > summary { cursor: pointer; padding: 8px 0; font-size: 12px; color: var(--muted); }
.hp-smart > summary:hover { color: var(--fg); }
.hp-smart-summary { display: flex; flex-wrap: wrap; gap: 14px; margin: 4px 0 12px; }
.hp-smart-fact { flex: 1 1 200px; min-width: 180px; max-width: 320px; background: #0f141a; border-radius: 6px; padding: 8px 10px; }
.hp-smart-value { font-size: 15px; margin: 2px 0; }
.hp-smart-value.ok { color: #3fb27f; }
.hp-smart-value.warn { color: var(--tl-evt); }
.hp-smart-value.bad { color: #e5534b; }
.hp-smart-table th, .hp-smart-table td { font-size: 11px; padding: 5px 8px; vertical-align: top; }
/* the attributes whose raw number means nothing are visibly quieter than the ones that matter */
.hp-smart-table tr.hp-muted td { opacity: 0.55; }
.hp-key { color: var(--tl-evt); font-size: 9px; text-transform: uppercase; letter-spacing: 0.06em; margin-left: 5px; }
.hp-table td.ok { color: #3fb27f; }
.hp-table td.warn { color: var(--tl-evt); }
.hp-table td.bad { color: #e5484d; }

/* Per-NVR health panels: its own disks and retention first, then the rest of the facts. */
.nvr-panels { display: grid; gap: 14px; }
.nvr-panel { background: var(--panel); border-radius: 8px; padding: 12px 14px; display: grid; gap: 10px; }
.nvr-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.nvr-head h3 { margin: 0; font-size: 15px; }
.nvr-why { color: var(--muted); font-size: 12px; }
.pill { font-size: 12px; padding: 2px 8px; border-radius: 999px; border: 1px solid currentColor; }
.pill.ok { color: #3fb27f; }
.pill.warn { color: var(--tl-evt); }
.pill.bad { color: #e5484d; }
/* The cards sit inside a panel, so they need a background of their own to stay readable. */
.nvr-headline .card { background: #0000001f; }
.nvr-facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 4px 14px; margin: 0; font-size: 13px; }
.nvr-facts dt { color: var(--muted); font-size: 12px; }
.nvr-facts dd { margin: 0 0 6px; }
.nvr-facts dd.ok { color: #3fb27f; }
.nvr-facts dd.warn { color: var(--tl-evt); }
.nvr-facts dd.bad { color: #e5484d; }
.nvr-disks { background: transparent; }

/* Live tile state dot (live-tile.js tileDot): green video arriving, red also being recorded,
   grey nothing arriving. Colour is never the only clue: the state is written out in the dot's
   title, and the recording dot also pulses. */
.tile .dot { flex: 0 0 auto; align-self: center; width: 9px; height: 9px; border-radius: 50%; background: var(--muted, #7a8290); box-shadow: 0 0 0 1px rgb(0 0 0 / 45%); pointer-events: auto; }
/* the label is a space-between row: let the name take the slack so the dot sits beside it */
.tile .label .name { flex: 1 1 auto; }
.tile .dot-live { background: #3fb27f; }
.tile .dot-rec { background: #e5484d; border-radius: 50%; box-shadow: 0 0 0 1px rgb(0 0 0 / 45%), 0 0 5px rgb(229 72 77 / 70%); }
.tile .dot-off { background: #7a8290; opacity: 0.75; }
@media (prefers-reduced-motion: no-preference) {
  .tile .dot-rec { animation: dot-rec-pulse 2s ease-in-out infinite; }
}
@keyframes dot-rec-pulse { 50% { opacity: 0.55; } }

/* The running build, top right: the only way to tell from the screen whether a deploy landed. */
.build { color: var(--muted); font-size: 11px; font-family: ui-monospace, Consolas, monospace; letter-spacing: 0.02em; }
/* An unobtrusive mark when the server treats this viewer as remote (through the tunnel): the video is
   converted and the link is slower, so on site the local address is worth using (playback hunt F1/F6). */
.remote-mark { color: var(--muted); font-size: 11px; letter-spacing: 0.02em; cursor: help; opacity: 0.8; }
.remote-mark::before { content: "\1F310"; margin-right: 0.35em; } /* a globe */

/* Digital zoom on the playback picture (playback.js): magnifies what is on screen only. */
.pb-video { overflow: hidden; }
.pb-video.zoomed { cursor: grab; }
.pb-video.zoomed:active { cursor: grabbing; }
.pb-zoom-badge { position: absolute; bottom: 8px; right: 10px; padding: 2px 7px; border-radius: 10px; background: rgb(0 0 0 / 55%); color: #e6e8eb; font: 600 12px ui-monospace, Consolas, monospace; pointer-events: none; }

/* Full screen on the playback page: the picture takes the space the page chrome gives back. */
body.is-fullscreen > header { display: none; }
body.is-fullscreen .alert-banner { display: none; }

/* Choosing a clip to export: two handles on the timeline (playback.js drawClip). The handle's hit
   area is far wider than the line you see, because a 3 px target is unusable with a thumb. */
.pb-clip-sel { position: absolute; top: 0; bottom: 0; background: rgb(224 160 48 / 18%); border-top: 2px solid var(--tl-evt); border-bottom: 2px solid var(--tl-evt); pointer-events: none; }
.pb-clip-handle { position: absolute; top: -4px; bottom: -4px; width: 26px; margin-left: -13px; cursor: ew-resize; touch-action: none; z-index: 3; }
.pb-clip-handle::after { content: ""; position: absolute; left: 8px; top: 0; bottom: 0; width: 10px; background: var(--tl-evt); border-radius: 3px; }
.pb-clip-handle:hover::after { background: #f3c877; }

/* Bookmarks (playback.js, bookmarks-view.js). A diamond at the start of each one, with a faint band
   over the stretch it covers. The diamond is a rotated square rather than a glyph so it stays the
   same size at every zoom and cannot be confused with the motion blocks below it. */
.pb-bm-marks { position: absolute; inset: 0; pointer-events: none; z-index: 4; }
.pb-bm-band { position: absolute; top: 0; bottom: 0; background: rgb(124 196 255 / 14%); pointer-events: none; }
.pb-bm-mark {
  position: absolute;
  top: -7px;
  width: 11px;
  height: 11px;
  margin-left: -6px;
  background: var(--tl-bm, #7cc4ff);
  border: 1px solid #0c1116;
  transform: rotate(45deg);
  cursor: pointer;
  pointer-events: auto;
}
.pb-bm-mark:hover, .pb-bm-mark:focus-visible { background: #b7ddff; }
#bookmark[aria-pressed="true"] { border-color: var(--tl-bm, #7cc4ff); color: var(--tl-bm, #7cc4ff); }

/* The list beside the picture. Same shape as the motion-search panel so the two read as one page. */
.pb-bookmarks {
  width: 300px;
  flex: none;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 12px;
  background: var(--panel);
  border-radius: 4px;
  overflow-y: auto;
}
/* display: flex above would otherwise beat the hidden attribute and leave the panel always open */
.pb-bookmarks[hidden] { display: none; }
.pb-bookmarks h2 { margin: 0; font-size: 14px; }
.pb-bookmarks label { flex-direction: column; align-items: stretch; gap: 4px; font-size: 12px; }
.pb-bookmarks .pb-bm-scope { flex-direction: row; align-items: center; gap: 6px; }
.pb-bm-status { margin: 0; color: var(--muted); font-size: 12px; }
.pb-bm-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.pb-bm-list li { padding: 6px 8px; background: #1a1f26; border-radius: 4px; border-left: 3px solid var(--tl-bm, #7cc4ff); }
.pb-bm-go { width: 100%; text-align: left; background: none; border: 0; padding: 0; color: var(--text); cursor: pointer; }
.pb-bm-when { display: block; color: var(--muted); font-size: 11px; font-variant-numeric: tabular-nums; }
.pb-bm-note { margin: 4px 0 0; font-size: 12px; color: var(--muted); white-space: pre-wrap; }
.pb-bm-actions { display: flex; gap: 6px; margin-top: 6px; }
.pb-bm-actions button { font-size: 11px; padding: 2px 8px; }

.pb-bm-dialog { max-width: min(460px, 94vw); background: var(--panel); color: var(--text); border: 1px solid var(--border); border-radius: 10px; padding: 16px 18px; }
.pb-bm-dialog::backdrop { background: rgb(0 0 0 / 55%); }
.pb-bm-dialog h2 { margin: 0 0 8px; font-size: 17px; }
.pb-bm-dialog label { display: block; margin: 8px 0; }
.pb-bm-dialog input[type="text"], .pb-bm-dialog select, .pb-bm-dialog textarea { width: 100%; margin-top: 3px; }
.pb-bm-times { display: flex; gap: 10px; }
.pb-bm-times label { flex: 1; }

/* The export dialog (playback.js). Someone uses this when something has gone wrong, so it is
   plain and the format choice explains itself rather than assuming knowledge. */
.pb-export { max-width: min(560px, 94vw); background: var(--panel); color: var(--text); border: 1px solid var(--border); border-radius: 10px; padding: 16px 18px; }
.pb-export::backdrop { background: rgb(0 0 0 / 55%); }
.pb-export h2 { margin: 0 0 8px; font-size: 17px; }
.pb-export label { display: block; margin: 8px 0; }
.pb-export input[type="text"], .pb-export input:not([type]), .pb-export input[type="password"], .pb-export select { width: 100%; margin-top: 3px; }
.pb-ex-period { margin: 0 0 6px; font-weight: 600; }
.pb-ex-warn { margin: 0 0 10px; padding: 8px 10px; border-radius: 6px; background: #3a2a0e; color: #f3c877; font-size: 13px; }
.pb-ex-formats { border: 1px solid var(--border); border-radius: 6px; padding: 8px 10px; margin: 10px 0; }
.pb-ex-formats label { font-size: 13px; margin: 6px 0; }
.pb-ex-msg { min-height: 1.2em; margin: 8px 0 0; font-size: 13px; color: var(--muted); }
.pb-ex-bad { color: #ff8b8e; }
.pb-ex-download { color: var(--accent); }
.pb-ex-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 12px; }
#exProgress { width: 100%; margin-top: 8px; }
.pb-on { background: #12314d; border-color: var(--accent); }

/* Which camera adjoins which, drawn on the map (camera-links.mjs). A suggestion is always dashed
   and always labelled a guess: a nearest-neighbour guess must never look like a drawn link. */
.map-view.linking, .map-view.linking .cam { cursor: pointer; }
.map-link { stroke: var(--accent); stroke-width: 2; stroke-opacity: 0.45; pointer-events: stroke; }
.map-link.sel { stroke-opacity: 1; stroke-width: 3; }
.map-link.suggested { stroke: var(--muted); stroke-width: 2; stroke-dasharray: 5 6; stroke-opacity: 0.8; }
.map-link-arrow { fill: var(--accent); fill-opacity: 0.45; }
.map-link-arrow.sel { fill-opacity: 1; }
.map-link-dir { color: var(--accent); font-size: 14px; padding: 0 4px; }
.map-link-label { width: 100%; margin-top: 4px; font-size: 12px; }
.map-link-actions { display: flex; gap: 8px; }
.map-link-oneway { display: flex; align-items: center; gap: 6px; }
.map-suggested li { opacity: 0.85; }
.map-suggest h2 { display: flex; align-items: center; gap: 6px; }
.map-guess-tag {
  border: 1px dashed var(--muted); border-radius: 10px; padding: 0 6px;
  color: var(--muted); font-size: 11px; font-weight: normal; text-transform: lowercase;
}

/* ---- the camera wall (wall.html) --------------------------------------------------------------
   Appended, never edited into the rules above: the playback page's own styling is in flight. The
   grid's column count is set by wall.js (gridLayout) rather than auto-fit, because the right number
   of columns depends on the shape of the space as well as the number of cameras. */
.wall-page { display: flex; flex-direction: column; height: 100vh; }
.wall-main { flex: 1; min-height: 0; display: flex; flex-direction: column; gap: 6px; padding: 8px; }
.wall-load { margin: 0; font-size: 13px; color: var(--muted); }
.wall-load[data-level="heavy"] { color: var(--tl-evt); }
.wall-load[data-level="over"] { color: var(--tl-gap); }
.wall-empty { margin: auto; color: var(--muted); }
.wall-grid {
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-columns: repeat(var(--wall-cols, 2), 1fr);
  grid-auto-rows: 1fr;
  gap: var(--gap);
}
.wall-tile { position: relative; background: var(--tile); border-radius: 4px; overflow: hidden; min-height: 0; }
.wall-tile canvas { width: 100%; height: 100%; object-fit: contain; display: block; }
/* A camera with nothing at this moment says so; it never just sits there black, because black
   reads as "broken" or "nothing happened", and on an investigation those are different answers. */
.wall-tile-msg {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  padding: 12px; text-align: center; font-size: 13px; line-height: 1.4;
  background: var(--panel); color: var(--muted);
}
.wall-tile[data-kind="undecodable"] .wall-tile-msg, .wall-tile[data-kind="error"] .wall-tile-msg { color: var(--tl-gap); }
.wall-tile-label {
  position: absolute; left: 0; right: 0; bottom: 0; display: flex; gap: 8px; justify-content: space-between;
  padding: 3px 6px; font-size: 12px; background: linear-gradient(transparent, rgba(0, 0, 0, 0.75));
}
.wall-tile-name { color: var(--text); text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.wall-tile-name:hover { color: var(--accent); text-decoration: underline; }
.wall-tile-time { color: var(--muted); white-space: nowrap; font-variant-numeric: tabular-nums; }

.wall-bar { flex: 0 0 auto; }
.wall-coverage { margin-left: 8px; color: var(--muted); font-size: 13px; }
/* one lane per camera, names down the left so a gap can be read off against a name */
.wall-lanes, .wall-ticks-row { display: flex; gap: 6px; }
.wall-lane-names { flex: 0 0 140px; }
.wall-lane-name {
  height: 14px; margin-bottom: 2px; font-size: 11px; color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.wall-lane-track { position: relative; flex: 1; background: var(--tl-track); border-radius: 4px; padding: 0; touch-action: none; cursor: crosshair; }
.wall-lane { position: relative; height: 14px; margin-bottom: 2px; }
.wall-lane div { position: absolute; top: 1px; bottom: 1px; border-radius: 2px; background: var(--tl-rec); }
/* the server has no index of this camera: the lane is empty because we do not know, not because
   there is nothing there */
.wall-lane-unknown {
  background: repeating-linear-gradient(45deg, transparent 0 5px, rgba(138, 147, 160, 0.15) 5px 10px);
}
.wall-ticks-row .pb-ticks { flex: 1; }

.wall-pick { background: var(--panel); color: var(--text); border: 1px solid #2a3139; border-radius: 6px; min-width: 320px; }
.wall-pick h3 { margin: 10px 0 4px; font-size: 12px; color: var(--muted); text-transform: none; }
.wall-pick-list { max-height: 50vh; overflow: auto; }
.wall-pick-row { display: block; padding: 2px 0; }
.wall-pick-note { margin: 4px 0 8px; font-size: 13px; color: var(--muted); }
.wall-pick-note[data-level="heavy"] { color: var(--tl-evt); }
.wall-pick-note[data-level="over"] { color: var(--tl-gap); }

/* Following someone: where this camera leads (playback.js). Following is guesswork, so the back
   step is as prominent as the forward ones, and a guess never looks like a known route. */
.pb-follow { display: flex; align-items: center; gap: 10px; padding: 8px 0 2px; flex-wrap: wrap; }
.pb-follow-title { color: var(--muted); font-size: 12px; }
.pb-follow-list { display: flex; gap: 8px; flex-wrap: wrap; }
.pb-follow-cam { display: flex; flex-direction: column; align-items: flex-start; gap: 1px; padding: 6px 10px; background: var(--panel); color: var(--text); border: 1px solid var(--border); border-radius: 6px; cursor: pointer; text-align: left; }
.pb-follow-cam:hover { border-color: var(--accent); }
.pb-follow-name { font-size: 13px; }
.pb-follow-where { font-size: 11px; color: var(--muted); }
.pb-follow-guess { border-style: dashed; opacity: 0.8; }
.pb-follow-back { padding: 6px 12px; background: var(--panel); color: var(--text); border: 1px solid var(--accent); border-radius: 6px; cursor: pointer; }
/* the recorder's own gaps, thin and on top of the recorded stretches (the reason is on hover) */
.wall-lane div[data-kind="gap"] { top: 4px; bottom: 4px; background: var(--tl-gap); opacity: 0.9; }

/* Multi-camera playback (phase 8): the tile toolbar, a maximised tile, the picker's search box and
   the pager. The pager exists because a fixed grid streams only the page on screen — the cameras on
   the other pages cost the NVRs nothing at all, which is the point of paging rather than shrinking. */
.wall-tile-tools {
  position: absolute; top: 4px; right: 4px; display: flex; gap: 2px;
  opacity: 0; transition: opacity 120ms; pointer-events: none;
}
.wall-tile:hover .wall-tile-tools, .wall-tile:focus-within .wall-tile-tools { opacity: 1; pointer-events: auto; }
.wall-tile-tools button {
  background: rgba(0, 0, 0, 0.6); color: var(--text); border: 1px solid #2a3139; border-radius: 3px;
  font-size: 13px; line-height: 1; padding: 4px 6px; cursor: pointer;
}
.wall-tile-tools button:hover { background: var(--accent); color: #fff; }
/* A maximised tile fills the grid. The others stay in the page (so the lanes and the order do not
   jump about) but are hidden, and a hidden tile stops streaming. */
.wall-grid.wall-maximised { --wall-cols: 1; }
.wall-grid.wall-maximised .wall-tile { display: none; }
.wall-grid.wall-maximised .wall-tile-max { display: block; }
.wall-pick-search { display: block; margin: 4px 0 8px; }
.wall-pick-search input { width: 100%; }
.wall-pager { display: inline-flex; align-items: center; gap: 4px; font-size: 12px; color: var(--muted); }

/* Alarms (phase 7). The list is meant to be read at a glance from across a room: the colour is the
   urgency, and anything still waiting for a person keeps a bar down its left edge, because colour
   alone is no use to a colour-blind viewer. */
.al-filters { display: flex; flex-wrap: wrap; gap: 10px; align-items: end; margin-bottom: 8px; }
.al-filters label { display: flex; flex-direction: column; font-size: 12px; color: var(--muted); gap: 2px; }
.al-summary { color: var(--muted); margin: 4px 0 10px; }
.al-table { width: 100%; border-collapse: collapse; }
.al-table th { text-align: left; padding: 8px 10px; color: var(--text-muted); font-weight: 500; font-size: 12px; border-bottom: 1px solid var(--border); }
.al-table td { text-align: left; padding: 10px; border-bottom: 1px solid var(--border); vertical-align: middle; }
.al-table tbody tr:hover { background: var(--surface-hover); }
.al-table tr.needs-ack td:first-child { box-shadow: inset 3px 0 0 var(--accent); }
.al-pri { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 12px; font-weight: 600; text-transform: capitalize; }
.al-pri.pri-critical { background: var(--bad-soft); color: var(--bad); }
.al-pri.pri-high { background: var(--warn-soft); color: var(--warn); }
.al-pri.pri-medium { background: var(--accent-soft); color: var(--accent-strong); }
.al-pri.pri-low { background: var(--surface-3); color: var(--text-muted); }
.al-when small { display: block; color: var(--text-muted); font-size: 12px; }
/* a line crossing's picture under its name (event-snapshot.mjs); it opens full size in a new tab */
.al-snap { display: block; width: 160px; max-width: 100%; aspect-ratio: 16 / 9; margin-top: var(--s1); border-radius: var(--r-sm); overflow: hidden; background: var(--video-bg); }
.al-snap img { display: block; width: 100%; height: 100%; object-fit: cover; }
.al-snap:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
/* a phone has little width to spare for the table: a smaller picture, still enough to tell who crossed */
@media (max-width: 600px) { .al-snap { width: 112px; } }
/* the alarm a link (a phone alert) pointed at: tinted, with the accent bar whether or not it still
   needs a look, so it is found at a glance among the rest */
.al-table tbody tr.al-target { background: var(--accent-soft); }
.al-table tr.al-target td:first-child { box-shadow: inset 3px 0 0 var(--accent); }
.al-table tr.al-target:focus { outline: 2px solid var(--focus); outline-offset: -2px; }
/* the rest of a row reads as ordinary text: only the urgency cell and the edge bar are coloured */
.al-table td:not(:first-child) { color: var(--text); }
.al-actions { display: flex; flex-wrap: wrap; gap: 4px; justify-content: flex-end; }
.al-actions button, .al-actions a {
  background: none; color: var(--text-muted); border: 1px solid transparent; border-radius: var(--r-sm);
  font-size: 12px; padding: 4px 8px; cursor: pointer; text-decoration: none;
}
.al-actions button:hover, .al-actions a:hover { background: var(--surface-hover); color: var(--text); }
.al-actions button.st-primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
/* the filter bar: three filters and Apply; the rest folded away */
.al-filters .al-search { flex: 1; min-width: 180px; }
.al-filters .al-search input { width: 100%; }
.al-more { flex-basis: 100%; }
.al-more > summary { cursor: pointer; color: var(--text-muted); font-size: 13px; width: max-content; }
.al-more-body { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 8px; }
.al-where { margin-top: 16px; color: var(--text-muted); }
.al-where > summary { cursor: pointer; font-size: 13px; }
.alarms > section { display: flex; flex-direction: column; gap: 10px; }
.alarms > section[hidden] { display: none; }
.al-sources { color: var(--muted); font-size: 13px; }
.al-sources h3 { margin: 8px 0 2px; font-size: 13px; color: var(--text); }
.al-rules { list-style: none; padding: 0; }
.al-rules li { display: flex; gap: 8px; align-items: center; padding: 4px 0; border-bottom: 1px solid #222a32; }
.al-rules li span { flex: 1; }
.al-rule-form { display: flex; flex-wrap: wrap; gap: 10px; align-items: end; margin-top: 8px; }
.al-rule-form label { display: flex; flex-direction: column; font-size: 12px; color: var(--muted); gap: 2px; }
.al-error { color: #ff6b6b; }
/* The tuning view: the live picture with the NVR's watched zones over it, and the meter under it. */
.al-tune { display: flex; flex-direction: column; gap: 12px; max-width: 1100px; }
.al-tune-pick { display: flex; align-items: center; gap: 8px; }
.al-tune-pick select { min-width: 240px; max-width: 100%; }
.al-tune-empty { margin: 0; padding: 28px 16px; border: 1px dashed var(--border); border-radius: 10px; color: var(--muted); text-align: center; }
.al-tune-body { display: grid; grid-template-columns: 1fr; gap: 16px; align-items: start; }
.al-tune-body[hidden] { display: none; }
@media (min-width: 900px) { .al-tune-body { grid-template-columns: minmax(0, 3fr) minmax(260px, 2fr); } }
.al-tune-side { display: flex; flex-direction: column; gap: 12px; }
.al-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.al-stat { background: var(--panel); border-radius: 8px; padding: 10px 12px; display: flex; flex-direction: column; gap: 2px; }
.al-stat b { font-size: 22px; font-weight: 600; font-variant-numeric: tabular-nums; }
.al-stat span { color: var(--muted); font-size: 12px; }
.al-tune-nvr { background: var(--panel); border-radius: 8px; padding: 12px 14px; display: flex; flex-direction: column; gap: 8px; }
.al-tune-nvr h3 { margin: 0; font-size: 14px; }
.al-tune-nvr form { display: flex; flex-wrap: wrap; gap: 8px; align-items: end; }
.al-tune-nvr form[hidden] { display: none; }
.al-tune-live { position: relative; }
.al-tune-live .tile { position: relative; background: #000; aspect-ratio: 16 / 9; }
.al-tune-live .tile canvas { width: 100%; height: 100%; object-fit: contain; }
.al-tune-overlay { position: absolute; inset: 0; pointer-events: none; }
.al-meter { font-variant-numeric: tabular-nums; }

/* Phones.
   The camera grid divides the window's height by however many rows the layout picker chose. That
   is right on a monitor and wrong on a phone: a 3x3 on a 375px screen gives nine slivers about
   120px wide and twice as tall, so every landscape picture sits letterboxed in a portrait box and
   is too small to recognise anyone in.
   On a narrow screen the pictures keep their own shape and the page scrolls instead: one camera at
   a time, full width. The layout picker still chooses which cameras are on the page; it just stops
   deciding how they are stacked.
   These rules come after #grid and .tile deliberately -- they override both, and a media query
   carries no extra weight of its own. */
@media (max-width: 700px), (pointer: coarse) and (max-height: 500px) {
  html, body { height: auto; min-height: 100%; }
  #grid {
    /* flex: 1 stretches the grid to the window, and a stretched grid shares that height out among
       its rows, which beats the tiles' own shape. Let it take the height its contents need. */
    flex: none;
    align-content: start;
    grid-template-columns: 1fr !important;
    grid-template-rows: none !important;
    grid-auto-rows: auto;
  }
  /* the page sets each tile's grid-area from the chosen layout, including the featured ones with a
     large first tile; in a single column there is nothing for those to mean */
  #grid > * { grid-area: auto !important; }
  .tile { aspect-ratio: 16 / 9; min-height: 0; }
}

/* Wide tables on a phone.
   The disk and SMART tables carry six columns of figures. At 375px they cannot fit however they
   are squeezed, and a table that does not fit stretches the whole page sideways -- which breaks
   every other page element rather than just itself. Letting them scroll on their own keeps the
   damage inside the card. */
@media (max-width: 700px), (pointer: coarse) and (max-height: 500px) {
  .hp-table { display: block; overflow-x: auto; -webkit-overflow-scrolling: touch; }
  .hp-table th, .hp-table td { white-space: nowrap; }
  /* the reason each attribute matters is a sentence, and a sentence that may not wrap is a very
     wide column; this one keeps its wrapping and a sensible width */
  .hp-smart-table td.hp-sub { white-space: normal; min-width: 220px; }
  .hp-smart-summary { gap: 8px; }
  .hp-smart-fact { flex: 1 1 100%; max-width: none; }
}

/* The on-screen display this app draws (public/osd-overlay.js). It is a second canvas laid exactly
   over the video canvas, with the same object-fit, so both letterbox the same way and the text sits
   where the settings put it relative to the picture rather than to the tile's black bars. It takes
   no pointer events, so clicking a tile still opens it full size; the drag handler on the playback
   preview listens on the tile itself, not here. Digital zoom transforms the video canvas only, which
   is deliberate: the overlay is an app label, not part of the picture, and should not zoom away. */
.tile canvas.osd { position: absolute; inset: 0; pointer-events: none; z-index: 2; }

/* The overlay placement picker on the Settings page. Nine buttons laid out as the picture itself
   is, so the button you press is in the place the text will appear -- that mapping is the whole
   point of the control, which is why it is a grid and not a drop-down list. */
/* Controls beside the picture they change, rather than above it. The preview used to sit below
   the corner buttons, so on a wide screen you clicked a corner with the result off-screen while
   the right half of the page stayed empty. */
.osd-place { border: 1px solid var(--line, #3a4048); border-radius: 4px; padding: 12px 14px; display: grid; grid-template-columns: minmax(280px, 26rem) minmax(260px, 1fr); gap: 20px; align-items: start; }
.osd-place > legend { padding: 0 4px; }
.osd-controls { min-width: 0; }
.osd-preview { min-width: 0; }
.osd-preview p { margin: 6px 0 0; }
/* one column once there is no room for two: the preview goes under the buttons rather than being
   squeezed to a postage stamp beside them */
@media (max-width: 900px) { .osd-place { grid-template-columns: 1fr; } }
.osd-corners { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 4px; max-width: 420px; }
.osd-corner { padding: 8px 4px; font: inherit; font-size: 12px; cursor: pointer; border: 1px solid var(--line, #3a4048); border-radius: 3px; background: none; color: inherit; }
.osd-corner.on { background: var(--accent, #3d7eff); color: #fff; border-color: transparent; }
.osd-fine { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; margin: 12px 0; }
.osd-fine label { display: flex; flex-direction: column; gap: 2px; font-size: 12px; }
.osd-fine .se-num { width: 100%; }
.osd-fine .st-meta { font-size: 11px; }
.osd-preview canvas { width: 100%; height: auto; border-radius: 3px; display: block; background: #000; }

/* Settings form fields.
   The grid packs as many 220px columns as the window allows, and anything marked se-wide spans the
   lot. On a 1400px screen that stretched the "Settings for" dropdown and the text box clear across
   the page while the two checkboxes sat in narrow columns far apart from each other -- a form that
   is technically responsive and reads as though it were assembled by accident. A field only needs
   to be as wide as what goes in it. */
.se-grid label.se-wide { max-width: 34rem; }
.se-grid label.se-wide select, .se-grid label.se-wide input { max-width: 22rem; }
/* checkboxes belong next to each other, not one per column of a wide grid */
.se-grid label.st-check { grid-column: auto; }

/* The longer explanations, folded away. Every panel opened with two or three paragraphs before the
   first control, which pushed the controls below the fold on a laptop. The sentence that matters
   stays; the rest is here for whoever wants it. */
.se-more { margin: -2px 0 12px; }
.se-more > summary { cursor: pointer; color: var(--muted); font-size: 12px; padding: 2px 0; }
.se-more > summary:hover { color: var(--text); }
.se-more p { margin: 8px 0 0; }

/* Form labels line up on the left with everything else.
   A stray align-items:center was centring both the label text and its field inside the label box,
   so on a wide panel "Settings for" and "Text" floated in the middle of the page while the
   headings, help text and buttons around them were left-aligned. A form reads as a column of
   things to fill in; centring each one breaks the line your eye follows down it. */
.se-grid label:not(.st-check) { align-items: stretch; text-align: left; }
.osd-fine label { align-items: stretch; text-align: left; }

/* In the shell, body is the two-column frame and .app-main is the page's own column. The rules above
   made body that column; these hand the job over without editing every page. */
body.has-shell { display: grid; }
body.has-shell .app-main > header { background: none; border-bottom: 1px solid var(--border); }
/* tablet widths: the tools get a row of their own instead of being squeezed beside the transport
   into a column of one button per line */
@media (max-width: 1100px) and (min-width: 641px) {
  .pb-tools { flex-basis: 100%; justify-content: center; }
}
/* a phone's tapped camera, full screen: edge to edge, no gap round it */
#grid:fullscreen > .tile.single-overlay { inset: 0; border-radius: 0; }
/* Health: the alert history folds away under its heading */
.hp-history > summary { cursor: pointer; list-style: none; }
.hp-history > summary::-webkit-details-marker { display: none; }
.hp-history > summary h2 { display: inline-flex; align-items: center; gap: 8px; }
.hp-history > summary h2::before { content: '▸'; color: var(--muted); font-size: 0.8em; transition: transform var(--t-fast); }
.hp-history[open] > summary h2::before { transform: rotate(90deg); }
.hp-count { color: var(--muted); font-weight: 400; font-size: 0.85em; }
/* Recording storage gone: the loudest thing on the page, on every page */
.alert-lines { display: flex; flex-direction: column; gap: 4px; }
.alert-banner.critical { background: var(--bad); color: #fff; border-bottom: 0; padding: 12px 16px; }
.alert-banner.critical .alert-critical { font-size: 15px; }
.alert-banner.critical a { color: #fff; font-weight: 600; text-decoration: underline; }
/* Settings: one tab at a time */
.se-tabs { flex-shrink: 0; display: flex; gap: 4px; border-bottom: 1px solid var(--border); overflow-x: auto; scrollbar-width: none; }
.se-tabs a { padding: 10px 14px; color: var(--muted); text-decoration: none; font-weight: 500; white-space: nowrap; border-bottom: 2px solid transparent; margin-bottom: -1px; }
.se-tabs a:hover { color: var(--text); }
.se-tabs a[aria-current] { color: var(--text); border-bottom-color: var(--accent); }
/* narrow screens: the tabs wrap onto a second row rather than scrolling out of sight (a phone could
   not see the Server tab, and nothing showed the row scrolled) */
@media (max-width: 699px), (pointer: coarse) and (max-height: 500px) {
  .se-tabs { flex-wrap: wrap; overflow-x: visible; row-gap: 0; }
  .se-tabs a { padding: 9px 10px; }
}
.se-tab-help { flex-shrink: 0; margin: 0; color: var(--muted); font-size: 13px; }
.se-section.se-off { display: none; }
/* page-tabs.js: the sections of the tabs not showing, on any page */
main > section[data-tab].tab-off { display: none !important; }
/* Sites: a block per site, a row per NVR */
.st-site { display: flex; flex-direction: column; gap: 8px; }
.st-site + .st-site { margin-top: 12px; }
.st-site-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 12px; }
.st-site-head h2 { margin: 0; font-size: 17px; }
.st-site-head .st-meta { margin: 2px 0 0; }
.st-nvrs { background: var(--surface-1); border: 1px solid var(--border); border-radius: var(--r-md); overflow: hidden; }
.st-nvr { display: grid; grid-template-columns: 12px minmax(0, 1fr) 150px auto; gap: 14px; align-items: center; padding: 12px 16px; }
.st-nvr + .st-nvr { border-top: 1px solid var(--border); }
.st-nvr:hover { background: var(--surface-hover); }
.st-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--text-faint); }
.st-dot.st-online { background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
.st-dot.st-connecting { background: var(--warn); box-shadow: 0 0 0 3px var(--warn-soft); }
.st-dot.st-offline { background: var(--bad); box-shadow: 0 0 0 3px var(--bad-soft); }
.st-nvr-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.st-nvr-name { font-weight: 600; }
.st-nvr-main .st-meta, .st-nvr-main p { margin: 0; overflow: hidden; text-overflow: ellipsis; }
.st-nvr-cams { display: flex; flex-direction: column; gap: 5px; font-variant-numeric: tabular-nums; font-size: 13px; }
.st-nvr-offline .st-nvr-cams { color: var(--bad); }
.st-bar { display: block; height: 4px; border-radius: 2px; background: var(--surface-3); overflow: hidden; }
.st-bar > span { display: block; height: 100%; background: var(--ok); }
.st-nvr-actions { display: flex; gap: 6px; justify-content: flex-end; }
.st-row-remove { color: var(--text-muted); }
.st-row-remove:hover { color: var(--bad); }
@media (max-width: 760px) {
  .st-nvr { grid-template-columns: 12px minmax(0, 1fr); }
  .st-nvr-cams, .st-nvr-actions { grid-column: 2; }
  .st-nvr-actions { justify-content: flex-start; flex-wrap: wrap; }
}
/* Settings, per camera: grouped by NVR, folded, one line per camera */
.se-cam-tools { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; }
.se-cam-tools input[type="search"] { flex: 1; min-width: 180px; max-width: 320px; }
.se-cam-group td { padding: 0 !important; background: var(--surface-2); }
.se-cam-toggle { all: unset; box-sizing: border-box; display: flex; gap: 8px; align-items: baseline; width: 100%; padding: 9px 10px; cursor: pointer; }
.se-cam-toggle:hover { background: var(--surface-hover); }
.se-cam-toggle:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
.se-caret { color: var(--text-muted); width: 10px; }
.se-cam-name { white-space: nowrap; }
.se-cam-name .st-meta { display: inline-block; min-width: 22px; font-variant-numeric: tabular-nums; }
.se-table td select, .se-table td input { padding: 4px 6px; font-size: 13px; }

/* ---- Playback, full screen: the picture fills the screen, the controls float on glass ---- */
body.playback-page.is-fullscreen .app-shell, body.playback-page.is-fullscreen .shell-bar,
body.playback-page.is-fullscreen .app-main > header { display: none; }
body.playback-page.is-fullscreen { grid-template-columns: minmax(0, 1fr); padding: 0; background: #000; }
body.playback-page.is-fullscreen .app-main { position: relative; overflow: hidden; }
body.playback-page.is-fullscreen .pb-main { flex: 1; height: 100%; }
body.playback-page.is-fullscreen .pb-video { height: 100%; border-radius: 0; }
body.playback-page.is-fullscreen .pb-bar {
  /* always the dark glass, whichever theme the rest of the app is in */
  --text: #f2f4f7; --text-muted: rgba(242, 244, 247, 0.7); --muted: rgba(242, 244, 247, 0.7);
  --surface-2: rgba(255, 255, 255, 0.08); --surface-3: rgba(255, 255, 255, 0.14); --surface-hover: rgba(255, 255, 255, 0.12);
  --border: rgba(255, 255, 255, 0.16); --panel: transparent; --tl-track: rgba(255, 255, 255, 0.08);
  color-scheme: dark; color: var(--text);
  position: absolute; left: 50%; bottom: 18px; transform: translateX(-50%);
  width: min(1180px, calc(100% - 36px)); max-height: 60%; overflow: auto;
  padding: 12px 16px; border-radius: 16px;
  background: linear-gradient(180deg, rgba(40, 44, 52, 0.55), rgba(18, 20, 24, 0.62));
  border: 1px solid rgba(255, 255, 255, 0.14);
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.12);
  -webkit-backdrop-filter: blur(18px) saturate(150%); backdrop-filter: blur(18px) saturate(150%);
  transition: opacity 0.35s ease, transform 0.35s ease;
  z-index: 20;
}
body.playback-page.is-fullscreen .pb-bar button, body.playback-page.is-fullscreen .pb-bar select,
body.playback-page.is-fullscreen .pb-bar input[type="date"] {
  background: rgba(255, 255, 255, 0.08); border-color: rgba(255, 255, 255, 0.14); color: var(--text);
}
body.playback-page.is-fullscreen .pb-bar button:hover { background: rgba(255, 255, 255, 0.16); }
body.playback-page.is-fullscreen .pb-search, body.playback-page.is-fullscreen .pb-bookmarks {
  position: absolute; top: 18px; right: 18px; bottom: auto; max-height: calc(100% - 36px); z-index: 21;
  background: rgba(18, 20, 24, 0.62); border: 1px solid rgba(255, 255, 255, 0.14); border-radius: 16px;
  -webkit-backdrop-filter: blur(18px) saturate(150%); backdrop-filter: blur(18px) saturate(150%);
}
/* idle: the glass steps aside and the cursor goes with it */
body.playback-page.is-fullscreen.pb-idle .pb-bar { opacity: 0; transform: translate(-50%, 12px); pointer-events: none; }
body.playback-page.is-fullscreen.pb-idle { cursor: none; }
@media (prefers-reduced-motion: reduce) { body.playback-page.is-fullscreen .pb-bar { transition: none; } }
/* the glass is kept short: the colour key and the help line are for the page, not the picture */
body.playback-page.is-fullscreen .pb-legend { display: none; }
body.playback-page.is-fullscreen .pb-bar { gap: 6px; padding: 10px 14px; }
body.playback-page.is-fullscreen .pb-timeline { height: 48px; }
.login-brand { display: flex; align-items: center; gap: 12px; }
.login-brand img { border-radius: 10px; }
/* Settings > Storage: the rarely needed parts folded under their heading */
.se-fold > summary { cursor: pointer; list-style: none; display: flex; align-items: center; gap: 8px; }
.se-fold > summary::-webkit-details-marker { display: none; }
.se-fold > summary::before { content: '▸'; color: var(--text-muted); font-size: 12px; transition: transform var(--t-fast); }
.se-fold[open] > summary::before { transform: rotate(90deg); }
.se-fold > summary h2 { margin: 0; }
.se-fold[open] > summary { margin-bottom: 10px; }
.se-add > summary { color: var(--accent-strong); font-weight: 500; }
.sr-cams > summary { cursor: pointer; color: var(--text-muted); font-size: 13px; margin-top: 8px; }

/* ---- Playback: the simple bar ---- */
.pb-simple { justify-content: space-between; align-items: center; }
.pb-simple .pb-transport { gap: 8px; grid-column: 2; justify-content: center; }
.pb-simple .pb-play { width: 44px; height: 44px; border-radius: 50%; font-size: 18px; background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.pb-speed-cycle { min-width: 46px; font-variant-numeric: tabular-nums; font-weight: 600; }
.pb-simple .pb-tools { flex: 0 0 auto; gap: 4px; grid-column: 3; justify-self: end; }
.pb-icon { width: 38px; height: 38px; padding: 0; display: inline-grid; place-items: center; border-radius: 10px; }
.pb-icon .icon { width: 20px; height: 20px; }
.pb-icon[aria-pressed="true"], .pb-icon[aria-expanded="true"] { color: var(--accent-strong); border-color: var(--accent); }
.pb-export:disabled { display: none; }
.pb-more { display: flex; flex-direction: column; gap: 10px; padding: 12px; border: 1px solid var(--border); border-radius: 12px; background: var(--surface-2); }
.pb-more[hidden] { display: none; }
.pb-more-row { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.pb-more-label { color: var(--text-muted); font-size: 12px; min-width: 44px; }
.pb-more .pb-speeds { flex: 0 1 auto; }
.pb-more .pb-legend { display: flex; }
@media (max-width: 640px) {
  .pb-simple { flex-direction: row !important; flex-wrap: wrap; }
  .pb-simple .pb-transport, .pb-simple .pb-tools { order: 0 !important; }
}
.pb-follow[hidden] { display: none !important; }

/* ---- Health: at a glance ---- */
.hp-hero { display: flex; flex-wrap: wrap; align-items: center; gap: 14px; padding: 16px 18px; border-radius: var(--r-lg); border: 1px solid var(--border); background: var(--surface-1); }
.hp-hero.ok { border-color: color-mix(in srgb, var(--ok) 40%, var(--border)); background: color-mix(in srgb, var(--ok) 8%, var(--surface-1)); }
.hp-hero.warn { border-color: color-mix(in srgb, var(--warn) 45%, var(--border)); background: color-mix(in srgb, var(--warn) 9%, var(--surface-1)); }
.hp-hero.bad { border-color: var(--bad); background: color-mix(in srgb, var(--bad) 14%, var(--surface-1)); }
.hp-hero-dot { width: 14px; height: 14px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 5px var(--ok-soft); flex: none; }
.hp-hero.warn .hp-hero-dot { background: var(--warn); box-shadow: 0 0 0 5px var(--warn-soft); }
.hp-hero.bad .hp-hero-dot { background: var(--bad); box-shadow: 0 0 0 5px var(--bad-soft); }
.hp-hero-text { display: flex; flex-direction: column; gap: 2px; min-width: 200px; flex: 1; }
.hp-hero-text strong { font-size: 20px; }
.hp-hero-text span { color: var(--text-muted); font-size: 13px; }
.hp-hero-tags { display: flex; flex-wrap: wrap; gap: 6px; flex-basis: 100%; }
.hp-hero-tags:empty { display: none; }
.hp-tag { padding: 3px 10px; border-radius: 999px; font-size: 12px; font-weight: 500; }
.hp-tag.warn { background: var(--warn-soft); color: var(--warn); }
.hp-tag.bad { background: var(--bad-soft); color: var(--bad); }
.hp-meter { display: block; height: 5px; border-radius: 3px; background: var(--surface-3); overflow: hidden; margin: 6px 0 2px; }
.hp-meter > span { display: block; height: 100%; border-radius: 3px; background: var(--ok); }
.hp-meter.warn > span { background: var(--warn); }
.hp-meter.bad > span { background: var(--bad); }
.hp-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--ok); flex: none; }
.hp-dot.warn { background: var(--warn); }
.hp-dot.bad { background: var(--bad); }
#nvrs { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr)); gap: 12px; align-items: start; }
.nvr-tile { padding: 0 !important; }
.nvr-tile[open] { grid-column: 1 / -1; }
.nvr-tile > :not(summary) { margin-left: 16px; margin-right: 16px; }
.nvr-tile > :last-child { margin-bottom: 16px; }
.nvr-sum { list-style: none; cursor: pointer; padding: 14px 16px; display: flex; flex-direction: column; gap: 10px; border-radius: inherit; }
.nvr-sum::-webkit-details-marker { display: none; }
.nvr-sum:hover { background: var(--surface-hover); }
.nvr-sum-head { display: flex; align-items: center; gap: 10px; }
.nvr-sum-head strong { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nvr-stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; }
.nvr-stat { display: flex; flex-direction: column; min-width: 0; }
.nvr-stat .lbl { color: var(--text-muted); font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; }
.nvr-stat .val { font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.nvr-stat.warn .val { color: var(--warn); }
.nvr-stat.bad .val { color: var(--bad); }
.nvr-tile .nvr-why { color: var(--bad); font-size: 12px; }
/* the Health page says it all in its own headline */
body.health-page .alert-banner { display: none; }
/* the transport sits in the middle of the bar, the icons on the right */
.pb-row.pb-simple { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; }
@media (max-width: 640px) {
  .pb-row.pb-simple { grid-template-columns: 1fr; justify-items: center; gap: 8px; }
  .pb-simple .pb-transport, .pb-simple .pb-tools { grid-column: 1; justify-self: center; }
}
/* Playback: One camera | Many cameras */
.pb-mode { display: inline-flex; padding: 3px; gap: 2px; border-radius: 10px; background: var(--surface-2); border: 1px solid var(--border); }
.pb-mode button { border: 0; background: none; color: var(--text-muted); padding: 5px 12px; border-radius: 7px; font-weight: 500; }
.pb-mode button[aria-pressed="true"] { background: var(--surface-3); color: var(--text); box-shadow: var(--shadow); }
.pb-mode button:hover:not([aria-pressed="true"]) { color: var(--text); }

/* ---- Every page's top bar: one tidy row ---- */
body.has-shell .app-main > header {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px;
  min-height: 56px; padding: 8px 16px; background: var(--bg); border-bottom: 1px solid var(--border);
}
body.has-shell .app-main > header .controls { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; flex: 1; min-width: 0; }
body.has-shell .app-main > header .brand:empty, body.has-shell .app-main > header .account { display: none; }
/* Health/Alarms/Settings/Reports keep only the (hidden) brand + account in the header, so it was a
   blank 56px bar above the content. With no real controls there is nothing to show: hide the bar. */
body.has-shell .app-main > header:not(:has(.controls)) { display: none; }
body.has-shell .app-main > header label { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; color: var(--text-muted); white-space: nowrap; }
body.has-shell .app-main > header button { white-space: nowrap; }
body.has-shell .app-main > header select, body.has-shell .app-main > header input[type="date"],
body.has-shell .app-main > header button:not(.pb-mode button) { height: 32px; padding-top: 0; padding-bottom: 0; border-radius: 8px; }
body.has-shell .app-main > header .wall-pager, body.has-shell .app-main > header #pager { display: inline-flex; align-items: center; gap: 6px; color: var(--text-muted); font-size: 13px; }
body.has-shell .app-main > header .wall-pager[hidden] { display: none; }
/* Live: tiles can be dragged to rearrange them */
@media (pointer: fine) { #grid .tile { cursor: grab; } #grid .tile:active { cursor: grabbing; } }
.live-tip { position: fixed; right: 20px; bottom: 20px; z-index: 60; max-width: 360px; display: flex; gap: 12px; align-items: center;
  padding: 12px 14px; border-radius: 12px; background: var(--surface-1); border: 1px solid var(--border); box-shadow: 0 8px 30px rgba(0,0,0,.35); font-size: 13px; }
.live-tip button { flex: none; }
/* a phone's tapped camera covers the whole screen, with or without the browser's full screen */
body.phone-full #grid { position: fixed; inset: 0; z-index: 90; background: #000; padding: 0; overflow: hidden; }
body.phone-full .shell-bar, body.phone-full .shell-sheet, body.phone-full .alert-banner, body.phone-full .live-tip { display: none !important; }
body.phone-full .tile.single-overlay canvas { object-fit: contain; }
body.phone-full #grid > .tile.single-overlay { position: fixed; inset: 0; border-radius: 0; width: 100vw; height: 100vh; height: 100dvh; aspect-ratio: auto; max-height: none; z-index: 91; }

/* ---- Phones: a slim glass top bar, and only what matters under it ---- */
.shell-top { display: none; }
@media (max-width: 699px), (pointer: coarse) and (max-height: 500px) {
  .shell-top {
    display: flex; align-items: center; gap: 10px; position: sticky; top: 0; z-index: 40;
    padding: calc(10px + env(safe-area-inset-top)) 16px 10px;
    background: color-mix(in srgb, var(--bg) 72%, transparent);
    -webkit-backdrop-filter: blur(18px) saturate(160%); backdrop-filter: blur(18px) saturate(160%);
    border-bottom: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
  }
  .shell-top img { width: 28px; height: 28px; border-radius: 8px; }
  .shell-top strong { font-size: 17px; letter-spacing: 0.01em; }
  .shell-top span { margin-left: auto; color: var(--text-muted); font-size: 14px; }
  /* the alert: one line, tap for the rest */
  .alert-banner { padding: 8px 14px !important; font-size: 13px; cursor: pointer; }
  .alert-banner:not(.open) .alert-lines > * { display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden; }
  .alert-banner:not(.open) .alert-lines > * + * { display: none; }
  .alert-banner.critical .alert-critical { font-size: 13px; }
  /* page bars: compact, and the rarely used switches left to bigger screens */
  body.has-shell .app-main > header { min-height: 0; padding: 8px 12px; gap: 8px; }
  body.live-page .app-main > header label:has(#smooth),
  body.live-page .app-main > header #fullscreen, body.live-page .app-main > header label:has(#hideOffline) { display: none !important; }
}

/* the camera's own full-screen button (a phone): the phone's native player, like a YouTube video */
.phone-fs-btn { display: none; }
@media (max-width: 899px) and (pointer: coarse), (pointer: coarse) and (max-height: 500px) {
  .single-overlay .phone-fs-btn { display: grid; place-items: center; position: absolute; right: 12px; top: 12px; z-index: 5;
    width: 44px; height: 44px; border-radius: 12px; border: 1px solid rgba(255,255,255,.2); color: #fff;
    background: rgba(20,22,26,.45); -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px); font-size: 20px; }
}
body.has-shell .app-main > header, .app-main > .shell-top, .app-main > .alert-banner { flex-shrink: 0; }
@media (max-width: 699px), (pointer: coarse) and (max-height: 500px) {
  body.has-shell .app-main > header > * { max-width: 100%; }
  body.has-shell .app-main > header .pb-mode { display: flex; width: 100%; }
  body.has-shell .app-main > header .pb-mode button { flex: 1; }
  body.has-shell .app-main > header .controls, body.playback-page .app-main > header > div:not(.pb-mode) { width: 100%; }
  body.has-shell .app-main > header select { max-width: 60vw; }
}

/* a phone's 2 x 2: two cameras across, the page scrolling to the second row */
@media (max-width: 700px), (pointer: coarse) and (max-height: 500px) {
  body.phone-g2 #grid { grid-template-columns: 1fr 1fr !important; }
  body.phone-g2 .tile .name { font-size: 11px; }
}
/* the ‹ › on the full-size camera: every screen (a PC had no way to step; ← → keys as well) */
.phone-step { display: none; }
.single-overlay .phone-step {
  display: grid; place-items: center; position: absolute; top: 50%; z-index: 5; transform: translateY(-50%);
  width: 40px; height: 56px; padding: 0 0 4px; border-radius: 14px; border: 1px solid rgba(255,255,255,.18);
  background: rgba(20,22,26,.35); color: rgba(255,255,255,.85); font-size: 30px; line-height: 1; cursor: pointer;
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
  animation: phone-step-hint 2.4s ease-out 1;
}
.single-overlay .phone-step:hover { background: rgba(20,22,26,.6); color: #fff; }
.single-overlay .phone-step-prev { left: 10px; }
.single-overlay .phone-step-next { right: 10px; }
/* A phone's full-size camera covers the whole screen (viewport-fit=cover), status bar and notch
   included: what sits at its edges keeps clear of the clock, the notch and the home bar (the LIVE
   badge was under the iPhone's clock in portrait). env() is 0 where there is nothing to avoid. */
body.phone-full .single-overlay .status { top: calc(8px + env(safe-area-inset-top)); left: calc(8px + env(safe-area-inset-left)); }
body.phone-full .single-overlay > .phone-fs-btn { top: calc(12px + env(safe-area-inset-top)); right: calc(12px + env(safe-area-inset-right)); }
body.phone-full .single-overlay .phone-step-prev { left: calc(10px + env(safe-area-inset-left)); }
body.phone-full .single-overlay .phone-step-next { right: calc(10px + env(safe-area-inset-right)); }
body.phone-full .single-overlay .label { padding-left: calc(8px + env(safe-area-inset-left)); padding-right: calc(8px + env(safe-area-inset-right)); padding-bottom: calc(12px + env(safe-area-inset-bottom)); }
/* a small nudge when the camera opens, so the eye finds them */
@keyframes phone-step-hint { 0%, 60% { opacity: 1; } 30% { opacity: .5; } 100% { opacity: .75; } }
.phone-fs-note { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 6; max-width: 80%;
  padding: 10px 14px; border-radius: 12px; background: rgba(20,22,26,.75); color: #fff; font-size: 13px; text-align: center;
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); }
.single-overlay canvas { position: relative; z-index: 1; }
/* the full-size camera's own controls stay above its picture (the picture was raised a layer over
   the hidden iPhone-player copy, and took the name bar, Recordings and Picture under it) */
.single-overlay > .label, .single-overlay > .stats, .single-overlay > canvas.osd { z-index: 4; }
/* The picture's canvas is GPU-composited on its own layer. The control strip has no transform or
   backdrop-filter, so it is NOT promoted and paints into the tile's base layer -- BELOW the canvas's
   layer -- so the name, Picture and Lines sat behind the picture (only OSD, over the black bar where
   the canvas has no pixels, showed through) until any resize re-composited it. Promote the strip to
   its own layer so z-index 4 wins. The other controls (status, arrows, fullscreen) carry a
   backdrop-filter and are already layered, so they are left alone (translateZ there can break blur). */
.single-overlay > .label, .single-overlay > .stats { transform: translateZ(0); }
.single-overlay > .tile-upgrade { z-index: 2; }
.single-overlay > .phone-fs-btn, .single-overlay > .phone-step, .single-overlay > .phone-fs-note { z-index: 6; }
/* phones: the camera list */
.phone-list-btn { display: none; }
.cam-sheet { position: fixed; inset: 0; z-index: 95; display: flex; flex-direction: column; background: color-mix(in srgb, var(--bg) 88%, transparent);
  -webkit-backdrop-filter: blur(20px) saturate(160%); backdrop-filter: blur(20px) saturate(160%); padding-top: env(safe-area-inset-top); }
.cam-sheet[hidden] { display: none; }
.cam-sheet-head { display: flex; gap: 8px; padding: 12px 14px; border-bottom: 1px solid var(--border); }
.cam-sheet-head input { flex: 1; height: 40px; border-radius: 10px; padding: 0 12px; font-size: 16px; }
.cam-sheet-close { width: 40px; height: 40px; border-radius: 10px; font-size: 16px; }
.cam-sheet-list { flex: 1; overflow-y: auto; padding: 6px 0 calc(20px + env(safe-area-inset-bottom)); -webkit-overflow-scrolling: touch; }
.cam-sheet-site { padding: 14px 16px 6px; font-size: 12px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--text-muted); }
.cam-sheet-row { all: unset; box-sizing: border-box; display: flex; align-items: center; gap: 12px; width: 100%; padding: 13px 16px; border-bottom: 1px solid color-mix(in srgb, var(--border) 60%, transparent); font-size: 15px; cursor: pointer; }
.cam-sheet-row:active { background: var(--surface-hover); }
.cam-sheet-row .cam-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); flex: none; }
.cam-sheet-row.off { opacity: .45; }
.cam-sheet-row.off .cam-dot { background: var(--text-faint); box-shadow: none; }
.cam-sheet-row .cam-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cam-sheet-row .cam-ch { color: var(--text-muted); font-size: 13px; }
@media (max-width: 699px), (pointer: coarse) and (max-height: 500px) { .phone-list-btn { display: inline-flex; align-items: center; font-weight: 600; } }
/* phones: the list of cameras, as cards (the redesign's phone mockup) */
@media (max-width: 700px), (pointer: coarse) and (max-height: 500px) {
  body.phone-list #grid { gap: 14px; padding: 12px 12px 24px; }
  body.phone-list #grid > .tile { border-radius: 16px; box-shadow: 0 6px 20px rgba(0,0,0,.25); }
  /* the list has 48 cells; with a site chosen (or few cameras) the rest were black boxes all the way
     down, which looked as if the site filter had done nothing */
  body.phone-list .tile.tile-empty { display: none; }
  body.phone-list .tile .label { padding: 22px 12px 10px; background: linear-gradient(to top, rgba(0,0,0,.72), transparent); font-size: 14px; }
  body.phone-list .tile .label .name { font-weight: 600; }
  body.phone-list .tile:has(.dot-live, .dot-rec)::before {
    content: 'LIVE'; position: absolute; top: 10px; left: 10px; z-index: 3; padding: 3px 8px; border-radius: 6px;
    font-size: 11px; font-weight: 700; letter-spacing: .06em; color: #fff; background: rgba(229, 72, 77, .92);
  }
  body.phone-list .tile.offline { opacity: .5; }
}
/* a tile's last-seen still, shown until its live picture arrives (stills.js) */
.tile.has-still { background-size: contain; background-position: center; background-repeat: no-repeat; }
.tile.has-still::after { content: ''; position: absolute; inset: 0; background: rgba(0,0,0,.28); pointer-events: none; z-index: 0; }
.tile.has-still > canvas:not(.osd):not(.ln-overlay):not(.ln-loupe) { visibility: hidden; } /* the player canvas is opaque black until its first frame; the Lines panel's drawing canvases in the same tile stay (a hidden one takes no pointer) */
/* Health: the live indicator */
.hp-live { display: inline-flex; align-items: center; gap: 6px; align-self: flex-end; font-size: 12px; color: var(--text-muted); }
.hp-live::before { content: ''; width: 8px; height: 8px; border-radius: 50%; background: var(--ok); }
.hp-live.pulse::before { animation: hp-pulse 1.2s ease-out 1; }
.hp-live.stale { color: var(--warn); }
.hp-live.stale::before { background: var(--text-faint); }
@keyframes hp-pulse { 0% { box-shadow: 0 0 0 0 var(--ok-soft); } 100% { box-shadow: 0 0 0 8px transparent; } }

/* The bandwidth-easing chip (live-tile.js): a lighter picture on a busy link reads as deliberate. One
   per page, top-centre, out of the way of the controls; it comes and goes with the server's easing. */
.ease-chip {
  position: fixed; z-index: 95;
  top: calc(10px + env(safe-area-inset-top)); left: 50%; transform: translateX(-50%);
  padding: 5px 13px; border-radius: 999px;
  background: rgba(20, 22, 26, .82); color: var(--text);
  font-size: 12px; font-weight: 600; letter-spacing: .02em;
  border: 1px solid var(--border);
  -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
  box-shadow: 0 2px 10px rgba(0, 0, 0, .35); pointer-events: none;
}

/* ---- Reports (reports.html) ---- */
.rp-main { max-width: 1100px; }
.rp-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px; }
.rp-head h2 { margin: 0; font-size: 18px; }
.rp-periods { display: inline-flex; gap: 4px; background: var(--panel); border-radius: 10px; padding: 3px; }
.rp-periods button { border: 0; background: transparent; padding: 6px 12px; border-radius: 8px; color: var(--muted); }
.rp-periods button[aria-pressed="true"] { background: var(--bg); color: var(--text); font-weight: 600; }
.rp-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; }
.rp-card { background: var(--panel); border-radius: 10px; padding: 12px 14px; display: flex; flex-direction: column; gap: 2px; }
.rp-card b { font-size: 24px; font-weight: 650; font-variant-numeric: tabular-nums; }
.rp-card span { color: var(--muted); font-size: 12px; }
.rp-card.warn b { color: var(--warn, #d29922); }
.rp-two { display: grid; grid-template-columns: 1fr; gap: 14px; }
@media (min-width: 800px) { .rp-two { grid-template-columns: 1fr 1fr; } }
.rp-table td:last-child, .rp-table th:last-child { text-align: right; }
.rp-share { display: inline-flex; align-items: center; gap: 8px; min-width: 160px; font-variant-numeric: tabular-nums; }
.rp-bar { flex: 1; height: 6px; border-radius: 3px; background: var(--border); overflow: hidden; min-width: 80px; }
.rp-bar i { display: block; height: 100%; background: var(--live, #2ea043); }
.rp-bar.mostly i { background: var(--warn, #d29922); }
.rp-bar.poor i { background: var(--danger, #da3633); }
.rp-all > summary { cursor: pointer; list-style: none; }
.rp-all > summary::-webkit-details-marker { display: none; }
.rp-all > summary h2 { display: inline; }

/* Settings > Alerts: webhooks */
.a-hooks { display: flex; flex-direction: column; gap: 6px; margin: 6px 0; }
.a-hook { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 1fr) auto; gap: 6px; }
@media (max-width: 600px) { .a-hook { grid-template-columns: 1fr; } }

/* playback: every control says what it is under its symbol (the owner: "hard to know what is what") */
.pb-capped { display: inline-flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; min-width: 56px; padding-top: 4px; padding-bottom: 4px; line-height: 1; }
.pb-capped .pb-glyph { display: inline-flex; align-items: center; justify-content: center; line-height: 1; }
.pb-capped .pb-cap { font-size: 10.5px; font-weight: 500; color: var(--text-muted, var(--muted)); white-space: nowrap; letter-spacing: 0.01em; }
.pb-capped[aria-pressed="true"] .pb-cap { color: inherit; }
@media (max-width: 420px) { .pb-capped { min-width: 44px; } .pb-capped .pb-cap { font-size: 9.5px; } }

/* ---- Users & access: the access editor (audit.js draws it, access-model.js decides) ---- */
.ac-dialog { width: min(780px, calc(100vw - 32px)); max-height: calc(100vh - 32px); max-height: calc(100dvh - 32px); }
.ac-dialog .ac-admin { flex-direction: row; align-items: center; gap: 8px; font-size: var(--fs-body); color: var(--text); }
.ac-dialog .ac-admin small, .ac-tree small { color: var(--text-muted); font-size: var(--fs-meta); font-weight: 400; }
.ac-dialog .ac-admin small { margin-left: 6px; }
.ac-body { display: flex; flex-direction: column; gap: 10px; min-width: 0; }
.ac-body[hidden] { display: none; }
.ac-tools { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; }
.ac-dialog .ac-copy { margin-left: auto; flex-direction: row; align-items: center; gap: 8px; }
/* the tree scrolls inside the dialog, its column heads staying in view */
.ac-tree-wrap { max-height: min(52vh, 520px); overflow: auto; border: 1px solid var(--border); border-radius: var(--r-md); }
.ac-tree { width: 100%; border-collapse: collapse; font-size: var(--fs-dense); }
.ac-tree thead th { position: sticky; top: 0; z-index: 1; background: var(--surface-2); color: var(--text-muted); font-weight: 500; padding: 6px 8px; text-align: center; white-space: nowrap; }
.ac-tree thead th.ac-rowhead { text-align: left; }
/* the tick columns keep room for a note such as "NVR recordings only" on one line */
.ac-tree thead th.ac-col { width: 116px; }
/* two header rows (audit.js, from access-model.js HEAD_ROWS): the second sticks under the first */
.ac-tree thead tr:nth-child(2) th { top: var(--ac-head-row, 29px); font-size: var(--fs-meta); }
.ac-tree thead th.ac-group { border-bottom: 1px solid var(--border); }
/* .ac-tree: `.ac-tree small { display: block }` below would show them on wide screens too */
.ac-tree .ac-row-notes { display: none; color: var(--warn); font-size: 11px; line-height: 1.25; }
.ac-tree tbody th, .ac-tree td { padding: 6px 8px; border-top: 1px solid var(--border); vertical-align: middle; }
.ac-tree tbody th { text-align: left; font-weight: 400; }
.ac-tree td { text-align: center; }
.ac-tree .ac-name { overflow-wrap: anywhere; }
.ac-tree small { display: block; }
.ac-all th, .ac-all td { background: var(--surface-2); }
.ac-all .ac-name, .ac-site .ac-name { font-weight: 600; }
.ac-site .ac-head { display: grid; grid-template-columns: 24px 1fr; column-gap: 4px; align-items: center; }
.ac-site .ac-head small { grid-column: 2; }
.ac-cam th { padding-left: 36px; }
.ac-cam[hidden] { display: none; }
.ac-fold { width: 24px; height: 24px; padding: 0; border: 0; background: none; color: var(--text-muted); font-size: 12px; line-height: 1; transition: transform var(--t-fast); }
.ac-fold[aria-expanded="true"] { transform: rotate(90deg); }
.ac-fold:disabled { visibility: hidden; }
.ac-tree input[type="checkbox"] { width: 18px; height: 18px; margin: 0; accent-color: var(--accent); cursor: pointer; }
/* what a tick alone would hide ("NVR recordings only"), right under the box */
.ac-note { display: block; max-width: 13em; margin: 2px auto 0; color: var(--warn); font-size: 11px; line-height: 1.25; }
.ac-note:empty { display: none; }
.ac-formats { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px; margin: 0; padding: 8px 12px; border: 1px solid var(--border); border-radius: var(--r-md); }
.ac-formats legend { padding: 0 4px; color: var(--text-muted); font-size: var(--fs-dense); }
.ac-format-list { display: flex; flex-wrap: wrap; gap: 8px 16px; }
.ac-kept h3 { margin: 0 0 4px; font-size: var(--fs-body); }
.ac-kept ul { list-style: none; margin: 6px 0 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.ac-kept li { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 6px 10px; border-radius: var(--r-sm); background: var(--warn-soft); font-size: var(--fs-dense); }
.ac-warn { display: flex; flex-direction: column; gap: 6px; }
.ac-warn:empty { display: none; }
.ac-dirty { margin-right: auto; align-self: center; }
.ac-user-actions { white-space: nowrap; text-align: right; }
@media (max-width: 560px) {
  /* max-width too: the browser caps a modal dialog at 100% - 2em - 6px */
  .ac-dialog { width: calc(100vw - 16px); max-width: calc(100vw - 16px); padding: 10px; }
  .ac-tree thead th.ac-col { width: 44px; }
  .ac-tree td { padding: 6px 2px; }
  .ac-tree input[type="checkbox"] { width: 22px; height: 22px; }
  .ac-cam th { padding-left: 12px; }
  .ac-dialog .ac-copy { margin-left: 0; }
  /* five boxes of 44 px leave no room for a note under each: they are said in the row's header */
  .ac-tree td .ac-note { display: none; }
  .ac-row-notes:not(:empty) { display: block; }
  /* the Rights table: one card per user */
  .rights-table, .rights-table tbody, .rights-table tr, .rights-table td { display: block; }
  .rights-table thead { display: none; }
  .rights-table tr { margin-bottom: 8px; border: 1px solid var(--border); border-radius: 8px; }
  .rights-table td { display: grid; grid-template-columns: 8.5em 1fr; gap: 8px; padding: 4px 10px; white-space: normal; border-bottom: 0; }
  .rights-table td::before { content: attr(data-label); color: var(--text-muted); }
}
