: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) { .controls { flex-wrap: wrap; } }

select, button {
  background: #232931;
  color: var(--text);
  border: 1px solid #2f3742;
  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; }
.tile .status { color: var(--muted); white-space: nowrap; }
.tile .status.live::before {
  content: "";
  display: inline-block;
  width: 7px; height: 7px;
  margin-right: 5px;
  border-radius: 50%;
  background: var(--live);
  vertical-align: 1px;
}
.tile.offline { cursor: default; }
.tile.offline .status { color: var(--offline); }
.tile.single { cursor: zoom-out; }

#notice {
  margin: 0;
  padding: 8px 16px;
  background: #3a2a10;
  color: #ffcf87;
}

.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 #2f3742;
  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: 4px; 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; }
.tabs a {
  color: var(--muted);
  text-decoration: none;
  padding: 4px 10px;
  border-radius: 6px;
}
.tabs a:hover { color: var(--text); background: #232931; }
.tabs a[aria-current="page"] { color: var(--text); background: #232931; }
.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: #232931;
  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 #2f3742; 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: #232931;
  color: var(--text);
  border: 1px solid #2f3742;
  border-radius: 6px;
  padding: 3px 8px;
  font: inherit;
  color-scheme: 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: #232931; border-color: #2f3742; 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: #3a2a10; color: #ffcf87; 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 #2f3742;
  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 #2f3742;
  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; }
.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; }
.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; }
.map-empty {
  position: absolute;
  inset: 0;
  z-index: 3;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  padding: 24px;
  text-align: center;
  color: var(--muted);
  cursor: default;
}
.map-empty[hidden] { display: none; }
.map-empty p { margin: 0; max-width: 420px; }
.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 #2f3742;
  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; gap: 6px; }
.map-help { margin: 0; color: var(--muted); font-size: 12px; }
.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: #232931; }
.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); }
.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 #2f3742; }
.map-actions { display: flex; gap: 8px; }
.map-actions button { flex: 1; padding: 6px 10px; }
@media (max-width: 800px) {
  .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 #2f3742;
  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 #2f3742; 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; }

/* ---- 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; }
.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 #2f3742;
  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 #2f3742; border-radius: 6px; padding: 2px 6px; font: inherit; color-scheme: 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 #2f3742; 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; }
@media (max-width: 600px) {
  .img-panel { top: auto; left: 8px; right: 8px; width: auto; height: 62%; }
}

/* ---- 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 #2f3742; 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; } }

/* 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 #2f3742; 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; }
.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 #2f3742; 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; }

/* 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 #2f3742; 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 #2f3742; 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 #2f3742; 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 #2f3742; 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, .al-table td { text-align: left; padding: 6px 8px; border-bottom: 1px solid #222a32; vertical-align: top; }
.al-table td:first-child { text-transform: capitalize; font-weight: 600; }
.al-table tr.needs-ack { border-left: 3px solid currentColor; }
.al-table tr.pri-critical { color: #ff6b6b; }
.al-table tr.pri-high { color: #ffa94d; }
.al-table tr.pri-medium { color: #ffd43b; }
.al-table tr.pri-low { color: var(--muted); }
/* 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; }
.al-actions button, .al-actions a {
  background: var(--panel); color: var(--text); border: 1px solid #2a3139; border-radius: 3px;
  font-size: 12px; padding: 3px 6px; cursor: pointer; text-decoration: none;
}
.al-actions button:hover, .al-actions a:hover { background: var(--accent); color: #fff; }
.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: 8px; max-width: 720px; }
.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) {
  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) {
  .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; }
