/* ============================================================================
   Theasis — control room
   ============================================================================

   One idea runs through this file: **light comes from the video, not from the
   chrome.** A monitoring room is dim on purpose — a bright interface beside a
   night-time infrared feed destroys the operator's dark adaptation and makes
   the picture itself harder to read. So every surface here is recessive and
   the camera tiles are the only bright rectangles on the page.

   The second idea follows from the first: **colour only ever means state.**
   There is no brand accent. Buttons are built from surface value and a
   hairline, not from hue. The only saturated pixels in the interface are a
   live indicator, a fault, and a stall — because in a product whose whole job
   is telling you whether something is wrong, an interface that is blue all
   over has spent the signal before there is anything to signal.

   Consequences worth not undoing:

   - No box-shadows. Depth is surface value and a 1px line. A soft grey shadow
     under every card is the thing that makes an interface look assembled from
     a kit rather than designed for a job.
   - No uppercase, no letter-spacing on labels. Tracked-out small caps read as
     decoration, and Greek loses its tonos under `text-transform: uppercase`.
   - One typeface. Figures are tabular everywhere, because channel numbers,
     ports and times sit in columns and must not shuffle.
   - Radii are small and vary by meaning: 3px on controls, 2px on a video tile
     (a monitor is square), 0 on the rail.
   ========================================================================= */

:root {
  /* ── Surfaces: a dim room, not a black one ──
     Deliberately mid-dark rather than near-black. Pure black chrome against a
     bright feed produces halation around the tile and makes the video edge
     glare; broadcast monitoring furniture is grey for the same reason. */
  --surface-0: #15181c;   /* page */
  --surface-1: #1c2026;   /* rail, bars */
  --surface-2: #242930;   /* controls at rest */
  --surface-3: #2d333b;   /* controls hovered */
  --cell: #1a1e23;        /* an unlit monitor */

  --line: #2b313a;
  --line-firm: #3c4550;

  --ink: #e3e7eb;
  --ink-2: #98a2ad;
  --ink-3: #6c7682;

  /* ── State. The only colour in the interface. ── */
  --live: #3ac07d;
  --fault: #e0574f;
  --hold: #d9a441;
  --on-inverted: #14171b;  /* text on an inverted (primary) control */

  --r-control: 3px;
  --r-tile: 2px;

  /* The rail is wide enough for a site name plus a 8-channel strip, and no
     wider — every pixel it takes is a pixel off the video. */
  --rail: 288px;
  --bar: 52px;
}

/* ── Daylight ──
   Not a courtesy: this app is opened on a phone, outdoors, by someone standing
   in a yard. The surfaces invert but the rule does not — the video is still
   the brightest thing, because everything around it is paper, not white. */
html.light-theme {
  --surface-0: #e9ecee;
  --surface-1: #ffffff;
  --surface-2: #f2f4f6;
  --surface-3: #e4e8eb;
  --cell: #dfe3e6;

  --line: #d2d7dc;
  --line-firm: #a7b0b9;

  --ink: #14181c;
  --ink-2: #566069;
  --ink-3: #7d8790;

  --live: #0f7a47;
  --fault: #b32d26;
  --hold: #8a5d15;
  --on-inverted: #ffffff;
}

/* ============================================================================
   Base
   ========================================================================= */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  color-scheme: dark;
}

html.light-theme {
  color-scheme: light;
}

body {
  margin: 0;
  min-height: 100vh;
  background: var(--surface-0);
  color: var(--ink);
  font-family: 'Fira Sans', system-ui, sans-serif;
  font-size: 14px;
  line-height: 1.45;
  /* Ports, channel numbers and times sit under one another in the rail and in
     the admin tables. Proportional figures make those columns wobble. */
  font-variant-numeric: tabular-nums;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4 {
  margin: 0;
  font-weight: 600;
  line-height: 1.2;
}

p {
  margin: 0;
}

a {
  color: var(--ink);
}

.hidden {
  display: none !important;
}

/* A carry-over from the previous design, kept because index.html still has the
   element. It draws nothing. */
.page-atmosphere {
  display: none;
}

::selection {
  background: var(--ink);
  color: var(--surface-0);
}

:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}

/* ============================================================================
   Identity
   ========================================================================= */

.wordmark {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  font-size: 15px;
  font-weight: 700;
  /* No tracking: it suited the old acronym (VMS), not a word in lower case. */
  color: var(--ink);
  text-decoration: none;
}

.wordmark-mark {
  flex: none;
  width: 16px;
  height: 16px;
  /* The one place a lens shape earns its keep: it is the product's only
     ornament, so it is not also tinted. */
  color: var(--ink-2);
}

.wordmark-sub { /* unused since the login tagline came off; kept for the notice pages */
  margin-top: 6px;
  color: var(--ink-3);
  font-size: 13px;
}

/* ============================================================================
   Controls

   Every button in the app is this one shape. The difference between them is
   surface value, not colour — including the primary action, which is the
   inverted surface rather than a coloured fill. An inverted button is the most
   emphatic thing available on a page with no accent, which is exactly what
   "primary" should mean.
   ========================================================================= */

button,
.btn {
  appearance: none;
  border: 1px solid var(--line-firm);
  border-radius: var(--r-control);
  background: var(--surface-2);
  color: var(--ink);
  padding: 6px 11px;
  font-family: inherit;
  font-size: 13px;
  font-weight: 500;
  line-height: 1.4;
  cursor: pointer;
  transition: background 110ms ease, border-color 110ms ease, color 110ms ease;
}

button:hover {
  background: var(--surface-3);
  border-color: var(--ink-3);
}

button:active {
  background: var(--surface-2);
}

button[disabled] {
  opacity: 0.45;
  cursor: not-allowed;
}

button[disabled]:hover {
  background: var(--surface-2);
  border-color: var(--line-firm);
}

.ghost-btn {
  background: transparent;
  border-color: var(--line);
  color: var(--ink-2);
}

.ghost-btn:hover {
  background: var(--surface-2);
  border-color: var(--line-firm);
  color: var(--ink);
}

.primary-btn {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--on-inverted);
  font-weight: 600;
}

.primary-btn:hover {
  background: var(--ink-2);
  border-color: var(--ink-2);
  color: var(--on-inverted);
}

/* Opens a form; it is not the action itself, so it is quiet. An inverted
   full-width button here made "add an NVR" the brightest object on a page
   whose subject is live video. */
.toggle-add-form-btn {
  width: 100%;
  margin-bottom: 10px;
  background: transparent;
  border-color: var(--line);
  color: var(--ink-2);
  font-weight: 500;
}

.toggle-add-form-btn:hover {
  background: var(--surface-2);
  border-color: var(--line-firm);
  color: var(--ink);
}

/* Destructive actions are the one place a hue is allowed outside status,
   because "this cannot be undone" is itself a state. Outlined, not filled: it
   has to be aimed at rather than fallen onto. */
.danger-btn,
.danger-outline-btn {
  background: transparent;
  border-color: var(--line-firm);
  color: var(--fault);
}

.danger-btn:hover,
.danger-outline-btn:hover {
  background: var(--fault);
  border-color: var(--fault);
  color: #fff;
}

.logout-btn {
  background: transparent;
  border-color: var(--line);
  color: var(--ink-2);
}

.logout-btn:hover {
  background: var(--surface-2);
  border-color: var(--line-firm);
  color: var(--ink);
}

/* Second click of the two-step delete. Amber is "about to happen". */
.delete-confirming,
.delete-confirming:hover {
  background: var(--hold);
  border-color: var(--hold);
  color: var(--on-inverted);
}

/* ── Inputs ──
   Recessed rather than raised: a field is a hole in the panel you type into.
   16px on touch because iOS Safari zooms the viewport on any smaller font. */
input,
select,
textarea {
  width: 100%;
  border: 1px solid var(--line-firm);
  border-radius: var(--r-control);
  background: var(--surface-0);
  color: var(--ink);
  padding: 7px 10px;
  font-family: inherit;
  font-size: 14px;
  font-variant-numeric: tabular-nums;
}

input::placeholder {
  color: var(--ink-3);
}

input:focus-visible,
select:focus-visible {
  outline: 2px solid var(--ink-2);
  outline-offset: -1px;
  border-color: var(--ink-2);
}

select {
  cursor: pointer;
}

label {
  display: block;
  font-size: 13px;
  color: var(--ink-2);
}

label input,
label select {
  margin-top: 4px;
}

/* Sentence case, no tracking. A tracked-out uppercase label above every field
   is decoration pretending to be hierarchy. */
.label {
  display: block;
  margin-bottom: 5px;
  font-size: 12px;
  font-weight: 500;
  color: var(--ink-3);
}

/* ============================================================================
   Language switch

   Two segments, not one button that names the other language. A single button
   reading "EN" is ambiguous — it could mean "you are in English" or "switch to
   English" — and that ambiguity lands hardest on the one person who cannot
   read the rest of the screen.
   ========================================================================= */

.lang-switch {
  display: inline-flex;
  border: 1px solid var(--line);
  border-radius: var(--r-control);
  overflow: hidden;
}

.lang-btn {
  border: none;
  border-radius: 0;
  background: transparent;
  color: var(--ink-3);
  padding: 4px 8px;
  font-size: 12px;
  font-weight: 600;
}

.lang-btn + .lang-btn {
  border-left: 1px solid var(--line);
}

.lang-btn:hover {
  background: var(--surface-2);
  color: var(--ink);
  border-color: transparent;
}

/* aria-pressed rather than a class, so the state a screen reader announces and
   the state that is painted are the same fact and cannot drift. */
.lang-btn[aria-pressed='true'],
.lang-btn[aria-pressed='true']:hover {
  background: var(--surface-3);
  color: var(--ink);
}

.theme-toggle-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 5px 7px;
  background: transparent;
  border-color: var(--line);
  color: var(--ink-3);
}

/* An icon button in the top bar, styled like the theme button beside it. The
   words are in its label and tooltip; .icon-btn-text shows them only in the
   phone menu, where nothing can be hovered. */
.icon-btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 5px 7px;
  background: transparent;
  border-color: var(--line);
  color: var(--ink-3);
}

.icon-btn:hover {
  color: var(--ink);
}

.icon-btn-text {
  display: none;
}

/* Line drawings in the text colour, so they follow the theme and stay
   achromatic: colour means state here, and a yellow sun would be a brand
   accent by the back door. */
.line-icon,
.theme-icon {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Each icon is the theme a click switches to. Display rules rather than
   .hidden, which the stylesheet could never override (see .hidden). */
.theme-icon-moon,
.light-theme .theme-icon-sun {
  display: none;
}

.light-theme .theme-icon-moon {
  display: block;
}

.theme-toggle-btn:hover {
  color: var(--ink);
}

/* ============================================================================
   Sign in

   The card sits on the wall it is about to switch on: the background is the
   dashboard's own tile grid, unlit. Before you sign in the monitors are dark,
   and that is the whole of the illustration — no gradient, no blur, no
   photograph of a server room.
   ========================================================================= */

.login-view {
  position: relative;
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: var(--surface-0);
  overflow: hidden;
}

.login-view::before {
  content: '';
  position: absolute;
  inset: -40px;
  /* Cells of about 16:9 with a 12px gutter. The gutters are painted in the
     page colour over a flat tint, which is what turns two stripe gradients
     into a grid of rectangles rather than a plaid. */
  background-color: var(--cell);
  background-image:
    repeating-linear-gradient(90deg, transparent 0 112px, var(--surface-0) 112px 120px),
    repeating-linear-gradient(180deg, transparent 0 63px, var(--surface-0) 63px 71px);
  opacity: 0.55;
}

.login-card {
  position: relative;
  width: 100%;
  max-width: 352px;
  padding: 26px 26px 20px;
  border: 1px solid var(--line-firm);
  border-radius: var(--r-control);
  background: var(--surface-1);
  animation: lift 420ms cubic-bezier(0.2, 0.7, 0.3, 1) both;
}

.login-card .wordmark {
  font-size: 17px;
}

.login-form {
  margin-top: 22px;
  display: grid;
  gap: 14px;
}

.login-field {
  display: grid;
  gap: 5px;
}

.login-label {
  font-size: 12px;
  font-weight: 500;
  color: var(--ink-3);
}

.login-card input {
  padding: 9px 11px;
  font-size: 16px;
}

.login-card .primary-btn {
  width: 100%;
  margin-top: 2px;
  padding: 10px;
  font-size: 14px;
}

/* The rule down the left edge carries the alarm; the sentence stays in the
   body colour. --fault on --surface-2 measures 2.7:1, which fails AA for text
   at this size, and an error message is the last thing that should be hard to
   read. */
.login-error {
  margin-top: 16px;
  padding: 9px 11px;
  border: 1px solid var(--line);
  border-left: 2px solid var(--fault);
  border-radius: var(--r-control);
  background: var(--surface-2);
  color: var(--ink);
  font-size: 13px;
  line-height: 1.45;
  overflow-wrap: break-word;
}

/* main.js clears it with textContent = '' — this is what hides it. */
.login-error:empty {
  display: none;
}

.login-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 20px;
  padding-top: 14px;
  border-top: 1px solid var(--line);
}

.login-links {
  display: flex;
  gap: 12px;
}

.login-links a {
  color: var(--ink-3);
  font-size: 12px;
  text-decoration: none;
}

.login-links a:hover {
  color: var(--ink);
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* Terms acceptance: the login card, wider, because it carries sentences
   rather than two fields. No new colour — the checkbox and the disabled
   button carry the state, as everywhere else. */
.terms-card {
  max-width: 460px;
}

.terms-heading {
  font-size: 17px;
  font-weight: 600;
}

.terms-intro {
  margin-top: 12px;
  color: var(--ink-2);
  font-size: 14px;
  line-height: 1.5;
}

.terms-points {
  margin: 10px 0 0;
  padding-left: 18px;
  display: grid;
  gap: 6px;
  font-size: 14px;
  line-height: 1.45;
}

.terms-links {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  margin-top: 14px;
  font-size: 13px;
}

.terms-links a {
  color: var(--ink);
  text-underline-offset: 3px;
}

.terms-form {
  margin-top: 18px;
  display: grid;
  gap: 14px;
}

.terms-check {
  display: flex;
  align-items: flex-start;
  gap: 9px;
  font-size: 14px;
  line-height: 1.4;
  cursor: pointer;
}

.terms-check input {
  margin-top: 2px;
  width: 16px;
  height: 16px;
  flex: none;
}

.terms-form .primary-btn {
  width: 100%;
  padding: 10px;
  font-size: 14px;
}

.terms-form .primary-btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/* The service agreement: the same card again, widest of the three, because it
   is a form of six fields. Two columns where a pair belongs together, one on a
   phone. */
.agreement-card {
  max-width: 560px;
}

.agreement-form {
  margin-top: 16px;
  display: grid;
  gap: 16px;
}

.agreement-group {
  margin: 0;
  padding: 0;
  border: 0;
  display: grid;
  gap: 12px;
}

.agreement-group legend {
  margin-bottom: 8px;
  padding: 0;
  font-size: 13px;
  font-weight: 600;
  color: var(--ink-2);
}

.agreement-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
}

@media (max-width: 480px) {
  .agreement-row {
    grid-template-columns: 1fr;
  }
}

.agreement-form input[type='text'],
.agreement-form input[type='tel'] {
  padding: 9px 11px;
  font-size: 16px;
}

/* A rejected field is a state, so it may carry the fault colour — on the line,
   not the text, as with the login error. */
.agreement-form input[aria-invalid='true'] {
  border-color: var(--fault);
}

.agreement-form .primary-btn {
  width: 100%;
  padding: 10px;
  font-size: 14px;
}

.agreement-form .primary-btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.waiting-actions {
  margin-top: 18px;
}

.waiting-actions .primary-btn {
  width: 100%;
  padding: 10px;
  font-size: 14px;
}

/* "Want access for your business?" under the login form. Quiet: it is for the
   occasional visitor without an account, not for the people signing in. */
.login-access {
  margin-top: 14px;
  color: var(--ink-3);
  font-size: 12px;
  line-height: 1.5;
}

.login-access a {
  color: var(--ink-2);
  text-underline-offset: 3px;
}

/* ============================================================================
   Top bar

   One bar, not two. The previous design spent 130px of vertical room on a
   title and a subtitle above a dashboard whose subject is video — on a laptop
   that is most of a camera tile.
   ========================================================================= */

.topbar {
  position: sticky;
  top: 0;
  z-index: 20;
  height: var(--bar);
  display: flex;
  align-items: center;
  padding: 0 16px;
  border-bottom: 1px solid var(--line);
  background: var(--surface-1);
}

.topbar-inner {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 14px;
}

.topbar-spacer {
  flex: 1;
}

/* On a computer the menu wrapper takes no box of its own: its children are
   laid out in the bar exactly as they were before it existed. */
.topbar-menu {
  display: contents;
}

/* "Πηγές" and "Μενού" exist for the phone layout only. */
.topbar-phone-only {
  display: none;
}

.drawer-scrim {
  display: none;
}

.identity-row {
  display: flex;
  align-items: center;
  gap: 8px;
}

.identity-email {
  font-size: 13px;
  /* An address has no natural break and would push the bar wider than a phone. */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--ink-2);
}

/* A role is a fact about the account, not a badge to win. */
.identity-role {
  font-size: 11px;
  color: var(--ink-3);
  border: 1px solid var(--line);
  border-radius: 2px;
  padding: 1px 5px;
}

.view-as {
  display: flex;
  align-items: center;
  gap: 7px;
}

.view-as-label {
  font-size: 12px;
  color: var(--ink-3);
  white-space: nowrap;
}

.view-as select {
  width: auto;
  min-width: 170px;
  padding: 4px 8px;
  font-size: 13px;
  background: var(--surface-2);
}

/* ============================================================================
   Shell: a rail of sources, and the wall
   ========================================================================= */

.layout {
  display: grid;
  grid-template-columns: var(--rail) minmax(0, 1fr);
  min-height: calc(100vh - var(--bar));
}

/* The rail is furniture. It is not a card, it has no radius, and it does not
   compete with the video for attention. */
.sites-panel {
  position: sticky;
  top: var(--bar);
  height: calc(100vh - var(--bar));
  overflow-y: auto;
  padding: 14px;
  border-right: 1px solid var(--line);
  background: var(--surface-1);
}

.streams-panel {
  padding: 10px 14px 14px;
  min-width: 0;
}

.panel-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 12px;
}

.panel-header h2 {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink-2);
}

.panel-caption {
  margin-top: 4px;
  color: var(--ink-3);
  font-size: 12px;
  line-height: 1.4;
}

.search-block {
  margin-bottom: 12px;
}

/* ── Source groups ── */

.sites-list {
  display: grid;
  gap: 2px;
}

.ddns-group {
  border-top: 1px solid var(--line);
}

.ddns-group:last-child {
  border-bottom: 1px solid var(--line);
}

.ddns-toggle {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  border: none;
  border-radius: 0;
  background: transparent;
  color: var(--ink);
  padding: 9px 2px;
  font-size: 13px;
  font-weight: 600;
  text-align: left;
}

.ddns-toggle:hover {
  background: var(--surface-2);
  border-color: transparent;
}

/* Drawn from borders rather than set as a character: ▸ is not in Fira Sans and
   fell back to a tofu box. A disclosure marker that depends on a glyph being
   present in whichever font loaded is a marker that sometimes is not there. */
.ddns-title::before {
  content: '';
  display: inline-block;
  width: 0;
  height: 0;
  margin-right: 9px;
  vertical-align: 1px;
  border-left: 5px solid var(--ink-3);
  border-top: 4px solid transparent;
  border-bottom: 4px solid transparent;
  transition: transform 120ms ease;
}

.ddns-group.is-open .ddns-title::before {
  transform: rotate(90deg);
}

.ddns-count {
  flex: none;
  font-size: 12px;
  font-weight: 400;
  color: var(--ink-3);
}

.ddns-sites {
  display: grid;
  gap: 10px;
  padding: 0 0 10px 14px;
}

.site-card {
  display: grid;
  gap: 7px;
}

/* Title and address take the first line with the brand tag pinned right; the
   two admin buttons drop to a line of their own. Side by side they needed about
   144px, which in a 288px rail left the site name nothing and pushed the delete
   button out past the edge. `display: contents` on the wrapper promotes both
   into this flex row so they can wrap independently — without it the tag and
   the buttons are one unbreakable block. */
.site-head-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 4px 8px;
}

.site-head-row > :first-child {
  flex: 1 1 auto;
  min-width: 0;
}

.site-title {
  font-size: 13px;
  font-weight: 500;
}

/* One line, clipped. A hostname broken across two lines in a 288px rail reads
   as a rendering fault; the full value is on the element's title attribute. */
.site-meta {
  margin-top: 1px;
  font-size: 12px;
  color: var(--ink-3);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.site-platform {
  flex: none;
  font-size: 11px;
  color: var(--ink-3);
}

/* ── The channel strip ──
   A camera's identity is its channel number: that is what is printed on the
   recorder, what the installer wrote on the wall plate, and what the customer
   says on the phone. So the control is the number, in a dense strip, the way
   an NVR front panel and every VMS channel selector present it — not a row of
   "Cam 1" pills that fit four to a line. */
.camera-buttons {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}

.camera-buttons button {
  min-width: 30px;
  padding: 4px 6px;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  text-align: center;
}

/* ── Inline forms ── */

.add-site-form {
  display: grid;
  gap: 8px;
  margin-bottom: 12px;
  padding: 12px;
  border: 1px solid var(--line);
  border-radius: var(--r-control);
  background: var(--surface-2);
}

.add-site-form h3 {
  font-size: 13px;
  font-weight: 600;
}

.form-grid-two {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}

/* ============================================================================
   The wall
   ========================================================================= */

/* One line, under the bar. It reports what just happened and then stops. */
.status {
  min-height: 20px;
  margin-bottom: 10px;
  font-size: 12px;
  color: var(--ink-2);
}

.status.is-error {
  color: var(--fault);
}

/* ── The wall bar: positions in use, the layout, stop everything ── */
.wall-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin-bottom: 8px;
}

.wall-count {
  font-size: 12px;
  color: var(--ink-2);
}

/* In the bar, the status line takes the room between the count and the
   controls instead of a row of its own. */
.wall-bar .status {
  flex: 1 1 220px;
  min-height: 0;
  margin: 0;
}

.wall-layout {
  display: inline-flex;
}

.wall-layout-btn {
  min-width: 42px;
  padding: 4px 10px;
  font-size: 12px;
}

.wall-layout-btn + .wall-layout-btn {
  border-left: 0;
}

/* The chosen layout is the inverted surface, as a primary action is: colour
   stays reserved for state. */
.wall-layout-btn[aria-pressed="true"] {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--on-inverted);
}

.wall-layout-btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/* One position or the quad view every recorder shows; MAX_ACTIVE_PLAYERS is 4,
   so the wall never goes past it. The positions are laid out whether or not a
   camera is in them, so the wall does not reflow as cameras open and close.

   Sized by the height as well as the width. A 16:9 tile's height follows its
   width, so on a wide screen two rows ran past the bottom (1044px of wall in a
   947px Full HD window) and the second row needed scrolling — on a camera
   wall, the one thing that must be seen at once. The cap is the width at
   which the rows exactly fill the visible height: --wall-offset is everything
   above and below the grid (top bar, padding, the wall bar). A narrow screen
   never reaches it, so there the width still decides; the floor keeps a very
   short window from shrinking the tiles to nothing. */
.camera-grid {
  --wall-offset: 118px;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 12px;
  margin-inline: auto;
  max-width: max(480px, calc((100vh - var(--wall-offset)) * 16 / 9));
  max-width: max(480px, calc((100dvh - var(--wall-offset)) * 16 / 9));
}

.camera-grid[data-layout="4"] {
  grid-template-columns: repeat(2, minmax(0, 1fr));
  max-width: max(640px, calc((100vh - var(--wall-offset) - 12px) * 16 / 9 + 12px));
  max-width: max(640px, calc((100dvh - var(--wall-offset) - 12px) * 16 / 9 + 12px));
}

/* A free position: the same shape as a tile, so the layout is visible before
   anything plays in it. */
.wall-slot {
  display: grid;
  place-content: center;
  gap: 2px;
  aspect-ratio: 16 / 9;
  padding: 16px;
  border: 1px dashed var(--line-firm);
  border-radius: var(--r-tile);
  color: var(--ink-3);
  font-size: 13px;
  text-align: center;
}

.wall-slot b {
  color: var(--ink-2);
  font-weight: 500;
}

/* A tile is a monitor: square corners, no shadow, black inside so letterboxing
   on a 4:3 camera reads as the bezel rather than as a rendering fault. */
.player-card {
  border-radius: var(--r-tile);
  overflow: hidden;
  animation: lift 260ms ease-out both;
}

.player-wrap {
  position: relative;
  aspect-ratio: 16 / 9;
  background: #000;
  border: 1px solid var(--line-firm);
  border-radius: var(--r-tile);
  overflow: hidden;
}

.go2rtc-frame {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
}

/* ── The on-screen display ──
   The camera's name belongs on the picture, not in a card header above it:
   with four tiles open you read the name off the tile you are already looking
   at. It sits at the TOP of the frame, which is the one part go2rtc's player
   leaves alone — its native transport controls occupy the bottom, and an OSD
   there was legible only while the video was paused. The stop control shares
   this row on the right, under the same scrim. */
.player-osd {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  display: flex;
  align-items: center;
  gap: 7px;
  /* Room on the right for the stop button, which overlays this row. */
  padding: 7px 78px 20px 10px;
  background: linear-gradient(to bottom, rgba(0, 0, 0, 0.72), rgba(0, 0, 0, 0));
  pointer-events: none;
}

.player-title {
  font-size: 12px;
  font-weight: 500;
  color: #f2f4f6;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.9);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.player-live {
  flex: none;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--live);
  box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.45);
}

/* If the picture never started, the dot stops claiming it did. This is the one
   place the rule "colour means state" has to be true or the whole scheme is
   decoration: a green light over a black rectangle is worse than no light. */
.is-stalled .player-live {
  background: var(--hold);
}

/* Always visible, never hover-only: a tile whose stop control appears on hover
   cannot be stopped from a phone. It rides the same scrim as the name. */
.player-head {
  position: absolute;
  top: 6px;
  right: 6px;
}

.stop-btn {
  padding: 3px 8px;
  font-size: 11px;
  background: rgba(10, 12, 14, 0.6);
  border-color: rgba(255, 255, 255, 0.3);
  color: #f2f4f6;
  backdrop-filter: blur(3px);
}

.stop-btn:hover {
  background: var(--fault);
  border-color: var(--fault);
  color: #fff;
}

/* Filled by streams.js when the picture has not started. Amber, because it is
   a "not yet" rather than a failure — the check button beside it is what turns
   it into one answer or the other. */
.player-note {
  margin-top: 6px;
  padding: 6px 9px;
  border-left: 2px solid var(--hold);
  background: var(--surface-1);
  color: var(--ink-2);
  font-size: 12px;
  line-height: 1.5;
}

.probe-offer {
  margin-left: 4px;
  padding: 2px 7px;
  font-size: 11px;
}

/* ============================================================================
   Admin console

   Deliberately plainer than the wall. It is a back office: read a table, click
   a button, leave. Everything here inherits the tokens above, so it changed
   colour and typeface with the rest of the app and nothing in it is styled to
   attract the eye.
   ========================================================================= */

.admin-panel-wrap {
  padding: 16px;
}

.admin-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin: 12px 0 16px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--line);
}

.admin-tabs button {
  background: transparent;
  border-color: transparent;
  color: var(--ink-3);
}

.admin-tabs button:hover {
  background: var(--surface-2);
  border-color: transparent;
  color: var(--ink);
}

.admin-tabs .active-tab,
.admin-tabs .active-tab:hover {
  background: var(--surface-2);
  border-color: var(--line-firm);
  color: var(--ink);
}

.admin-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
}

.admin-table th {
  text-align: left;
  font-weight: 500;
  font-size: 12px;
  color: var(--ink-3);
  padding: 0 10px 7px 0;
  border-bottom: 1px solid var(--line);
}

.admin-table td {
  padding: 8px 10px 8px 0;
  border-bottom: 1px solid var(--line);
  vertical-align: middle;
}

.admin-table tr:last-child td {
  border-bottom: none;
}

.admin-table .row-muted td {
  color: var(--ink-3);
}

.row-actions {
  display: flex;
  gap: 5px;
  flex-wrap: wrap;
}

.row-actions button {
  padding: 3px 8px;
  font-size: 12px;
}

/* The ⋯ menu on a person's row. Absolutely placed under its button, right
   edges aligned; depth is surface value and a firm line, never a shadow. */
.row-menu {
  position: relative;
}

.row-menu > summary {
  display: inline-flex;
  align-items: center;
  padding: 3px 6px;
  border: 1px solid var(--line);
  border-radius: var(--r-control);
  cursor: pointer;
  list-style: none;
}

.row-menu > summary::-webkit-details-marker {
  display: none;
}

.row-menu > summary svg {
  fill: currentColor;
}

.row-menu[open] > summary {
  background: var(--surface-2);
  border-color: var(--line-firm);
  color: var(--ink);
}

.row-menu-panel {
  position: absolute;
  top: calc(100% + 4px);
  right: 0;
  z-index: 10;
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: max-content;
  padding: 4px;
  background: var(--surface-1);
  border: 1px solid var(--line-firm);
  border-radius: var(--r-control);
}

.row-menu-panel button {
  text-align: left;
}

.pill {
  display: inline-block;
  padding: 1px 6px;
  border: 1px solid var(--line-firm);
  border-radius: 2px;
  font-size: 11px;
  color: var(--ink-3);
}

/* Status, so it is allowed a colour. */
.pill-on {
  border-color: var(--live);
  color: var(--live);
}

.pill-off {
  border-color: var(--line-firm);
  color: var(--ink-3);
}

/* Needs the operator: an agreement unsigned, no cameras given. */
.pill-hold {
  border-color: var(--hold);
  color: var(--hold);
}

/* ── System tab ── */

.system-screen {
  display: grid;
  gap: 14px;
}

.system-screen .nvr-toolbar .ghost-btn {
  margin-left: auto;
}

.system-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: 10px;
}

.system-card {
  display: grid;
  gap: 2px;
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: var(--r-control);
  background: var(--surface-1);
}

.system-card-key {
  font-size: 12px;
  color: var(--ink-3);
}

.system-card-value {
  font-size: 16px;
  font-weight: 500;
  overflow-wrap: anywhere;
}

/* A card's colour is its state and nothing else: the version and the memory
   figure have none. */
.system-card.is-ok .system-card-value {
  color: var(--live);
}

.system-card.is-hold .system-card-value {
  color: var(--hold);
}

.system-card.is-fault {
  border-color: var(--fault);
}

.system-card.is-fault .system-card-value {
  color: var(--fault);
}

/* ── Customer screen ── */

.customer-screen {
  display: grid;
  grid-template-columns: 260px minmax(0, 1fr);
  gap: 24px;
  align-items: start;
}

/* ── The customer list ── */

.customer-list-pane {
  display: grid;
  gap: 8px;
  padding-right: 20px;
  border-right: 1px solid var(--line);
}

.customer-list {
  display: grid;
  max-height: 64vh;
  overflow-y: auto;
  border-top: 1px solid var(--line);
}

.customer-item {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 2px 8px;
  align-items: baseline;
  width: 100%;
  padding: 8px 8px;
  border: 0;
  border-bottom: 1px solid var(--line);
  border-radius: 0;
  background: transparent;
  color: var(--ink);
  text-align: left;
  cursor: pointer;
}

.customer-item:hover {
  background: var(--surface-2);
}

/* The selection is marked by surface value and a rule on the left edge, not by
   a colour: colour is reserved for state. */
.customer-item[aria-current="true"] {
  background: var(--surface-3);
  box-shadow: inset 2px 0 0 var(--ink);
}

.customer-item-name {
  overflow: hidden;
  font-weight: 500;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.customer-item.is-off .customer-item-name {
  color: var(--ink-3);
}

.customer-item-meta {
  grid-column: 1 / -1;
  font-size: 12px;
  color: var(--ink-3);
}

.customer-detail {
  display: grid;
  gap: 16px;
  min-width: 0;
}

/* ── What the customer still needs ── */

.customer-checklist {
  display: grid;
  gap: 6px;
  margin: 0;
  padding: 10px 12px;
  list-style: none;
  border: 1px solid var(--line);
  border-radius: var(--r-control);
  background: var(--surface-1);
  font-size: 13px;
}

.customer-checklist li {
  position: relative;
  padding-left: 22px;
}

/* Drawn from borders: Fira Sans has no check-mark glyph, and a typed ✓ would
   render as an empty box. */
.customer-checklist li::before {
  content: "";
  position: absolute;
  left: 4px;
}

.customer-checklist li.is-done::before {
  top: 1px;
  width: 5px;
  height: 10px;
  border-right: 2px solid var(--live);
  border-bottom: 2px solid var(--live);
  transform: rotate(45deg);
}

.customer-checklist li.is-todo::before {
  top: 5px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--hold);
}

.customer-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 12px;
  padding-bottom: 14px;
  border-bottom: 1px solid var(--line);
}

.customer-name {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 18px;
  font-weight: 600;
}

.customer-head-actions {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
}

.customer-cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 28px;
  align-items: start;
}

.customer-col-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 4px;
}

.customer-col-head h4 {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink-2);
}

.customer-col-head .toggle-add-form-btn {
  width: auto;
  margin-bottom: 0;
}

.customer-col .panel-caption {
  margin-bottom: 10px;
}

/* ── Dialogs over the console: giving access, adding or editing an NVR ── */

.admin-dialog {
  width: min(560px, calc(100vw - 32px));
  max-height: min(640px, calc(100vh - 32px));
  padding: 0;
  border: 1px solid var(--line-firm);
  border-radius: var(--r-control);
  background: var(--surface-1);
  color: var(--ink);
}

.admin-dialog::backdrop {
  background: rgba(0, 0, 0, 0.55);
}

.grant-dialog-body {
  display: grid;
  grid-template-rows: auto auto auto minmax(0, 1fr) auto;
  gap: 10px;
  max-height: inherit;
  padding: 14px;
}

.admin-dialog-head,
.admin-dialog-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}

.admin-dialog-head h3 {
  font-size: 15px;
}

.grant-filter {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--ink-2);
}

.grant-filter input {
  width: auto;
}

.grant-list {
  min-height: 120px;
  overflow-y: auto;
  border-top: 1px solid var(--line);
}

.grant-pick {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: 2px 10px;
  align-items: baseline;
  padding: 8px 4px;
  border-bottom: 1px solid var(--line);
  cursor: pointer;
}

.grant-pick:hover {
  background: var(--surface-2);
}

.grant-pick input {
  width: auto;
  margin: 0;
}

.grant-pick-meta {
  font-size: 12px;
  color: var(--ink-3);
}

.grant-pick-who {
  grid-column: 2 / -1;
  font-size: 12px;
  color: var(--ink-3);
}

/* ── NVR tab ── */

.nvr-screen {
  display: grid;
  gap: 10px;
}

.nvr-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
}

.nvr-toolbar input[type="search"] {
  flex: 1 1 260px;
  max-width: 360px;
}

.nvr-toolbar .primary-btn {
  margin-left: auto;
}

.nvr-filters {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

/* A chosen filter is the inverted surface, like the primary action: colour is
   kept for state. */
.nvr-filter[aria-pressed="true"] {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--on-inverted);
}

.nvr-address {
  color: var(--ink-2);
  overflow-wrap: anywhere;
}

/* A wide table scrolls inside its own box; the page never scrolls sideways. */
.table-scroll {
  overflow-x: auto;
}

/* On a phone, let it: an address broken at any character — "62.74.242" /
   ".246:5901" / "9" — is four lines a row and unreadable, while a row on one
   line with the rest a swipe away is the scroll box doing its job. */
@media (max-width: 900px) {
  .table-scroll .nvr-address {
    overflow-wrap: normal;
    white-space: nowrap;
  }

  .table-scroll .row-actions {
    flex-wrap: nowrap;
  }
}

.cell-muted {
  color: var(--ink-3);
}

.nvr-form {
  display: grid;
  gap: 10px;
  padding: 14px;
}

.nvr-form label {
  display: grid;
  gap: 4px;
  font-size: 13px;
  color: var(--ink-2);
}

/* ── Activity tab ── */

.activity-screen {
  display: grid;
  gap: 10px;
}

.activity-screen select {
  width: auto;
  min-width: 200px;
}

.activity-list {
  display: grid;
}

.activity-day {
  padding: 14px 0 4px;
  border-bottom: 1px solid var(--line);
  font-size: 12px;
  color: var(--ink-3);
}

.activity-row {
  display: grid;
  grid-template-columns: 56px minmax(0, 1fr) auto;
  gap: 12px;
  align-items: baseline;
  padding: 7px 0;
  border-bottom: 1px solid var(--line);
  font-size: 13px;
}

.activity-time,
.activity-customer {
  color: var(--ink-3);
  font-size: 12px;
}

/* A refused sign-in or a denied camera: the one thing on this page that may
   need a phone call, so the one thing with a colour. */
.activity-row.is-warn .activity-text {
  color: var(--fault);
}

#activityMore {
  justify-self: start;
}

/* ============================================================================
   Notice pages (/info, /gdpr)
   ========================================================================= */

.doc-page {
  background: var(--surface-0);
  color: var(--ink);
}

.doc-bar {
  border-bottom: 1px solid var(--line);
  background: var(--surface-1);
}

.doc-bar-inner {
  max-width: 680px;
  margin: 0 auto;
  padding: 13px 22px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 12px;
}

.doc-main {
  max-width: 680px;
  margin: 0 auto;
  padding: 34px 22px 64px;
}

.doc-main h1 {
  font-size: 26px;
  font-weight: 600;
}

.doc-updated {
  margin: 6px 0 26px;
  color: var(--ink-3);
  font-size: 12px;
}

.doc-main h2 {
  margin: 30px 0 8px;
  padding-top: 18px;
  border-top: 1px solid var(--line);
  font-size: 14px;
  font-weight: 600;
  color: var(--ink-2);
}

/* Prose, so it gets prose measure and leading rather than the 1.45 the
   interface uses. */
.doc-main p,
.doc-main li {
  font-size: 15px;
  line-height: 1.62;
  max-width: 68ch;
}

.doc-main ul {
  padding-left: 20px;
}

.doc-main li {
  margin-bottom: 7px;
}

.doc-main a {
  color: var(--ink);
  text-underline-offset: 3px;
}

.doc-main em {
  color: var(--ink-2);
}

.doc-lang[hidden] {
  display: none;
}

.doc-foot {
  margin-top: 40px;
  padding-top: 16px;
  border-top: 1px solid var(--line);
  font-size: 13px;
}

.doc-foot a {
  color: var(--ink-2);
}

/* ============================================================================
   Motion

   One movement, once: the thing that just arrived rises into place. There are
   no hover transitions on cards, no staggered section reveals and no entrance
   animation on anything the user did not ask for.
   ========================================================================= */

@keyframes lift {
  from {
    opacity: 0;
    transform: translateY(6px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* ============================================================================
   Narrow
   ========================================================================= */

@media (max-width: 900px) {
  .layout {
    grid-template-columns: minmax(0, 1fr);
  }

  /* The rail becomes a drawer over the wall, opened by "Πηγές". It used to be
     a section above the wall, which put a 234-recorder list between the top
     of the page and the first camera. Hidden, it is also taken out of the tab
     order (visibility), not just moved off screen. */
  .sites-panel {
    position: fixed;
    top: var(--bar);
    bottom: 0;
    left: 0;
    z-index: 25;
    width: min(320px, 88vw);
    height: auto;
    border-right: 1px solid var(--line-firm);
    transform: translateX(-100%);
    visibility: hidden;
    transition: transform 200ms ease, visibility 0s linear 200ms;
  }

  .layout.drawer-open .sites-panel {
    transform: none;
    visibility: visible;
    transition: transform 200ms ease;
  }

  .layout.drawer-open .drawer-scrim {
    display: block;
    position: fixed;
    inset: var(--bar) 0 0 0;
    z-index: 24;
    background: rgba(0, 0, 0, 0.5);
  }

  /* One row: "Πηγές", the wordmark, "Μενού". Everything else is in the menu. */
  .topbar {
    padding: 0 10px;
  }

  .topbar-inner {
    gap: 8px;
  }

  .topbar-phone-only {
    display: inline-flex;
  }

  #menuBtn {
    margin-left: auto;
  }

  .topbar-menu {
    display: none;
    position: absolute;
    top: var(--bar);
    right: 8px;
    left: 8px;
    z-index: 30;
    flex-direction: column;
    align-items: stretch;
    gap: 10px;
    padding: 12px;
    border: 1px solid var(--line-firm);
    border-radius: var(--r-control);
    background: var(--surface-1);
  }

  .topbar-menu .icon-btn-text {
    display: inline;
  }

  .topbar-menu.is-open {
    display: flex;
  }

  .topbar-menu .topbar-spacer {
    display: none;
  }

  .topbar-menu .identity-row {
    flex-wrap: wrap;
  }

  .view-as select {
    flex: 1;
    min-width: 0;
  }

  /* One column whatever the layout, and only the first free position: three
     empty boxes stacked on a phone are three screens of scrolling. */
  .camera-grid,
  .camera-grid[data-layout="4"] {
    grid-template-columns: minmax(0, 1fr);
    max-width: none;
  }

  .wall-slot ~ .wall-slot {
    display: none;
  }

  .wall-layout {
    display: none;
  }

  .form-grid-two {
    grid-template-columns: 1fr;
  }

  .customer-cols {
    gap: 22px;
  }

  .customer-screen {
    grid-template-columns: minmax(0, 1fr);
  }

  /* The brand is in the edit dialog; the address is what tells two sites apart. */
  .nvr-brand {
    display: none;
  }

  .nvr-toolbar .primary-btn {
    margin-left: 0;
  }

  .customer-list-pane {
    padding-right: 0;
    border-right: 0;
  }

  .customer-list {
    max-height: 240px;
  }

  .activity-row {
    grid-template-columns: 48px minmax(0, 1fr);
  }

  .activity-customer {
    grid-column: 2;
  }
}

@media (max-width: 430px) {
  .login-view {
    padding: 14px;
  }

  .login-card {
    padding: 20px 18px 16px;
  }

  .doc-main {
    padding: 24px 18px 48px;
  }
}
