:root {
  /* The site is dark-only. Declaring it lets the browser draw native
     widgets (select popups, scrollbars, date pickers) dark instead of
     rendering them white on a dark page. */
  color-scheme: dark;

  /* ---------- Company brand colours ---------- */
  --brand: #0d3540;      /* primary   - deep petrol */
  --brand-2: #edb758;    /* secondary - gold, reserved for small accents */

  /* Surfaces. The primary brand colour drives the page and all chrome. */
  --bg: #0d3540;         /* page background = brand primary */
  --bg-alt: #11414f;     /* raised panels: cards, modals, group buttons */
  --bg-dark: #0a2b35;    /* deeper brand shade - top bars */
  --bg-hover: #17505f;   /* hover / pressed surface */
  --border: #1e5a6a;     /* brand-tinted hairline */

  /* Text */
  --text: #eaf4f6;
  --text-dim: #9cc0c8;

  /* Interactive accent = the secondary brand colour */
  --accent: #edb758;
  --accent-ink: #0d3540; /* text placed ON gold, for contrast */

  /* Semantic - deliberately conventional, not brand-tinted */
  --danger: #ef4444;
  --ok: #22c55e;
  --off: #ef4444;
}

/* ---------- Persian (Farsi) typography ----------
   Vazirmatn — a modern Persian sans-serif (SIL OFL 1.1), self-hosted so the
   PWA renders Persian correctly offline and on machines with no Persian system
   font. The unicode-range deliberately covers Arabic-script code points ONLY,
   so Latin UI text falls through to the system stack below and none of the
   existing English interface changes. ZWNJ (U+200C) is included because
   Persian relies on it for correct letter joining (e.g. "میشود"). */
@font-face {
  font-family: 'Vazirmatn';
  src: url('/fonts/Vazirmatn-Regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+08A0-08FF,
                 U+FB50-FDFF, U+FE70-FEFF, U+200C-200D, U+0640;
}
@font-face {
  font-family: 'Vazirmatn';
  src: url('/fonts/Vazirmatn-Medium.woff2') format('woff2');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+08A0-08FF,
                 U+FB50-FDFF, U+FE70-FEFF, U+200C-200D, U+0640;
}
@font-face {
  font-family: 'Vazirmatn';
  src: url('/fonts/Vazirmatn-Bold.woff2') format('woff2');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+08A0-08FF,
                 U+FB50-FDFF, U+FE70-FEFF, U+200C-200D, U+0640;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  background: var(--bg);
  color: var(--text);
  font-family: 'Vazirmatn', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}

/* ---------- Bidi for user-supplied names (groups / cameras) ----------
   Group and camera names may be Persian. `plaintext` lets the browser choose
   the paragraph direction from the first strong character of each element, so
   a Persian name lays out RTL inside the otherwise-LTR interface while an
   English name is unaffected. `dir="auto"` on the inputs does the same for
   typing (caret + selection follow the text direction). */
.group-btn,
#live-group-title,
.mgmt-table td,
.mgmt-table input {
  unicode-bidi: plaintext;
}

button {
  font-family: inherit;
  cursor: pointer;
}

.view {
  display: none;
  height: 100vh;
  flex-direction: column;
}
.view.active { display: flex; }

/* ---------- Decorative right-edge photo (login / chooser / groups) ----------
   One fixed layer anchored to the physical right edge of the viewport, fading
   out as it moves left: 90% opaque at its right edge, fully transparent at its
   left. `mask-image` does that for free -- the gradient's ALPHA becomes the
   element's opacity -- so no overlay element is needed and the photo melts
   into whatever page background sits behind it (here, the brand petrol).

   `z-index: -1` keeps it behind all page content. That only works because the
   view it lives in creates its own stacking context (see .view.has-photo): as
   a child of the root context a negative-z element paints *before* body's
   opaque background and would be invisible.

   Physical `right` / `to left` (not logical `inset-inline-end`) is deliberate:
   the brief was "right side of the website", so the photo must not mirror when
   the UI flips to RTL for Persian and Kurdish. */
.view.has-photo {
  position: relative;
  z-index: 0;
}
.page-photo {
  position: fixed;
  top: 0;
  right: 0;
  height: 100vh;
  height: 100dvh;
  width: min(46vw, 640px);
  background: url("img/site-photo.jpg") right center / cover no-repeat;
  /* the fade: opaque right edge -> fully transparent left edge */
  -webkit-mask-image: linear-gradient(to left, rgba(0, 0, 0, 0.9), rgba(0, 0, 0, 0));
  mask-image: linear-gradient(to left, rgba(0, 0, 0, 0.9), rgba(0, 0, 0, 0));
  pointer-events: none;
  z-index: -1;
}
/* a whisper of brand petrol so the photo sits in the UI's colour world */
.page-photo::after {
  content: "";
  position: absolute;
  inset: 0;
  background: rgba(13, 53, 64, 0.14);
}
/* phones: same look, narrower band */
@media (max-width: 720px) {
  .page-photo { width: min(70vw, 380px); }
}

/* ---------- Login ---------- */
#view-login {
  align-items: center;
  justify-content: center;
}
.login-card {
  display: flex;
  flex-direction: column;
  gap: 12px;
  width: 280px;
  padding: 24px;
  background: var(--bg-alt);
  border: 1px solid var(--border);
  border-radius: 10px;
}
.login-card h1 {
  margin: 0 0 8px;
  font-size: 20px;
  text-align: center;
  font-weight: 600;
}
.login-card input {
  padding: 10px 12px;
  border-radius: 6px;
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--text);
  font-size: 14px;
}
.login-card button {
  padding: 10px;
  border-radius: 6px;
  border: none;
  background: var(--accent);
  color: var(--accent-ink);
  font-size: 15px;
  font-weight: 600;
}
.login-card button:hover { filter: brightness(1.08); }
.error-text {
  color: var(--danger);
  font-size: 13px;
  margin: 0;
  min-height: 16px;
}

/* ---------- Topbar ---------- */
.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 14px;
  background: var(--bg-dark);
  /* the gold rule is the brand signature; it runs across every screen */
  border-bottom: 2px solid var(--brand-2);
  flex: 0 0 auto;
}
.topbar-title {
  font-size: 16px;
  font-weight: 600;
  flex: 1;
  text-align: center;
}
.topbar-actions {
  display: flex;
  gap: 6px;
}
.icon-btn {
  background: none;
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 6px;
  width: 36px;
  height: 36px;
  font-size: 16px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.icon-btn:hover { background: var(--bg-alt); }
.back-btn { font-size: 20px; }

/* ---------- Home ---------- */
.home-title {
  text-align: center;
  color: var(--brand-2);
  font-size: 20px;
  font-weight: 700;
  margin: 40px 0 8px;
  padding: 0 20px;
  line-height: 1.6;
}
.home-grid {
  flex: 1;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  /* v56: centre the group tiles in the space left below the topbar. With 11
     groups this is a 4-row block sitting in the middle of the screen instead
     of hugging the top. Safe when they do NOT fit (short phone, many groups):
     the flex item's automatic minimum size grows the container to its content,
     so there is no free space to centre and the block falls back to the top --
     nothing is clipped and the page just scrolls. Verified both ways in a
     browser: 11 tiles -> 83px above / 83px below; 40 tiles -> 20/20, no clipping. */
  align-content: center;
  gap: 12px;
  padding: 20px;
  max-width: 700px;
  margin: 0 auto;
  width: 100%;
}
.group-btn {
  padding: 22px 8px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--bg-alt);
  color: var(--text);
  font-size: 16px;
  font-weight: 600;
  text-align: center;
  transition: background 120ms ease, border-color 120ms ease;
  min-width: 190px;
  max-width: 240px;
  flex: 1 1 auto;
}
.group-btn:hover { background: var(--bg-hover); border-color: var(--brand-2); }

@media (max-width: 600px) {
  .group-btn { min-width: 150px; max-width: none; }
}

/* ---------- Camera grid ---------- */
/* One page = 4 columns x 3 rows = 12 tiles. renderCameraGrid() never puts more
   than PAGE_SIZE tiles in the DOM, so at most 12 HLS sessions are open at once
   regardless of how many cameras the group holds. */
.camera-grid {
  flex: 1;
  display: grid;
  gap: 1px;
  background: var(--border);
  overflow: hidden;
  grid-template-columns: repeat(4, 1fr);
  grid-auto-rows: 1fr;
}

@media (max-width: 640px) {
  .camera-grid { grid-template-columns: repeat(2, 1fr); }
}

/* ---------- Pager ---------- */
/* Circular auto-scroll toggle in the live-view header. Hidden unless the
   current group spans more than one page.
   NOTE: written as `.icon-btn.autoplay-btn` on purpose -- a later, equal-
   specificity `.icon-btn` rule would otherwise win and square off the circle. */
.icon-btn.autoplay-btn {
  border-radius: 50%;
  font-size: 13px;
  line-height: 1;
  padding-left: 3px; /* optically centre the play triangle */
}
.icon-btn.autoplay-btn.active {
  background: var(--brand-2);
  border-color: var(--brand-2);
  color: var(--brand);
  padding-left: 0;
}
.icon-btn.autoplay-btn.active:hover {
  background: var(--brand-2);
  border-color: var(--brand-2);
  color: var(--brand);
}
.pager {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  padding: 8px 0 10px;
  background: var(--bg);
}
.pager[hidden] { display: none; }
.pager-btn {
  width: 46px;
  height: 34px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 18px;
  line-height: 1;
  color: var(--text);
  background: var(--bg-alt);
  border: 1px solid var(--border);
  border-radius: 8px;
  transition: background 120ms ease, border-color 120ms ease, opacity 120ms ease;
}
.pager-btn:hover:not(:disabled) {
  background: var(--bg-hover);
  border-color: var(--brand-2);
}
.pager-btn:disabled {
  opacity: 0.35;
  cursor: default;
}
.pager-info {
  font-size: 14px;
  font-weight: 600;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
  min-width: 48px;
  text-align: center;
}
.cam-tile {
  position: relative;
  background: #000;
  overflow: hidden;
  cursor: pointer;
}
.cam-tile video {
  width: 100%;
  height: 100%;
  object-fit: contain;
  background: #000;
  display: block;
}
.cam-tile.offline {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-dim);
  font-size: 13px;
  letter-spacing: 0.3px;
}
.cam-tile.offline::before {
  content: "Camera Offline";
}
.cam-tile.enlarged {
  position: fixed;
  z-index: 50;
  top: 0; left: 0; right: 0; bottom: 0;
  width: 100vw;
  height: 100vh;
}

/* ---------- Management pages ---------- */
.mgmt-main {
  flex: 1;
  overflow: auto;
  padding: 16px;
}
.mgmt-table-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
}
.mgmt-table-header h2 {
  margin: 0;
  font-size: 16px;
}
.plus-btn, .minus-btn { font-size: 18px; font-weight: 700; }
/* The two management tables (camera management, users) are data grids whose
   columns are mostly Latin -- a select-all checkbox, `#`, RTSP URL, username,
   status -- so they read best left-to-right in every UI language: the checkbox
   belongs on the far left and the columns run left to right. `direction: ltr`
   on the table is the entire fix, because every rule below is written in
   logical properties (`text-align: start`/`end`, `padding-inline-*`,
   `margin-inline-*`) and therefore mirrors with it -- no other rule needs
   touching. `unicode-bidi: plaintext` on the cells still lets a Persian group
   or camera name lay out right-to-left *inside* its own cell, so no text is
   reversed; only the grid is. Headings and the rest of the UI stay RTL. */
.mgmt-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 14px;
  direction: ltr;
}
.mgmt-table th, .mgmt-table td {
  padding: 8px 10px;
  border-bottom: 1px solid var(--border);
  /* `start`, not `left`: in RTL the column order mirrors, and the cell text
     has to hug the mirrored leading edge. */
  text-align: start;
}
/* Row-number column (`#`) — the first content column, ahead of Group. Kept
   narrow and aligned to the trailing edge with tabular figures so the digits
   line up as a clean column and the fixed width never steals space from
   Group / Name. `end` keeps it on the Group-facing side in both directions. */
.mgmt-table th.row-num,
.mgmt-table td.row-num {
  width: 40px;
  padding-inline-end: 6px;
  text-align: end;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}
.mgmt-table input[type="text"],
.mgmt-table select {
  background: var(--bg-alt);
  border: 1px solid var(--border);
  color: var(--text);
  padding: 4px 6px;
  border-radius: 4px;
  font-size: 13px;
  width: 100%;
}
.status-dot {
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 50%;
}
.status-dot.live { background: var(--ok); }
.status-dot.offline, .status-dot.unknown { background: var(--off); }
.row-remove-btn {
  background: none;
  border: none;
  color: var(--danger);
  font-size: 13px;
  cursor: pointer;
}

/* Any element carrying the [hidden] attribute must stay hidden, even when a
   class rule declares a display value (.icon-btn uses display:flex). Without
   this, [hidden] silently fails and admin-only controls leak into the viewer
   UI. Applies to modals, buttons, and everything else. */
[hidden] {
  display: none !important;
}

/* ---------- Focus ring (brand gold) ---------- */
/* One consistent, visible focus treatment for every field. The gold secondary
   reads clearly against the petrol panels and is keyboard-visible. */
input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 2px rgba(237, 183, 88, 0.28);
}

/* ---------- Modals ---------- */
.modal {
  position: fixed;
  top: 0; left: 0; right: 0; bottom: 0;
  background: rgba(0,0,0,0.6);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 100;
}
.modal-card {
  background: var(--bg-alt);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 20px;
  width: 300px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.modal-card h3 { margin: 0 0 4px; font-size: 15px; }
.modal-card input,
.modal-card select {
  padding: 8px 10px;
  border-radius: 6px;
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--text);
}
.modal-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
}
.btn-primary, .btn-secondary {
  padding: 8px 14px;
  border-radius: 6px;
  border: 1px solid var(--border);
  font-size: 13px;
}
.btn-primary { background: var(--accent); color: var(--accent-ink); border: none; }
.btn-primary:hover { filter: brightness(1.08); }
.btn-secondary { background: none; color: var(--text); }

/* ---------- Toast ---------- */
.toast {
  position: fixed;
  bottom: 20px;
  left: 50%;
  transform: translateX(-50%);
  background: var(--bg-alt);
  border: 1px solid var(--border);
  color: var(--text);
  padding: 10px 18px;
  border-radius: 8px;
  font-size: 13px;
  z-index: 200;
  max-width: 90vw;
}
.toast.error { border-color: var(--danger); color: #fca5a5; }

/* ---------- My Profile (viewer self-service) ---------- */
.profile-card {
  background: var(--bg-alt);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 20px;
  width: 100%;
  max-width: 420px;
  margin: 20px auto;
  display: flex;
  flex-direction: column;
  gap: 8px;
  box-sizing: border-box;
}
.profile-note {
  color: var(--text);
  opacity: 0.75;
  font-size: 13px;
  margin: 0 0 6px;
}
.profile-label {
  font-size: 12px;
  color: var(--text);
  opacity: 0.85;
  margin-top: 6px;
}
.profile-label .muted { opacity: 0.55; }
.profile-card input {
  padding: 8px 10px;
  border-radius: 6px;
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--text);
  width: 100%;
  box-sizing: border-box;
}
.profile-card .modal-actions { margin-top: 10px; }

/* ---------- User row: role dropdown sits immediately before Remove ---------- */
/* The generic `.mgmt-table select { width: 100% }` rule (meant for full-width
   form fields) also hit the role dropdown, so it swallowed the entire actions
   cell and shoved the Remove button past the row, the table, and the viewport.
   Shrink-wrap the cell and let the dropdown size to its own content. */
.mgmt-table td.row-actions {
  white-space: nowrap;
  text-align: end;
  width: 1%; /* hug content; let the other columns absorb the slack */
}
.mgmt-table td.row-actions .edit-role {
  width: auto;       /* undo the 100% inherited from .mgmt-table select */
  max-width: 110px;  /* never wider than "Viewer"/"Admin" need */
  margin-inline-end: 8px;
  vertical-align: middle;
}
.mgmt-table td.row-actions .row-remove-btn {
  padding: 4px 2px;
  vertical-align: middle;
}

/* ============================================================
   MODERN POLISH (presentational only)
   Nothing here touches a layout contract, an id, or the
   [hidden] / row-actions rules above. It is CSS-only, so the
   52-camera wall keeps its performance budget - no extra JS,
   no per-viewer transcoding, no new network requests.
   ============================================================ */

/* ---------- Ambient background ----------
   Two very soft light sources instead of one flat fill, so the page reads as
   layered rather than painted. Fixed, so it never scrolls with content. */
body {
  background:
    radial-gradient(1200px 640px at 10% -15%, rgba(237, 183, 88, 0.10), transparent 60%),
    radial-gradient(950px 560px at 105% 8%, rgba(38, 140, 160, 0.20), transparent 58%),
    var(--bg);
  background-attachment: fixed;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* ---------- Scrollbars ----------
   Thin gold-on-petrol rails so long camera and table lists stop looking raw. */
* { scrollbar-width: thin; scrollbar-color: rgba(237, 183, 88, 0.35) transparent; }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: rgba(237, 183, 88, 0.30);
  border-radius: 20px;
  border: 3px solid transparent;
  background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover {
  background: rgba(237, 183, 88, 0.55);
  background-clip: content-box;
}

/* ---------- Login (the brand hero) ---------- */
#view-login {
  background:
    radial-gradient(760px 460px at 50% 26%, rgba(237, 183, 88, 0.13), transparent 66%),
    radial-gradient(900px 600px at 50% 112%, rgba(38, 140, 160, 0.20), transparent 62%);
}
.login-card {
  width: 330px;
  max-width: calc(100vw - 32px);
  padding: 30px 28px 26px;
  border-radius: 18px;
  background: linear-gradient(165deg, rgba(255,255,255,0.085), rgba(255,255,255,0.015)), var(--bg-alt);
  border: 1px solid rgba(255,255,255,0.11);
  box-shadow:
    0 26px 64px rgba(0,0,0,0.46),
    0 0 0 1px rgba(237,183,88,0.10),
    0 0 70px rgba(237,183,88,0.07);
  animation: rise-in 320ms cubic-bezier(.2,.85,.3,1);
}
.brand-logo {
  display: block;
  width: 96px;
  height: 96px;
  margin: 0 auto;
  border-radius: 24%;
  box-shadow: 0 12px 30px rgba(0,0,0,0.48), 0 0 0 1px rgba(255,255,255,0.09);
}
.login-card h1 {
  font-size: 19px;
  letter-spacing: 0.2px;
  margin: 4px 0 10px;
}
.login-card input {
  padding: 11px 13px;
  border-radius: 10px;
  background: rgba(5, 26, 32, 0.55);
  border: 1px solid rgba(255,255,255,0.12);
  transition: border-color 140ms ease, box-shadow 140ms ease, background 140ms ease;
}
.login-card button {
  padding: 12px;
  margin-top: 2px;
  border-radius: 10px;
  background: linear-gradient(180deg, #f3c76d, #dfa63c);
  color: var(--accent-ink);
  box-shadow: 0 4px 14px rgba(237,183,88,0.30);
  transition: transform 140ms ease, box-shadow 140ms ease, filter 140ms ease;
}
.login-card button:hover {
  filter: brightness(1.04);
  transform: translateY(-1px);
  box-shadow: 0 8px 22px rgba(237,183,88,0.40);
}
.login-card button:active { transform: translateY(0); filter: brightness(0.98); }

/* ---------- Language switcher (login card) ---------- */
/* Written as `.login-card .lang-btn` so it out-specifies the gold
   `.login-card button` rule above -- these are small quiet chips, not CTAs. */
.lang-switch {
  display: flex;
  gap: 8px;
  justify-content: center;
  margin-top: 16px;
  padding-top: 14px;
  border-top: 1px solid rgba(255,255,255,0.09);
  /* These three labels are Latin ("ENG"/"PER"/"KURD"), so pin the row to LTR:
     otherwise switching to Persian or Kurdish mirrors the page and the chips
     swap places under the cursor that just clicked one. */
  direction: ltr;
}
.login-card .lang-btn {
  flex: 1 1 0;
  padding: 7px 4px;
  margin-top: 0;
  border-radius: 8px;
  border: 1px solid rgba(255,255,255,0.16);
  background: rgba(255,255,255,0.05);
  color: var(--muted, #9cc0c8);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.6px;
  cursor: pointer;
  box-shadow: none;
  transition: background 140ms ease, color 140ms ease, border-color 140ms ease;
}
.login-card .lang-btn:hover {
  filter: none;
  transform: none;
  background: rgba(237,183,88,0.14);
  border-color: rgba(237,183,88,0.5);
  color: #f0dcb4;
  box-shadow: none;
}
.login-card .lang-btn.active {
  background: linear-gradient(180deg, #f3c76d, #dfa63c);
  border-color: transparent;
  color: var(--accent-ink);
}
.login-card .lang-btn.active:hover {
  background: linear-gradient(180deg, #f3c76d, #dfa63c);
  color: var(--accent-ink);
}

/* Stack the credential fields and centre them:
   the form was a plain block, so the two inputs fell back to the browser's
   default ~209px width and the button to its text width (69px) — all pinned
   to the left edge of a 272px content box, leaving a ragged hole on the right.
   Making the form a column flex container + full-width children gives the
   fields and the button one shared left/right edge, so the block is centred. */
.login-card form {
  display: flex;
  flex-direction: column;
  gap: 10px;
  width: 100%;
}
.login-card input,
.login-card button {
  width: 100%;
}
.login-card .error-text { text-align: center; }

/* ---------- Top bar ---------- */
.topbar {
  position: relative;
  padding: 11px 16px;
  background: linear-gradient(180deg, rgba(14,55,66,0.94), rgba(9,40,50,0.92));
  backdrop-filter: blur(12px) saturate(150%);
  -webkit-backdrop-filter: blur(12px) saturate(150%);
  border-bottom: 2px solid var(--brand-2);   /* the gold signature rule */
  box-shadow: 0 10px 26px rgba(0,0,0,0.34), 0 0 28px rgba(237,183,88,0.13);
  z-index: 10;
}
.topbar-brand {
  display: flex;
  align-items: center;
  gap: 11px;
  min-width: 0;
}
/* The brand block hugs the leading edge; sub-view titles stay centred. */
.topbar-brand .topbar-title { flex: none; text-align: start; }
.topbar-logo {
  display: block;
  width: 32px;
  height: 32px;
  flex: none;
  border-radius: 26%;
  box-shadow: 0 4px 12px rgba(0,0,0,0.45), 0 0 0 1px rgba(255,255,255,0.10);
}
/* .topbar-title and .topbar-subtitle are inline spans, so as flex children their
   line boxes stack flush against each other (measured gap: 0px). The gap is the
   breathing room between the short name and the full factory line. */
.topbar-brand-text { display: flex; flex-direction: column; min-width: 0; gap: 5px; }
.topbar-title {
  font-size: 15px;
  font-weight: 650;
  letter-spacing: 0.3px;
  line-height: 1.15;
}
.topbar-subtitle {
  font-size: 10.5px;
  line-height: 1.2;
  color: var(--brand-2);
  opacity: 0.85;
  letter-spacing: 0.4px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
@media (max-width: 380px) { .topbar-subtitle { display: none; } }

/* ---------- Icon buttons ---------- */
.icon-btn {
  border-radius: 10px;
  background: rgba(255,255,255,0.055);
  border: 1px solid rgba(255,255,255,0.11);
  transition: background 140ms ease, border-color 140ms ease, color 140ms ease,
              transform 140ms ease, box-shadow 140ms ease;
}
.icon-btn:hover {
  background: rgba(237,183,88,0.15);
  border-color: rgba(237,183,88,0.65);
  color: var(--brand-2);
  transform: translateY(-1px);
  box-shadow: 0 5px 14px rgba(0,0,0,0.32);
}
.icon-btn:active { transform: translateY(0); }

/* ---------- Home zone tiles ---------- */
.group-btn {
  position: relative;
  padding: 26px 14px;
  border-radius: 14px;
  overflow: hidden;
  background: linear-gradient(165deg, rgba(255,255,255,0.075), rgba(255,255,255,0.015)), var(--bg-alt);
  border: 1px solid rgba(255,255,255,0.10);
  box-shadow: 0 5px 16px rgba(0,0,0,0.24);
  letter-spacing: 0.2px;
  transition: transform 160ms ease, box-shadow 160ms ease,
              border-color 160ms ease, background 160ms ease;
}
/* Gold rule that draws itself across the tile bottom on hover. */
.group-btn::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 3px;
  background: var(--brand-2);
  transform: scaleX(0);
  transition: transform 220ms cubic-bezier(.2,.85,.3,1);
}
.group-btn:hover {
  transform: translateY(-3px);
  border-color: rgba(237,183,88,0.55);
  background: linear-gradient(165deg, rgba(237,183,88,0.12), rgba(255,255,255,0.02)), var(--bg-alt);
  box-shadow: 0 14px 30px rgba(0,0,0,0.38), 0 0 0 1px rgba(237,183,88,0.22);
}
.group-btn:hover::after { transform: scaleX(1); }

/* ---------- Camera wall ---------- */
.camera-grid {
  gap: 6px;
  padding: 10px;
  background: transparent;   /* tiles now separate on the page colour */
}
.cam-tile {
  border-radius: 10px;
  border: 1px solid rgba(255,255,255,0.06);
  box-shadow: 0 3px 12px rgba(0,0,0,0.32);
  transition: border-color 160ms ease, box-shadow 160ms ease;
}
.cam-tile:not(.enlarged):hover {
  border-color: rgba(237,183,88,0.5);
  box-shadow: 0 10px 26px rgba(0,0,0,0.46), 0 0 0 1px rgba(237,183,88,0.28);
}
/* The zoomed tile is a full-screen cinema view - strip the card chrome. */
.cam-tile.enlarged {
  border-radius: 0;
  border: none;
  box-shadow: none;
}
.cam-tile.offline {
  background: linear-gradient(160deg, #0b2c35, #061e26);
  text-transform: uppercase;
  letter-spacing: 1.3px;
  font-size: 11px;
  font-weight: 600;
  color: var(--text-dim);
}

/* ---------- Tables ---------- */
.mgmt-table th {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.7px;
  font-weight: 600;
  color: var(--text-dim);
  border-bottom: 1px solid rgba(255,255,255,0.16);
}
.mgmt-table tbody tr { transition: background 120ms ease; }
.mgmt-table tbody tr:hover { background: rgba(255,255,255,0.035); }
.mgmt-table input[type="text"], .mgmt-table select {
  background: rgba(5,26,32,0.5);
  border: 1px solid rgba(255,255,255,0.11);
  border-radius: 7px;
  transition: border-color 140ms ease, box-shadow 140ms ease;
}
.row-remove-btn {
  border-radius: 7px;
  padding: 4px 8px;
  transition: background 130ms ease, color 130ms ease;
}
.row-remove-btn:hover { background: rgba(239,68,68,0.16); color: #fca5a5; }
.status-dot { box-shadow: 0 0 0 1px rgba(0,0,0,0.35); }

/* ---------- Modals ---------- */
.modal {
  background: rgba(4,18,23,0.66);
  backdrop-filter: blur(7px);
  -webkit-backdrop-filter: blur(7px);
  animation: fade-in 150ms ease;
}
.modal-card {
  border-radius: 16px;
  padding: 22px;
  background: linear-gradient(165deg, rgba(255,255,255,0.075), rgba(255,255,255,0.015)), var(--bg-alt);
  border: 1px solid rgba(255,255,255,0.11);
  box-shadow: 0 28px 64px rgba(0,0,0,0.52), 0 0 0 1px rgba(237,183,88,0.08);
  animation: pop-in 180ms cubic-bezier(.2,.9,.3,1.15);
}
.modal-card input, .modal-card select {
  border-radius: 9px;
  background: rgba(5,26,32,0.5);
  border: 1px solid rgba(255,255,255,0.12);
}

/* ---------- Buttons ---------- */
.btn-primary {
  background: linear-gradient(180deg, #f3c76d, #dfa63c);
  color: var(--accent-ink);
  border-radius: 9px;
  box-shadow: 0 3px 12px rgba(237,183,88,0.26);
  transition: transform 140ms ease, box-shadow 140ms ease, filter 140ms ease;
}
.btn-primary:hover {
  filter: brightness(1.04);
  transform: translateY(-1px);
  box-shadow: 0 7px 20px rgba(237,183,88,0.36);
}
.btn-primary:active { transform: translateY(0); }
.btn-secondary {
  background: rgba(255,255,255,0.05);
  border: 1px solid rgba(255,255,255,0.13);
  color: var(--text);
  border-radius: 9px;
  transition: background 140ms ease, border-color 140ms ease;
}
.btn-secondary:hover {
  background: rgba(255,255,255,0.10);
  border-color: rgba(255,255,255,0.22);
}

/* ---------- Profile card + toast ---------- */
.profile-card {
  border-radius: 16px;
  background: linear-gradient(165deg, rgba(255,255,255,0.07), rgba(255,255,255,0.015)), var(--bg-alt);
  border: 1px solid rgba(255,255,255,0.10);
  box-shadow: 0 18px 44px rgba(0,0,0,0.36);
}
.profile-card input {
  border-radius: 9px;
  background: rgba(5,26,32,0.5);
  border: 1px solid rgba(255,255,255,0.12);
}
.toast {
  border-radius: 12px;
  background: rgba(9,40,50,0.94);
  backdrop-filter: blur(9px);
  -webkit-backdrop-filter: blur(9px);
  border: 1px solid rgba(237,183,88,0.30);
  box-shadow: 0 14px 36px rgba(0,0,0,0.48);
  animation: toast-in 220ms cubic-bezier(.2,.85,.3,1.05);
}
.toast.error { border-color: rgba(239,68,68,0.7); color: #fca5a5; }

/* ---------- Motion ---------- */
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes pop-in {
  from { opacity: 0; transform: translateY(10px) scale(0.975); }
  to   { opacity: 1; transform: none; }
}
@keyframes rise-in {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}
@keyframes toast-in {
  from { opacity: 0; transform: translate(-50%, 12px); }
  to   { opacity: 1; transform: translate(-50%, 0); }
}

/* Honour "reduce motion" - the polish must never be an accessibility cost. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
}

/* ---------- Clone camera (toolbar button + modal) ---------- */
/* The clone glyph (U+29C9) is typographic rather than an emoji, so it inherits
   the button's own colour and hover treatment exactly like the +/- buttons. */
.clone-btn { font-size: 17px; font-weight: 700; line-height: 1; }
.clone-btn:disabled { opacity: 0.5; }
.modal-label {
  font-size: 12px;
  opacity: 0.85;
  margin-bottom: -4px;   /* tighten the label against the field below it */
}
.modal-hint {
  margin: 0;
  font-size: 11.5px;
  line-height: 1.45;
  color: var(--text-dim);
}
/* The source picker can hold 50+ cameras with long (sometimes Persian) names.
   Pin it to the card width so an over-long option cannot stretch the modal. */
.modal-card select#clone-source,
.modal-card input#clone-count {
  width: 100%;
  max-width: 100%;
  box-sizing: border-box;
}
.modal-card select#clone-source {
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ===================== FACTORY MAP ===================== */
/* The stage is an absolutely-positioned canvas: the plan image (or the built-in
   zone schematic) fills it, and markers sit on top at percentage coordinates.
   Sizing is done in JS (fitMapStage) rather than with CSS aspect-ratio, because
   the box has to fit BOTH the plan's ratio and the space the topbar/toolbar/
   legend leave behind -- and aspect-ratio alone cannot express a "contain"
   constraint in both axes at once. */
.map-main {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 12px 16px 16px;
  overflow: auto;
}

.map-stage {
  position: relative;
  flex: 0 0 auto;
  margin: 0 auto;
  background: var(--bg-dark);
  border: 1px solid var(--border);
  border-radius: 10px;
  /* Deliberately NOT `hidden`: a marker's name label pops out above it, and
     clipping the stage would swallow the label of every camera placed along
     the top edge. The plan image is rounded on its own instead. */
  overflow: visible;
  /* Touch gestures are for panning the page, never for panning the map. */
  touch-action: manipulation;
}

/* object-fit: fill (not contain) is deliberate. The stage's aspect ratio is
   derived from the image's own aspect, so the two already agree -- and any
   letterboxing introduced by `contain` would shift every marker off its spot. */
.map-plan {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: fill;
  border-radius: 9px;
  opacity: 0.85;
  pointer-events: none;
}

/* No plan image configured yet: lay a faint surveyor's grid over the stage so
   the circles read as positions on a drawing instead of stickers on a black
   box. Pure CSS on an element that already exists, so the "no plan" path costs
   the client nothing. */
.map-stage.no-plan {
  background-image:
    linear-gradient(rgba(237, 183, 88, 0.10) 1px, transparent 1px),
    linear-gradient(90deg, rgba(237, 183, 88, 0.10) 1px, transparent 1px);
  background-size: 40px 40px, 40px 40px;
}

/* The marker layer covers the stage but must not swallow clicks meant for the
   stage itself -- individual markers re-enable pointer events. */
.map-markers {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/* ---------- Markers ----------
   A camera IS its live state: a green circle while the stream is up, red when
   it is down, and the host's last octet printed inside it. The camera's own
   name is the tooltip -- 52 permanent name labels would be unreadable at this
   size and would bury the plan underneath.

   NOTE: physical `left`/`top`, NOT logical inset-inline/block. The stage is a
   physical floor plan: mirroring it for RTL would put the east hall on the
   left and make the map a lie. The interface mirrors; the plan does not. */
/* The circles are 70% of their original size: the label identifies the camera
   from across the room, so 30% was taken off the diameter as asked and the
   number only followed to ~77% -- at a true 70% (9.1px) three digits stop
   being readable, and 1-3 digits still clear the ring at 10px. */
.map-marker {
  position: absolute;
  transform: translate(-50%, -50%);
  width: 24px;
  height: 24px;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  border: 1.5px solid rgba(255, 255, 255, 0.85);
  background: #6b7280;
  color: #fff;
  font-family: inherit;
  font-size: 10px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  pointer-events: auto;
  cursor: pointer;
  z-index: 2;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.45);
  transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.map-marker:hover,
.map-marker:focus-visible {
  transform: translate(-50%, -50%) scale(1.18);
  z-index: 5;
  box-shadow: 0 0 0 4px rgba(237, 183, 88, 0.45), 0 2px 8px rgba(0, 0, 0, 0.5);
}

/* The circle's FILL carries the liveness of the camera. */
.map-marker.status-live { background: var(--ok); border-color: #eafff2; }
.map-marker.status-offline { background: var(--off); border-color: #ffe6e6; }
.map-marker.status-unknown { background: #6b7280; border-color: #e5e7eb; }

/* The camera whose quick view is currently open. */
.map-marker.open {
  z-index: 6;
  box-shadow: 0 0 0 3px rgba(237, 183, 88, 0.9), 0 2px 8px rgba(0, 0, 0, 0.55);
}

/* A slow halo on offline cameras so a dead one is findable at a glance
   without animating all 52 markers (which the i3 client machines would pay
   for in repaints). */
.map-marker.status-offline::after {
  content: '';
  position: absolute;
  inset: -2px;
  border-radius: 50%;
  border: 1.5px solid var(--off);
  animation: map-pulse 2.4s ease-out infinite;
}
@keyframes map-pulse {
  0%   { opacity: 0.9; transform: scale(1); }
  100% { opacity: 0;   transform: scale(1.9); }
}
@media (prefers-reduced-motion: reduce) {
  .map-marker.status-offline::after { animation: none; opacity: 0.5; }
}

/* ---------- Arrange (edit) mode ---------- */
.map-stage.editing .map-marker {
  cursor: grab;
  /* Without this the browser claims the gesture for scrolling on touch and
     the drag never reaches our pointermove handler. */
  touch-action: none;
  width: 21px;
  height: 21px;
}
.map-stage.editing .map-marker.dragging {
  cursor: grabbing;
  z-index: 7;
  transform: translate(-50%, -50%) scale(1.25);
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.45);
  transition: none; /* follow the finger exactly, no easing lag */
}

.map-toolbar {
  flex: 0 0 auto;
  width: 100%;
  max-width: 900px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: 9px;
  padding: 11px 13px;
  background: var(--bg-alt);
  border: 1px solid var(--border);
  border-radius: 10px;
}
.map-hint {
  margin: 0;
  font-size: 12.5px;
  color: var(--brand-2);
}

/* Upload block. Its own bordered row so the button reads as the primary way to
   set a plan, with the URL field below as the fallback. */
.map-upload {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 10px;
  padding: 10px 12px;
  border: 1px dashed var(--border);
  border-radius: 9px;
  background: var(--bg-dark);
}
/* The filename is a filename: LTR always, even under RTL, or a mixed name like
   "plan-1402.webp" gets its segments reordered by the bidi algorithm. */
.map-upload-name {
  min-width: 0;
  font-size: 12px;
  color: var(--text-dim);
  direction: ltr;
  unicode-bidi: plaintext;
  text-align: start;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Full width forces the spec line onto its own row under the button, so it
   never competes with the filename for the same line. */
.map-upload-note {
  flex: 1 1 100%;
  margin: 0;
  font-size: 11.5px;
  line-height: 1.6;
  color: var(--text-dim);
  opacity: 0.92;
}
.map-toolbar-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 9px;
}
.map-field {
  display: flex;
  flex-direction: column;
  gap: 3px;
  font-size: 11.5px;
  color: var(--text-dim);
}
.map-field input {
  padding: 6px 8px;
  border-radius: 6px;
  border: 1px solid var(--border);
  background: var(--bg-dark);
  color: var(--text);
  font-family: inherit;
  font-size: 12.5px;
}
.map-field:first-child { flex: 1 1 220px; }
.map-field:nth-child(2) { flex: 0 1 110px; }
.map-toolbar-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
}

.map-legend {
  flex: 0 0 auto;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 14px;
  font-size: 12px;
  color: var(--text-dim);
}
.map-legend-item { display: inline-flex; align-items: center; gap: 6px; }
.map-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  display: inline-block;
}
.map-dot.live { background: var(--ok); }
.map-dot.offline { background: var(--off); }
.map-dot.unknown { background: var(--text-dim); }
.map-legend-count { color: var(--text); }

/* Tight stage (a phone, or a laptop with the arrangement toolbar open): the
   circles shrink so 50+ of them in a small box stop bumping into each other.
   Kept in the same 70%-of-base proportion as the default circle, so the two
   states still differ by the same step. NOTE: at 20px this is now under the
   ~24px comfortable-touch-target size -- see the marker-size note. */
.map-stage.compact .map-marker { width: 20px; height: 20px; font-size: 9px; }

/* ---------- Quick view popup ----------
   A centred modal over the plan, not a callout beside the marker: it is always
   in the same place, so the eye knows where to look and the placement can never
   strand it off the stage edge. */
/* Dim layer under the centred quick view. It exists because centring the popup
   necessarily puts it on top of the circle that opened it, and the circle was
   the control that closed it again. Clicking this backdrop restores that
   gesture -- "second click returns to the map" now holds anywhere on the map,
   not just on the one circle. Sits below the popup (z-index 19 vs 20) and above
   the markers. */
.map-popup-backdrop {
  position: absolute;
  inset: 0;
  z-index: 19;
  background: rgba(3, 12, 16, 0.62);
  border-radius: 9px;
}

/* Dead-centre in the stage, both axes, at every size. `translate(-50%, -50%)`
   is what makes that exact regardless of the popup's own size -- centring with
   JS arithmetic drifts the moment the video's intrinsic size settles and the
   box grows underneath the numbers. Deliberately no `backdrop-filter: blur()`:
   the operator clients are i3s on integrated graphics, and a blur is not worth
   a dropped frame on the live wall behind it. */
.map-popup {
  position: absolute;
  z-index: 20;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  /* Sized to FILL the stage, not to sit in the middle of it. The cap is 1800px
     because the stream behind it is the 2560x1440 MAIN feed -- there is real
     detail to show, so the preview should be as large as the stage allows
     rather than a thumbnail. 88% width / 96% height are the practical maximums
     that keep the 16:9 body from being clamped by max-height: a taller popup
     than the stage can hold would letterbox the video, and the letterbox bars
     are what make a big preview feel broken. On a 1920x1080 operator screen the
     stage lands near 1724x970, so the percentage governs and the popup opens
     1517x890 -- about 2.2x the linear size of the old 680px cap, with no bars. */
  width: min(1800px, 88%);
  max-height: 96%;
  /* Flex column so the video area can give height back on a short stage instead
     of being clipped by max-height above. Safe against the global
     `[hidden] { display: none !important }` -- that one wins outright. */
  display: flex;
  flex-direction: column;
  background: var(--bg-alt);
  border: 1px solid var(--brand-2);
  border-radius: 10px;
  box-shadow: 0 12px 34px rgba(0, 0, 0, 0.6);
  overflow: hidden;
}
.map-popup-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  /* The bar scales with the frame: a 12.5px title over a 1500px-wide video
     reads as a stray label rather than a title. */
  padding: 8px 12px;
  background: var(--brand);
  color: var(--text);
  font-size: 13.5px;
}
.map-popup-title {
  flex: 1;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.map-popup-close {
  flex: 0 0 auto;
  width: 28px;
  height: 28px;
  padding: 0;
  border: none;
  border-radius: 6px;
  background: transparent;
  color: var(--text);
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
}
.map-popup-close:hover { background: rgba(255, 255, 255, 0.14); }
.map-popup-body {
  position: relative;
  /* 16:9 is the natural shape, but as a shrinkable flex item it surrenders
     height on a short stage rather than overflowing the popup. The video is
     object-fit: contain, so any mismatch costs a little letterboxing, never a
     cropped picture. */
  flex: 1 1 auto;
  min-height: 0;
  aspect-ratio: 16 / 9;
  background: #000;
}
.map-popup-video {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: contain;
  background: #000;
}
/* Shown over the black video box until the first frame actually lands, so a
   slow on-demand main stream never looks like a broken player. Hidden by JS
   on the first progress event. */
.map-popup-state {
  position: absolute;
  inset: 0;
  margin: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-dim);
  font-size: 12.5px;
  text-align: center;
  padding: 10px;
  pointer-events: none;
}
.map-popup-state.hidden { display: none; }

/* ===================== CHOOSER (post-login landing) ===================== */
.chooser-grid {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 22px;
  padding: 18px 20px 34px;
}
.chooser-card {
  flex: 0 1 300px;
  min-height: 190px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 9px;
  padding: 26px 20px;
  background: var(--bg-alt);
  border: 1px solid var(--border);
  border-radius: 14px;
  color: var(--text);
  font-family: inherit;
  cursor: pointer;
  transition: transform 0.14s ease, border-color 0.14s ease, background 0.14s ease;
}
.chooser-card:hover,
.chooser-card:focus-visible {
  transform: translateY(-3px);
  border-color: var(--brand-2);
  background: var(--bg-hover);
}
.chooser-icon { font-size: 46px; line-height: 1; }
.chooser-label {
  font-size: 19px;
  font-weight: 600;
  color: var(--brand-2);
  text-align: center;
}
.chooser-sub {
  font-size: 12.5px;
  color: var(--text-dim);
  text-align: center;
}
@media (max-width: 700px) {
  .chooser-grid { gap: 14px; padding: 12px 14px 26px; }
  .chooser-card { flex: 1 1 100%; min-height: 0; padding: 20px 16px; }
  .chooser-icon { font-size: 36px; }
  .chooser-label { font-size: 17px; }
  /* Phone: the stage is small, so give the popup nearly all of it -- a 16:9
     video inside a phone-sized stage is already short. */
  .map-popup { width: min(680px, 96%); }
}

/* ---------- Arrange mode: per-user camera order ---------- */
/* Every signed-in user can arrange their own wall. The edit button turns the
   grid into a drag board; Save stores the order against that account only.
   Tiles keep playing throughout -- a reorder MOVES the existing nodes instead
   of rebuilding them, so no stream is torn down or restarted. */

.icon-btn.save-btn {
  background: linear-gradient(180deg, #f3c76d, #dfa63c);
  border-color: transparent;
  color: var(--accent-ink, #0d3540);
  font-weight: 700;
}
.icon-btn.save-btn:hover { filter: brightness(1.05); }

/* Save / Cancel are the only two .icon-btn's that carry a WORD instead of a glyph
   -- every other one is an arrow or an emoji. The 36x36 square base leaves a 34px
   content box, and the labels do not fit it: measured 2px of overflow for ذخیره and
   5px for انصراف, with the Kurdish pair (پاشەکەوتکردن / پاشگەزبوونەوە) far longer
   again. So these two size themselves to their label instead of to the square; the
   square stays the rule for every glyph button. */
#btn-live-save,
#btn-live-cancel {
  width: auto;
  min-width: 36px;
  padding-inline: 12px;
  white-space: nowrap;
  /* Never let a crowded topbar squeeze these back down: with nowrap content a
     shrink would just re-create the overflow this rule exists to fix. The title
     (flex:1) gives up its space instead. */
  flex: none;
}


/* A dragging tile must not scroll the page instead of moving; touch-action:none
   is what makes pointer drags work under a finger. */
.camera-grid.editing .cam-tile {
  cursor: grab;
  touch-action: none;
  outline: 1px dashed rgba(237, 183, 88, 0.5);
  outline-offset: -3px;
}
.camera-grid.editing .cam-tile.dragging {
  cursor: grabbing;
  opacity: 0.55;
  z-index: 2;
  outline: 2px dashed #edb758;
  outline-offset: -3px;
}

/* Shown only while arranging, and only on a group with more than one page. */
.tile-move-btn {
  position: absolute;
  top: 7px;
  inset-inline-end: 7px;
  display: none;
  z-index: 3;
  width: 30px;
  height: 30px;
  padding: 0;
  border-radius: 8px;
  border: 1px solid rgba(237, 183, 88, 0.55);
  background: rgba(6, 24, 30, 0.82);
  color: #edb758;
  font: inherit;
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  transition: background 140ms ease, transform 140ms ease;
}
.tile-move-btn:hover {
  background: rgba(237, 183, 88, 0.9);
  color: #0d3540;
  transform: scale(1.06);
}
.camera-grid.editing .tile-move-btn { display: block; }

/* Page chooser inside the move modal. */
.page-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  justify-content: center;
  margin: 4px 0 2px;
}
.page-chip {
  min-width: 44px;
  padding: 9px 14px;
  border-radius: 9px;
  border: 1px solid rgba(255, 255, 255, 0.13);
  background: rgba(255, 255, 255, 0.05);
  color: inherit;
  font: inherit;
  font-size: 14px;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  transition: background 140ms ease, border-color 140ms ease;
}
.page-chip:hover:not(:disabled) {
  background: rgba(237, 183, 88, 0.16);
  border-color: rgba(237, 183, 88, 0.5);
}
.page-chip.current {
  background: rgba(237, 183, 88, 0.9);
  border-color: transparent;
  color: #0d3540;
  font-weight: 700;
  cursor: default;
}

/* ===================== ADMIN AUDIT LOG (view-logs) ===================== */
/* The log page was first written against a LIGHT palette -- dark-petrol ink
   (#0d3540) on a white panel -- while the site itself is dark petrol. That is
   the reported bug: a white slab behind the rows, pale #9cc0c8 text on it, and
   filter labels in dark ink on a dark page. Everything below is the dark
   palette: raised petrol panels, --text on them, --accent gold for the audit
   vocabulary. Contrast, measured on the real render:
     #eaf4f6 on #11414f  ~ 11:1    body cells
     #9cc0c8 on #11414f  ~ 5.6:1   secondary (details, summary, row numbers)
     #edb758 on #0a2b35  ~  8:1    sticky header
   Keep any new log style on these variables; never hard-code #fff here again. */
.log-filters{display:flex;flex-wrap:wrap;gap:10px;align-items:flex-end;margin:0 0 10px;
  padding:10px 12px;background:var(--bg-alt);border:1px solid var(--border);
  border-radius:12px}
.log-filter{display:flex;flex-direction:column;gap:4px;font-size:.78rem;color:var(--text-dim)}
.log-filter select,.log-filter input{font:inherit;font-size:.86rem;padding:6px 8px;
  border:1px solid var(--border);border-radius:8px;background:var(--bg);color:var(--text);
  min-width:130px}
.log-filter input[type="search"]{min-width:230px}
.log-filter input::placeholder{color:var(--text-dim);opacity:.7}
.log-summary{margin:0 0 8px;font-size:.8rem;color:var(--text-dim);min-height:1.1em}
/* Its own scroll box so the sticky header works and the page itself never
   scrolls sideways. */
.log-table-wrap{overflow:auto;max-height:calc(100vh - 260px);border:1px solid var(--border);
  border-radius:12px;background:var(--bg-alt)}
.log-table{margin:0;min-width:880px}
.log-table thead th{position:sticky;top:0;z-index:2;background:var(--bg-dark);color:var(--accent)}
.log-table th,.log-table td{white-space:nowrap}
/* Rows sit on the raised petrol panel and carry --text. They used to inherit
   the pale --text-dim, which is what washed them out on the old white panel. */
.log-table tbody td{color:var(--text)}
.log-table td.log-detail{white-space:normal;max-width:340px;color:var(--text-dim)}
.log-table th.row-num,.log-table td.row-num{color:var(--text-dim)}
/* Dates and times line up column-wise with tabular figures. */
.log-date,.log-time{font-variant-numeric:tabular-nums}
.log-actor{font-weight:500;color:var(--text)}
.log-empty{margin:14px 0;text-align:center;color:var(--text-dim)}
.log-act{display:inline-block;padding:2px 8px;border-radius:999px;font-size:.76rem;
  background:rgba(255,255,255,.10);color:var(--text)}
.log-act-camera{background:rgba(237,183,88,.22);color:var(--accent)}
.log-act-auth{background:rgba(255,255,255,.14)}
.log-act-system{background:rgba(156,192,200,.16)}

/* Log view: its TEXT is English by design, so the content is laid out LTR even
   when the app itself is RTL -- an English sentence with ' — ' and ' · '
   separators reorders confusingly inside an RTL paragraph. Scoped to main so
   the topbar (back button on the right, like every other view) is unchanged. */
#view-logs .mgmt-main { direction: ltr; }
.log-note { margin: 6px 0 0; font-size: 13px; color: var(--text-dim); }


/* ---------- Site-wide footer ----------
   One node, moved into the active view by showView(), so it appears under the
   content of every page. The views are 100vh flex columns and this is their
   last child: every <main>/grid in this file is flex:1, so they give up the
   ~32px it needs and nothing else has to change. `dir="auto"` in the markup
   keeps the Latin attribution LTR inside the RTL Persian and Kurdish pages. */
.site-footer {
  flex: 0 0 auto;
  padding: 9px 14px 11px;
  text-align: center;
  font-size: .72rem;
  line-height: 1.35;
  color: var(--text-dim);
  border-top: 1px solid var(--border);
}
.site-footer a {
  color: var(--accent);
  text-decoration: none;
  font-weight: 600;
}
.site-footer a:hover,
.site-footer a:focus-visible {
  text-decoration: underline;
}

/* ---------- Log page fills the viewport (v60) ----------
   The table used to size itself with calc(100vh - 260px): a guess at the height
   of the chrome around it -- topbar, filters, summary, note, pager. v60 added
   the site footer to that stack and the guess came up 14px short, dropping the
   pager just below the fold on a 577px-tall window. Instead of retuning the
   constant, the page becomes a flex column so the table takes exactly the space
   that is left over, and keeps doing so when anything around it changes. Every
   sibling is pinned to its natural height so only the table grows or shrinks.
   The 260px rule above no longer binds; it is kept as the pre-v60 baseline. */
#view-logs .mgmt-main {
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
#view-logs .log-filters,
#view-logs .log-summary,
#view-logs .log-note,
#view-logs .log-empty,
#view-logs .pager { flex: 0 0 auto; }
#view-logs .log-table-wrap {
  flex: 1;
  min-height: 0;
  max-height: none;
}
