:root {
  --bg: #16181d;
  --panel: #1f232b;
  --panel-border: #2c313b;
  --text: #e6e8eb;
  --text-dim: #9aa1ab;
  --accent: #4f8cff;
  --danger: #e5484d;
  --offline: #3a2a2a;
  --offline-text: #e5a3a3;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 14px/1.4 system-ui, -apple-system, Segoe UI, sans-serif;
}

header {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 16px;
  background: var(--panel);
  border-bottom: 1px solid var(--panel-border);
  flex-wrap: wrap;
}

/* A fixed width, not just nowrap: the title text differs per page ("BlueIrisCombo — Replay" is the
   longest, measured at ~186px) and sits directly before #shell-nav in this flex row. Without a width
   reserved for it, the nav's own links start at a different x position on every page — the same links
   visibly jumping left or right as you move between them, which is worse than a title that is never
   quite centred in its own column. 210px covers every title in use today with room to spare; flex-shrink
   is left at its default off (a min-width stops a flex item shrinking below it, which is all this needs). */
header h1 {
  font-size: 16px;
  margin: 0 16px 0 0;
  white-space: nowrap;
  min-width: 210px;
}

select, button, input[type="text"] {
  background: var(--panel);
  color: var(--text);
  border: 1px solid var(--panel-border);
  border-radius: 6px;
  padding: 6px 10px;
  font-size: 13px;
}

button {
  cursor: pointer;
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}

button.secondary {
  background: transparent;
  color: var(--text);
  border-color: var(--panel-border);
}

button.danger {
  background: var(--danger);
  border-color: var(--danger);
}

#customer-select-bar {
  /* Always shown today: there is no real login system yet (see dashboard.js's
     comment), so every request runs as a platform admin, which always requires an
     explicit customer selection. This becomes conditional once real auth exists. */
  display: flex;
  gap: 8px;
  align-items: center;
}

main {
  padding: 16px;
}

#empty-state {
  color: var(--text-dim);
  padding: 40px;
  text-align: center;
}

.grid-stack {
  background: transparent;
}

.grid-stack-item-content {
  background: #000;
  border: 1px solid var(--panel-border);
  border-radius: 8px;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

/* The `overflow: hidden` above was being silently overruled, which is why every camera tile
   had a scrollbar down its right-hand side.

   Gridstack's own stylesheet ships this:

     .grid-stack > .grid-stack-item > .grid-stack-item-content { overflow-x: hidden; overflow-y: auto }

   Three classes (0,3,0) against our one (0,1,0), so it wins on specificity no matter that
   dashboard.css is loaded second. The rule above looked like it was doing the job and was not —
   the classic cancelling-selectors failure, and invisible unless you open devtools and notice the
   struck-through declaration.

   A camera tile has nothing to scroll *to*: the header is auto-height, the body is `flex: 1` with
   `min-height: 0`, and the image is `object-fit: contain` inside it, so the contents are sized to
   the tile by construction. A scrollbar there is never a way to see more — it is a sliver of
   chrome on every tile, stealing width from the picture and inviting a drag that does nothing.
   If something ever does overflow, that is a layout bug to fix rather than something to hand the
   viewer a scrollbar for.

   Written at Gridstack's specificity rather than with `!important`, so a future upgrade that
   changes their selector shows up as this rule no longer applying — which is findable — instead
   of as an `!important` quietly winning against a rule that no longer exists. */
.grid-stack > .grid-stack-item > .grid-stack-item-content {
  overflow: hidden;
}

.tile-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 4px 8px;
  background: var(--panel);
  font-size: 12px;
  color: var(--text-dim);
}

/* Added with the overflow fix above, because it is the thing that fix breaks otherwise. Camera
   names come from Blue Iris verbatim and some of them are long; with the tile no longer
   scrollable, a long one would either push the remove button off the edge or be cut through the
   middle of a letter. `min-width: 0` is the part that is easy to leave out — a flex child will
   not shrink below its content width without it, so the ellipsis never gets a chance to apply. */
.tile-header span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

.tile-header button {
  background: transparent;
  border: none;
  color: var(--text-dim);
  padding: 0 4px;
  font-size: 14px;
  line-height: 1;
}

.tile-body {
  flex: 1;
  position: relative;
  min-height: 0;
  cursor: pointer;
}

.tile-body img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}

/* --- the camera wall: a fixed or automatic number of cameras across --- */

/* The columns are set from dashboard.js — `repeat(auto-fill, minmax(…))` for Auto, which stretches
   the tiles to use the whole row with no gap at the end, or `repeat(N, …)` for a chosen number. */
.camera-wall {
  display: grid;
  gap: 8px;
}

.camera-wall[hidden] {
  display: none;
}

/* Same look as a Gridstack tile. The picture area keeps a 16:9 shape, which is what nearly every
   Blue Iris camera sends, so a row of tiles lines up however many there are. */
.wall-tile {
  background: #000;
  border: 1px solid var(--panel-border);
  border-radius: 8px;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.wall-tile .tile-body {
  flex: none;
  aspect-ratio: 16 / 9;
}

.per-row {
  display: flex;
  align-items: center;
  gap: 6px;
  color: var(--text-dim);
  white-space: nowrap;
}

.per-row[hidden] {
  display: none;
}

.per-row .show-names {
  display: flex;
  align-items: center;
  gap: 4px;
  margin-left: 10px;
  cursor: pointer;
}

.per-row .show-names[hidden] {
  display: none;
}

/* Motion (PRD §23): a bright border and glow drawn inside the tile, so the layout never shifts. */
.motion-on .tile-body.motion::after {
  content: "";
  position: absolute;
  inset: 0;
  border: 3px solid #ff9f1a;
  box-shadow: inset 0 0 14px rgba(255, 159, 26, 0.75);
  pointer-events: none;
  z-index: 3;
  animation: motion-pulse 1.2s ease-in-out infinite alternate;
}

@keyframes motion-pulse {
  from { opacity: 1; }
  to { opacity: 0.55; }
}

/* Bandwidth (PRD §22): small live readout at the end of the toolbar row. */
.bandwidth-readout {
  margin-left: 14px;
  font-variant-numeric: tabular-nums;
  color: var(--text-dim);
}

/* "Show camera names" off: the pictures get the whole tile. A header holding only the name goes
   away; one that also holds the ✕ remove button (a saved dashboard) keeps just that button, as a
   small overlay in the top-right corner of the picture. */
.wall-tile {
  position: relative;
}

body.hide-names .tile-header.no-actions {
  display: none;
}

body.hide-names .tile-header {
  position: absolute;
  top: 4px;
  right: 4px;
  z-index: 2;
  padding: 2px;
  background: rgba(0, 0, 0, 0.55);
  border-radius: 4px;
}

body.hide-names .tile-header span {
  display: none;
}

body.hide-names .tile-header button {
  color: #fff;
}

/* "Show remove buttons" off: no ✕ on any tile. With names also hidden, the header then has nothing
   left in it and goes away entirely, so the picture is the whole tile. */
body.hide-remove .tile-header button {
  display: none;
}

body.hide-names.hide-remove .tile-header {
  display: none;
}

/* The "No image" message and the picture take turns, toggled with the `hidden` attribute — and an
   author `display` rule beats the browser's own [hidden] rule, so without these both were always
   drawn: "No image" printed under every working picture, and a failed picture still showed its
   broken-image icon. Same fix as .modal-backdrop[hidden] below. */
.tile-offline[hidden],
.tile-body img[hidden] {
  display: none;
}

/* --- one camera, full screen --- */

#fullscreen-view {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: #000;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: zoom-out;
}

#fullscreen-view[hidden] {
  display: none;
}

/* The picture area; the camera list, when shown, takes its own column beside it. */
#fullscreen-main {
  position: relative;
  flex: 1;
  align-self: stretch;
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}

#fullscreen-img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* Cameras / Grid, top right of the picture. Their clicks do not go back to the board (dashboard.js). */
#fullscreen-bar {
  position: absolute;
  top: 12px;
  right: 12px;
  display: flex;
  gap: 6px;
  z-index: 3;
  cursor: default;
}

#fullscreen-bar button {
  background: rgba(15, 17, 21, 0.75);
  border: 1px solid var(--panel-border);
  color: var(--text);
}

#fullscreen-bar button[aria-pressed="true"] {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}

/* The list of the board's cameras, at the right: the same shape as Replay's. */
.fs-side {
  flex: 0 0 280px;
  align-self: stretch;
  overflow-y: auto;
  background: var(--panel);
  border-left: 1px solid var(--panel-border);
  padding: 10px;
  cursor: default;
}

.fs-side[hidden] { display: none; }

.fs-side h2 {
  margin: 0 0 8px;
  font-size: 14px;
}

.fs-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.fs-list button {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 10px;
  text-align: left;
  background: transparent;
  color: var(--text);
  border: 1px solid transparent;
  border-radius: 6px;
  padding: 4px;
}

.fs-list button:hover:not(:disabled) { background: var(--bg); }
.fs-list button.current { border-color: var(--accent); background: var(--bg); }
.fs-list button:disabled { opacity: 0.45; cursor: default; }

.fs-list .thumb {
  flex: 0 0 72px;
  height: 44px;
  border-radius: 4px;
  background: #111;
  overflow: hidden;
}

.fs-list .thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.fs-list .label {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.fs-list .label span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.fs-list .when {
  color: var(--text-dim);
  font-size: 11px;
}

/* The PTZ button only means something for a camera that has controls, which ptz.js shows by un-hiding its panel. */
#fullscreen-view:not(:has(.ptz-panel:not([hidden]))) #fullscreen-ptz-btn { display: none; }

/* Hidden by the PTZ button; kept as a class on the view so it holds as the camera is switched. */
#fullscreen-view.hide-ptz .ptz-panel { display: none; }

/* The PTZ panel sits inside the whole view, so it steps left of the camera list when that is showing. */
#fullscreen-view.with-cameras .ptz-panel { right: 292px; }

@media (max-width: 700px) {
  .fs-side { flex-basis: 200px; }
  #fullscreen-view.with-cameras .ptz-panel { right: 212px; }
}

#fullscreen-caption,
#fullscreen-hint {
  position: absolute;
  left: 12px;
  padding: 4px 10px;
  border-radius: 6px;
  background: rgba(0, 0, 0, 0.55);
  color: var(--text);
  font-size: 13px;
  pointer-events: none;
}

#fullscreen-caption { top: 12px; }

/* Fades after a few seconds so it does not sit over the picture. */
#fullscreen-hint {
  bottom: 12px;
  color: var(--text-dim);
  animation: fullscreen-hint-fade 4s forwards;
}

@keyframes fullscreen-hint-fade {
  0%, 70% { opacity: 1; }
  100% { opacity: 0; }
}

.tile-body[data-site-online='true'] {
  cursor: zoom-in;
}

.tile-offline {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
  background: var(--offline);
  color: var(--offline-text);
  font-size: 13px;
  text-align: center;
  padding: 8px;
}

/* --- modal --- */

.modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.6);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 100;
}

/* An author display:flex rule otherwise beats the browser's default [hidden]
   behavior at equal specificity (author stylesheets always win over the UA
   stylesheet) — without this, toggling .hidden via the DOM attribute would do
   nothing and every modal would render open at load. */
.modal-backdrop[hidden] {
  display: none;
}

.modal {
  background: var(--panel);
  border: 1px solid var(--panel-border);
  border-radius: 10px;
  padding: 20px;
  min-width: 320px;
  max-width: 90vw;
  max-height: 80vh;
  overflow: auto;
}

/* A refusal shown inside a dialog — the picker's and the rename dialog's. */
.modal .error {
  margin: 0 0 12px;
  padding: 8px 10px;
  border: 1px solid var(--danger);
  border-radius: 6px;
  background: rgba(229, 72, 77, 0.12);
  color: var(--offline-text);
}

.modal .error[hidden] {
  display: none;
}

.modal h2 {
  margin-top: 0;
  font-size: 15px;
}

.modal-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 16px;
}

.camera-picker-list {
  list-style: none;
  margin: 0;
  padding: 0;

  /* Taller than the 300px it was, because rows now carry a thumbnail and three visible rows is not
     a picker. Still bounded: the dialog has to fit a laptop screen with the header and buttons. */
  max-height: 60vh;
  overflow: auto;
}

.camera-picker-list li {
  padding: 8px;
  border-bottom: 1px solid var(--panel-border);
  display: flex;
  gap: 10px;
  justify-content: space-between;
  align-items: center;
}

/* Big enough to recognise a doorway or a car park, small enough that fifty of them scroll.
   `object-fit: cover` because Blue Iris cameras are not all the same aspect ratio and a row of
   letterboxed thumbnails of different shapes is harder to scan than a row of uniform ones. */
.camera-picker-thumb {
  flex: 0 0 auto;
  width: 96px;
  height: 54px;
  object-fit: cover;
  border-radius: 4px;
  background: var(--bg);
  border: 1px solid var(--panel-border);
}

/* What replaces the image when the snapshot does not arrive — an offline site, a blocked
   credential, a camera Blue Iris no longer reports. Same footprint, so the list does not reflow as
   images resolve one by one while you scroll. */
.camera-picker-thumb-missing {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 10px;
  color: var(--text-dim);
}

/* Search and "Add all" above the camera list. */
.picker-top {
  display: flex;
  gap: 8px;
  align-items: center;
  margin-bottom: 8px;
}

.picker-top input[type="search"] {
  flex: 1 1 auto;
  min-width: 0;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--panel-border);
  border-radius: 6px;
  padding: 6px 10px;
  font-size: 13px;
}

.picker-top input[hidden] {
  display: none;
}

/* A site's heading in the picker. Sticky, so the site you are looking at stays named as you
   scroll through its cameras. */
.camera-picker-list li.camera-picker-section {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--bg);
  font-weight: 600;
  color: var(--text);
  padding: 6px 8px;
}

.camera-picker-list li[hidden] {
  display: none;
}

/* A site heading is also the switch that opens and closes its section. */
.camera-picker-list li.camera-picker-section {
  cursor: pointer;
  user-select: none;
}

.camera-picker-section .picker-arrow {
  display: inline-block;
  width: 1.2em;
  color: var(--text-dim);
}

.picker-tools {
  display: flex;
  gap: 8px;
  align-items: center;
  flex-wrap: wrap;
  margin-bottom: 8px;
  color: var(--text-dim);
}

.picker-tools label {
  display: flex;
  align-items: center;
  gap: 4px;
  cursor: pointer;
}

/* The two tick boxes on the left, the two buttons pushed to the right. */
.picker-tools label + button {
  margin-left: auto;
}

/* Takes the slack, so the Add button stays hard right however long the camera and site names are. */
.camera-picker-name {
  flex: 1 1 auto;
  min-width: 0;
}

.camera-picker-list li button {
  flex: 0 0 auto;
  padding: 4px 10px;
  font-size: 12px;
}

.hint {
  color: var(--text-dim);
  font-size: 11px;
}

/* --- top bar: customer scope and account (work order 06) ------------------- */

/* A customer user's scope is not selectable, so it is rendered as text rather than a
   disabled dropdown — a disabled control invites people to try to enable it. */
.scope-label {
  padding: 6px 10px;
  background: var(--bg);
  border: 1px solid var(--panel-border);
  border-radius: 4px;
  color: var(--text);
  white-space: nowrap;
}

/* Pushes the account block to the far right of the header, away from the controls. */
#account-bar {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-left: auto;
}

#account-email {
  white-space: nowrap;
}

/* --- the shared shell nav (work order 08) --- */

#shell-nav {
  display: flex;
  gap: 4px;
  align-items: center;
}

#shell-nav a {
  color: var(--text-dim);
  text-decoration: none;
  padding: 6px 10px;
  border-radius: 6px;
  white-space: nowrap;
}

#shell-nav a:hover {
  color: var(--text);
  background: var(--bg);
}

/* The page you are on. Marked rather than removed — a nav that hides where you are makes it
   harder to tell one screen from another, not easier. */
#shell-nav a.current {
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--panel-border);
}

/* The filter row — scope selectors and per-screen filters, on their own line **below** the
   navigation rather than wedged between the page title and the nav links.

   Sitting in the header, a scope selector read as part of the page's identity: "Sites [Thermal Club]"
   looked like a title rather than a control somebody had set, and on a narrow window it wrapped into
   the nav. On its own row it reads as what it is — the thing currently narrowing what is below it.

   Quieter than the header on purpose: the page background rather than the panel, so the two rows
   separate without a second heavy band. */
.toolbar {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  padding: 8px 16px;
  background: var(--bg);
  border-bottom: 1px solid var(--panel-border);
}

/* A screen whose only control is one a customer user cannot change would otherwise leave an empty
   band under the nav. `hidden` is what the scripts toggle, so the row keys on the same thing. */
.toolbar:not(:has(> :not([hidden]))) {
  display: none;
}

/* A picker row whose camera is already on the board. Dimmed rather than removed: the row still
   carries the thumbnail and the name, which is how somebody scanning a long list recognises what
   they have already done — removing it would make the list reflow under the cursor mid-run. */
/* Already on the board: the picture and name are dimmed, the Remove button is not. */
.camera-picker-added .camera-picker-thumb,
.camera-picker-added .camera-picker-name {
  opacity: 0.5;
}

.camera-picker-added .camera-picker-name::after {
  content: " — on this dashboard";
  font-style: italic;
}

/* --- Camera info on a tile (PRD §26): the name, and under it building, location and address --- */

.tile-titles {
  display: flex;
  flex-direction: column;
  min-width: 0;
  flex: 1;
}

.tile-info {
  font-size: 11px;
  opacity: 0.8;
}

.tile-info[hidden] {
  display: none;
}

.info-options {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  margin-left: 6px;
}

/* --- PTZ controls in the full-screen view (PRD §28) --- */

.ptz-panel {
  position: absolute;
  top: 56px;
  right: 12px;
  bottom: auto;
  width: 264px;
  max-height: calc(100% - 96px);
  overflow-y: auto;
  padding: 10px 12px;
  background: rgba(15, 17, 21, 0.88);
  border: 1px solid var(--panel-border);
  border-radius: 8px;
  color: var(--text);
  font-size: 13px;
  cursor: default;
  z-index: 3;
}

.ptz-panel[hidden] { display: none; }

.ptz-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 8px;
}

.ptz-panel.collapsed .ptz-body,
.ptz-panel.collapsed .ptz-status,
.ptz-panel.collapsed .ptz-error { display: none; }

.ptz-panel.collapsed .ptz-head { margin-bottom: 0; }

.ptz-toggle {
  background: transparent;
  border: none;
  color: var(--text-dim);
  font-size: 14px;
  padding: 2px 6px;
  cursor: pointer;
}

.ptz-move {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  flex-wrap: wrap;
}

.ptz-pad {
  display: grid;
  grid-template-columns: repeat(3, 44px);
  grid-template-rows: repeat(3, 44px);
  gap: 4px;
}

.ptz-pad .up { grid-column: 2; grid-row: 1; }
.ptz-pad .left { grid-column: 1; grid-row: 2; }
.ptz-pad .right { grid-column: 3; grid-row: 2; }
.ptz-pad .down { grid-column: 2; grid-row: 3; }

.ptz-zoom {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.ptz-btn {
  width: 44px;
  height: 44px;
  background: var(--panel);
  border: 1px solid var(--panel-border);
  border-radius: 8px;
  color: var(--text);
  font-size: 16px;
  cursor: pointer;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
}

.ptz-btn:hover { border-color: var(--accent); }
.ptz-btn.active { background: var(--accent); border-color: var(--accent); color: #fff; }

.ptz-speed-label {
  flex-basis: 100%;
  display: flex;
  align-items: center;
  gap: 6px;
}

.ptz-speed {
  background: var(--panel);
  color: var(--text);
  border: 1px solid var(--panel-border);
  border-radius: 6px;
  padding: 2px 6px;
}

.ptz-section {
  margin: 12px 0 6px;
  color: var(--text-dim);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.ptz-presets {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.ptz-presets li {
  display: flex;
  align-items: center;
  gap: 4px;
}

.ptz-presets li.editing {
  flex-direction: column;
  align-items: stretch;
  padding: 6px;
  background: var(--panel);
  border: 1px solid var(--panel-border);
  border-radius: 6px;
}

.ptz-go {
  flex: 1 1 auto;
  min-width: 0;
  text-align: left;
  background: var(--panel);
  border: 1px solid var(--panel-border);
  border-radius: 6px;
  color: var(--text);
  padding: 7px 10px;
  cursor: pointer;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ptz-go:hover { border-color: var(--accent); }
.unnamed .ptz-go { color: var(--text-dim); }

.ptz-tools { display: inline-flex; gap: 4px; flex: 0 0 auto; }

.ptz-tool {
  background: transparent;
  border: 1px solid var(--panel-border);
  border-radius: 6px;
  color: var(--text-dim);
  padding: 5px 8px;
  font-size: 12px;
  cursor: pointer;
}

.ptz-tool:hover { color: var(--text); border-color: var(--accent); }
.ptz-tool.primary { background: var(--accent); border-color: var(--accent); color: #fff; }

.ptz-input {
  width: 100%;
  margin: 4px 0;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--panel-border);
  border-radius: 6px;
  padding: 6px 8px;
  font-size: 13px;
}

.ptz-edit {
  margin-top: 10px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.ptz-wide {
  width: 100%;
  background: var(--accent);
  border: 1px solid var(--accent);
  border-radius: 6px;
  color: #fff;
  padding: 7px 10px;
  cursor: pointer;
}

.ptz-wide.secondary { background: transparent; color: var(--text); border-color: var(--panel-border); }

.ptz-status { margin-top: 8px; min-height: 1em; }
.ptz-error { color: var(--danger); margin-top: 6px; }
.ptz-error:empty { display: none; }

/* On a phone the panel sits along the bottom instead of covering the side of the picture. */
@media (max-width: 700px) {
  .ptz-panel {
    top: auto;
    right: 8px;
    left: 8px;
    bottom: 8px;
    width: auto;
    max-height: 48vh;
  }
}
/* --- the View page's clips panel, at the left of the board ------------------------------------------------- */
#view-body { display: flex; align-items: flex-start; }
#view-body > main { flex: 1; min-width: 0; }
#view-clips { position: sticky; top: 0; flex: 0 0 300px; width: 300px; max-height: 100vh; overflow-y: auto; padding: 8px; box-sizing: border-box; border-right: 1px solid #444; }
#view-clips[hidden] { display: none; }
.view-clips-head { display: flex; align-items: center; gap: 6px; }
.view-clips-head h2 { flex: 1; margin: 0; font-size: 1rem; }
#view-clips .fs-list button { display: flex; gap: 8px; width: 100%; text-align: left; }
#view-clips .fs-list .thumb { width: 96px; height: 54px; object-fit: cover; flex: none; background: #222; }
#view-clips .fs-list .text { display: flex; flex-direction: column; min-width: 0; }
.tile-body.selected { outline: 3px solid #4aa3ff; outline-offset: -3px; }
#view-clips .fs-list button.current { outline: 2px solid #4aa3ff; background: rgba(74,163,255,.15); }
.view-clip-modal { width: min(900px, 94vw); max-width: none; }
.view-clip-modal video { width: 100%; max-height: 70vh; background: #000; }

#view-clips { flex-basis: 340px; width: 340px; }
.view-clip-modal .modal-actions { flex-wrap: wrap; }

.toolbar[hidden] { display: none; }
#toolbar-toggle-btn { margin-left: 8px; }

#header-tools { display: inline-flex; align-items: center; gap: 10px; margin-left: 8px; }
#header-tools label { font-size: 0.85rem; opacity: .85; margin-right: 4px; }
#camera-search { min-width: 200px; }
#per-row-head[hidden] { display: none; }
.wall-tile[hidden], .grid-stack-item[hidden] { display: none; }

/* A tile or a clip row is tapped twice to open it: without this a tablet reads the second tap as a zoom. */
.tile-body, #view-clips .fs-list button { touch-action: manipulation; }
