/* GManage portal v2 — «Slate enterprise» αναβαθμισμένο: fluid centered layout, dark mode,
   πλουσιότερα components (KPI/donut/sparkline/timeline/tabs). Χωρίς frameworks/CDN.
   CSP: default-src 'self' → ΚΑΝΕΝΑ inline style πουθενά — δυναμικές τιμές ΜΟΝΟ μέσω
   class steps (w-0..w-100) ή SVG presentation attributes (stroke-dasharray, rect x/y/w/h). */

:root {
  /* Brand / accents */
  /* 10% darker than #0d9488 (2026-08-19). White-on-accent measured 3.74 and AA wants 4.5 for the
     button, chip and pager labels that sit on it. The step is small enough that the brand colour
     reads the same; the alternative - leaving white text at 3.74 - fails on exactly the controls
     people are meant to click. */
  --accent: #0c857a;
  --on-accent: #ffffff;   /* text painted ON the accent - see the dark override */
  --accent-hover: #0f766e;
  --accent-soft: #ccfbf1;
  --accent-2: #2563eb;

  /* Sidebar (σκούρο και στα δύο θέματα — brand identity) */
  --sidebar-bg: #1c2733;
  --sidebar-bg-2: #16202a;
  --sidebar-text: #b7c4d1;
  /* 4.34 on the sidebar's #1c2733 before 2026-08-19 - just under AA. #808e9c clears it (4.52). */
  --sidebar-muted: #808e9c;

  /* Surfaces / text (light) */
  --content-bg: #eef1f5;
  --card-bg: #ffffff;
  --card-bg-2: #f6f8fa;
  --border: #d7dde5;
  --border-soft: #e8edf2;
  --text: #1a2632;
  --text-2: #5b6b7c;
  /* #8296a9 until 2026-08-19: measured 3.05 on the card and 2.69 on the page background, where AA
     wants 4.5 for text this size - and this token paints the SMALL text (.muted, .cell-sub, counts,
     chart axes), i.e. exactly the type that needs the help. #5c7083 is the smallest step along the
     same grey that clears it: 5.12 on white, 4.52 on --content-bg. Still visibly quieter than
     --text (#1a2632), which is the whole job of the token. */
  --text-3: #5c7083;
  --hover-row: #f4f7f9;

  /* Semantic */
  --danger: #dc2626;
  --danger-bg: #fee2e2;
  --danger-tx: #991b1b;
  --info-bg: #e0f2fe;
  --info-text: #075985;
  --warn-bg: #fef3c7;
  --warn-tx: #92400e;

  /* Badges */
  --badge-ok-bg: #ccfbf1;   --badge-ok-tx: #0f766e;
  --badge-bad-bg: #fee2e2;  --badge-bad-tx: #991b1b;
  --badge-wait-bg: #fef3c7; --badge-wait-tx: #92400e;
  --badge-run-bg: #dbeafe;  --badge-run-tx: #1e40af;
  --badge-mut-bg: #e5e7eb;  --badge-mut-tx: #4b5563;

  /* Terminal output (σκούρο και στα δύο) */
  --output-bg: #0e1117;
  --output-tx: #d5dae1;

  --shadow: 0 1px 2px rgba(16, 24, 40, 0.06), 0 1px 3px rgba(16, 24, 40, 0.08);
  --shadow-lg: 0 4px 14px rgba(16, 24, 40, 0.12);
  --radius: 10px;
  --radius-lg: 14px;
  --font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, "Cascadia Code", Consolas, "Courier New", monospace;
}

/* ===== Dark theme (theme.js βάζει data-theme πριν το render· toggle στο topbar) ===== */
html[data-theme="dark"] {
  --accent: #14b8a6;
  /* Dark keeps the vivid teal (user decision 2026-08-19) and flips the LABEL instead: white on
     #14b8a6 measures 2.49, near-black measures 7.37. One token, so every control that sits on the
     accent - buttons, active chip, active pager - moves together and cannot drift apart. */
  --on-accent: #0f151d;
  --accent-hover: #0d9488;
  --accent-soft: rgba(20, 184, 166, 0.16);

  --sidebar-bg: #10161e;
  --sidebar-bg-2: #0b1118;
  --sidebar-text: #aebccb;
  /* 3.85 on the dark sidebar's #10161e; #708196 clears it (4.56). */
  --sidebar-muted: #708196;

  --content-bg: #0f151d;
  --card-bg: #161f2a;
  --card-bg-2: #1b2532;
  --border: #263241;
  --border-soft: #1f2a37;
  --text: #dbe4ee;
  --text-2: #97a7ba;
  /* Dark's own muted grey: 3.61 on the card, 3.98 on the page. #74879c is the smallest step the
     other way (4.50 / 4.97) - lighter here, darker in light mode, because "quiet" is a direction,
     not a colour. */
  --text-3: #74879c;
  --hover-row: #1a2432;

  --danger: #f87171;
  --danger-bg: rgba(248, 113, 113, 0.14);
  --danger-tx: #fca5a5;
  --info-bg: rgba(56, 189, 248, 0.14);
  --info-text: #7dd3fc;
  --warn-bg: rgba(251, 191, 36, 0.14);
  --warn-tx: #fcd34d;

  --badge-ok-bg: rgba(20, 184, 166, 0.16);  --badge-ok-tx: #5eead4;
  --badge-bad-bg: rgba(248, 113, 113, 0.14); --badge-bad-tx: #fca5a5;
  --badge-wait-bg: rgba(251, 191, 36, 0.14); --badge-wait-tx: #fcd34d;
  --badge-run-bg: rgba(96, 165, 250, 0.14);  --badge-run-tx: #93c5fd;
  --badge-mut-bg: rgba(148, 163, 184, 0.14); --badge-mut-tx: #b0becf;

  --shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
  --shadow-lg: 0 6px 18px rgba(0, 0, 0, 0.5);
}

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body { font-family: var(--font); color: var(--text); font-size: 15px; line-height: 1.5; }

.ic { width: 18px; height: 18px; flex-shrink: 0; }

/* ===== App layout ===== */
body.app { display: flex; min-height: 100vh; background: var(--content-bg); }

.sidebar {
  width: 232px; flex-shrink: 0; background: var(--sidebar-bg); color: var(--sidebar-text);
  display: flex; flex-direction: column; padding: 18px 12px; position: sticky; top: 0; height: 100vh;
}
.brand {
  display: flex; align-items: center; gap: 9px; padding: 4px 10px 16px; color: #f1f5f9;
  font-size: 17px; font-weight: 600; letter-spacing: 0.2px;
}
.brand .ic { width: 22px; height: 22px; color: #2dd4bf; }

.nav { display: flex; flex-direction: column; gap: 3px; flex: 1; }
.nav-item {
  display: flex; align-items: center; gap: 10px; padding: 9px 10px; border-radius: 7px;
  color: var(--sidebar-text); text-decoration: none; font-size: 13.5px; transition: background 0.15s, color 0.15s;
}
.nav-item:hover { background: var(--sidebar-bg-2); color: #e2e8f0; }
.nav-item.active {
  background: linear-gradient(135deg, #0d9488 0%, #0f766e 100%);
  color: var(--on-accent); box-shadow: 0 2px 8px rgba(13, 148, 136, 0.35);
}
.nav-item.disabled { color: var(--sidebar-muted); cursor: default; }
.nav-item.disabled:hover { background: transparent; color: var(--sidebar-muted); }
.nav-item em { margin-left: auto; font-style: normal; font-size: 10.5px; opacity: 0.75; border: 1px solid currentColor; border-radius: 99px; padding: 1px 7px; }

.sidebar-foot { padding: 10px 0 0; font-size: 11.5px; color: var(--sidebar-muted); }
/* TV mode, moved here from the dashboard (2026-08-19). Styled off `.nav-item` rather than as a
   button: it sits in the navigation column, so it should read as one more way to go somewhere -
   quieter than the nav entries above it, since it leaves the app shell behind. */
.sidebar-tv { display: flex; align-items: center; gap: 10px; padding: 9px 10px; border-radius: 7px;
              color: var(--sidebar-muted); text-decoration: none; font-size: 12.5px; transition: background 0.15s, color 0.15s; }
.sidebar-tv:hover { background: var(--sidebar-bg-2); color: #e2e8f0; }
.sidebar-tv .ic { width: 18px; height: 18px; flex-shrink: 0; }

.main { flex: 1; display: flex; flex-direction: column; min-width: 0; }

.topbar {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  padding: 12px 26px; background: var(--card-bg); border-bottom: 1px solid var(--border);
  position: sticky; top: 0; z-index: 20;
}
.topbar h1 { margin: 0; font-size: 17px; font-weight: 600; white-space: nowrap; }
.topbar-right { display: flex; align-items: center; gap: 12px; }
/* Auto-refresh: the on/off button and the interval read as ONE control, so they sit in one box with
   a tighter gap than the topbar's 12px - otherwise the select looks like an unrelated dropdown that
   happens to be next to a button. */
.refresh-ctl { display: flex; align-items: center; gap: 6px; }
.refresh-secs { height: 32px; padding: 0 6px; font-size: 12.5px; font-family: var(--mono); color: var(--text-2);
                background: var(--card-bg); border: 1px solid var(--border); border-radius: 7px; cursor: pointer; }
.refresh-secs:hover { border-color: var(--accent); color: var(--text); }
.refresh-secs:focus { outline: none; border-color: var(--accent); }

/* Global search (topbar) */
.top-search { display: flex; align-items: center; flex: 1; max-width: 420px; margin: 0 8px; position: relative; }
.top-search .ic { position: absolute; left: 10px; width: 15px; height: 15px; color: var(--text-3); pointer-events: none; }
.top-search input {
  width: 100%; padding: 7px 12px 7px 32px; font-size: 13px; font-family: var(--font);
  border: 1px solid var(--border); border-radius: 99px; background: var(--content-bg); color: var(--text);
  transition: border-color 0.15s, box-shadow 0.15s, background 0.15s;
}
.top-search input:focus { outline: none; border-color: var(--accent); background: var(--card-bg); box-shadow: 0 0 0 3px rgba(13, 148, 136, 0.15); }

.lang-toggle {
  color: var(--text-2); text-decoration: none; font-size: 12.5px; font-weight: 600;
  border: 1px solid var(--border); border-radius: 6px; padding: 4px 9px;
}
.lang-toggle:hover { border-color: var(--accent); color: var(--accent); }

.user-chip { display: flex; align-items: center; gap: 9px; text-decoration: none; color: inherit; padding: 4px 8px; border-radius: 8px; transition: background 0.15s; }
a.user-chip:hover { background: var(--content-bg); }
.avatar {
  width: 32px; height: 32px; border-radius: 50%; background: linear-gradient(135deg, #14b8a6, #0d9488); color: #fff;
  display: flex; align-items: center; justify-content: center; font-size: 12.5px; font-weight: 600;
}
.user-meta { display: flex; flex-direction: column; line-height: 1.25; }
.user-meta strong { font-size: 13px; font-weight: 600; }
.user-meta span { font-size: 11.5px; color: var(--text-3); }

.logout-form { margin: 0; display: flex; }
.icon-btn {
  background: none; border: 1px solid var(--border); border-radius: 6px; padding: 6px;
  color: var(--text-2); cursor: pointer; display: flex; transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.icon-btn:hover { color: var(--accent); border-color: var(--accent); }
.icon-btn.danger-hover:hover { color: var(--danger); border-color: var(--danger); }
.icon-btn.is-on { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); }
/* Θέμα: δείξε ήλιο σε dark, φεγγάρι σε light */
.ic-sun { display: none; }
html[data-theme="dark"] .ic-sun { display: block; }
html[data-theme="dark"] .ic-moon { display: none; }

/* Fluid centered content — γεμίζει την οθόνη, κεντραρισμένο (τέλος το «κενό δεξιά») */
.content { padding: 26px 30px 40px; max-width: 1560px; width: 100%; margin: 0 auto; }

/* ===== Grid utilities (12-col) ===== */
.grid { display: grid; grid-template-columns: repeat(12, 1fr); gap: 16px; margin-bottom: 16px; }
.grid .card { margin: 0; }
.grid > .card + .card { margin-top: 0; }  /* το γενικό `.card + .card {margin-top:16px}` (πιο κάτω, ίδια specificity) leak-άρει μέσα στο grid → τα non-first cards ξεκινούσαν 16px χαμηλότερα (misaligned tops)· εδώ το μηδενίζουμε — το spacing στο grid το δίνει το gap. ⚠ ΜΟΝΟ direct grid children (`>`): stacked cards ΜΕΣΑ σε column-wrapper (π.χ. dashboard `.g-4` → Τοποθεσίες + Νέα μηχανήματα) πρέπει να κρατούν το 16px spacing — το descendant `.grid .card+.card` τα κολλούσε (#076→#077). */
.g-3 { grid-column: span 3; } .g-4 { grid-column: span 4; } .g-5 { grid-column: span 5; }
.g-6 { grid-column: span 6; } .g-7 { grid-column: span 7; } .g-8 { grid-column: span 8; }
.g-9 { grid-column: span 9; } .g-12 { grid-column: span 12; }
/* content-height cards (top-aligned) αντί stretch — ομοιόμορφα κουτάκια χωρίς κενό, & το μικρό card
   δίπλα σε ψηλό (π.χ. Job stats δίπλα σε Timeline) δεν τεντώνεται άσκοπα. */
.grid.g-top { align-items: start; }

/* Column wrapper inside the grid whose bottom must line up with the neighbouring column.
   The grid stretches the WRAPPER (align-items:stretch is the default), but a plain .card inside it
   keeps its content height — that is exactly the ragged gap under the shorter column (dashboard:
   one "Recent jobs" card facing a two-card right column). Flex column + the LAST card taking the
   slack lines both bottoms up, whichever side happens to be taller.
   ⚠ NO `gap` here: the 16px between stacked cards already comes from `.card + .card` (see #077 on
   line 187 — the descendant rule is deliberately NOT zeroed inside column wrappers). Adding gap
   would double it. */
.col-stack { display: flex; flex-direction: column; }
/* The slack is shared by ALL cards of the column, not dumped on the last one: with `:last-child`
   alone the shorter column ended level but its bottom card grew to 641px around ~5 rows — one big
   void instead of a ragged edge, which is not an improvement. `flex:1 1 auto` splits the extra
   space evenly, so each card takes a little and none of them looks empty. */
.col-stack > .card { flex: 1 1 auto; }

/* Dashboard bottom row as a REAL 2x2 (2026-08-19). `.col-stack` lines up the two BOTTOMS, which is
   all it ever promised - but the edge between the stacked cards of one column has no reason to meet
   the edge of the other, and it did not: "Locations" ended a good 80px above "Recent jobs", so the
   row read as two unrelated columns. Grid rows solve exactly this by construction (cells of one row
   share its height), so the four cards become direct grid children with EXPLICIT placement.
   Explicit, not auto-flow, for one reason: the DOM order stays jobs -> logs -> locations -> new
   machines, which is the order that must survive when everything collapses to one column below. */
.grid-2x2 > .g-8 { grid-column: 1 / span 8; }
.grid-2x2 > .g-4 { grid-column: 9 / span 4; }
.grid-2x2 > .r1  { grid-row: 1; }
.grid-2x2 > .r2  { grid-row: 2; }

/* #077: cards ίσου ύψους (default stretch) με ΚΑΤΑΝΕΜΗΜΕΝΟ περιεχόμενο ώστε να μη μένει κενό κάτω
   (καρτέλα PC: Ταυτότητα/Υλικό/Agent + Job stats/Timeline). Ο χρήστης: «δεν θέλω κενά όπου γίνεται». */
.cards-eq > .card { display: flex; flex-direction: column; }
/* kv λίστα που γεμίζει το ύψος: οι σειρές κεντράρονται κάθετα (κρατούν τον ρυθμό τους) → το slack
   μοιράζεται ισορροπημένα πάνω/κάτω αντί για κενό κάτω· όχι space-between (θα σκόρπιζε τις σειρές/borders). */
.kv-fill { display: flex; flex-direction: column; flex: 1 1 auto; justify-content: center; }
/* Job stats: donut + κατανομή status, κεντραρισμένα κάθετα → ισορροπημένο, όχι μάζεμα πάνω με κενό κάτω δίπλα στο ψηλό Timeline */
.stat-fill { display: flex; flex-direction: column; flex: 1 1 auto; justify-content: center; gap: 12px; }
.stat-break { display: flex; flex-direction: column; gap: 8px; width: 100%; }
.sb-row { display: grid; grid-template-columns: 104px 1fr 26px; align-items: center; gap: 9px; font-size: 12.5px; }
.sb-bar { height: 7px; background: var(--content-bg); border-radius: 4px; overflow: hidden; }
.sb-fill { display: block; height: 100%; border-radius: 4px; }
.sb-ok { background: var(--accent); } .sb-bad { background: var(--danger); }
.sb-wait { background: #f59e0b; } .sb-run { background: #3b82f6; } .sb-muted { background: var(--text-3); }
.sb-n { text-align: right; font-variant-numeric: tabular-nums; color: var(--text-2); }
.stat-total { text-align: center; font-size: 12.5px; color: var(--text-2); }
.cards-eq .tl-scroll { flex: 1 1 auto; }  /* Timeline γεμίζει το ύψος της κάρτας (cap παραμένει max-height) */

/* ===== Cards / alerts ===== */
.card {
  background: var(--card-bg); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 20px 22px; box-shadow: var(--shadow);
}
.card + .card { margin-top: 16px; }
/* Whole-pixel line-height, same reason as .card h3: 15 x 1.5 = 22.5 leaves everything below the
   heading on a half pixel, and small text there renders blurry at devicePixelRatio 1. */
.card h2 { margin: 0 0 6px; font-size: 15px; line-height: 22px; font-weight: 600; display: flex; align-items: center; gap: 8px; }
.card h2 .ic { width: 16px; height: 16px; color: var(--text-3); }
.card p { margin: 6px 0 0; color: var(--text-2); font-size: 14px; }
/* Count beside a card title (how many rows it is showing). One step down from the heading and muted,
   so it annotates the title instead of competing with it. It sits before `.card-link`, which keeps
   its `margin-left:auto` and stays pinned right. */
.card h2 .h2-count { font-size: 12.5px; font-weight: 600; color: var(--text-3); }
.card-link { font-size: 12.5px; margin-left: auto; color: var(--accent); text-decoration: none; font-weight: 500; }
.card-link:hover { text-decoration: underline; }

.alert { border-radius: var(--radius); padding: 11px 14px; font-size: 13.5px; margin-bottom: 14px; }
.alert-error { background: var(--danger-bg); color: var(--danger-tx); }
.alert-info { background: var(--info-bg); color: var(--info-text); }
.alert-warn { background: var(--warn-bg); color: var(--warn-tx); }

/* I29 version-policy notice: icon + text on one baseline, wraps as one block. */
.vpol { display: flex; align-items: flex-start; gap: 9px; }
.vpol .ic { width: 15px; height: 15px; flex: 0 0 auto; margin-top: 1px; }
.vpol a { color: inherit; text-decoration: underline; white-space: nowrap; }

/* ===== Forms ===== */
label { display: block; font-size: 12.5px; font-weight: 600; color: var(--text-2); margin: 14px 0 5px; }
.hint { font-size: 12px; color: var(--text-3); margin-top: 4px; }

input[type="text"], input[type="email"], input[type="password"] {
  width: 100%; padding: 9px 12px; font-size: 14px; font-family: var(--font);
  border: 1px solid var(--border); border-radius: 6px; background: var(--card-bg); color: var(--text);
  transition: border-color 0.15s, box-shadow 0.15s;
}
input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px rgba(13, 148, 136, 0.15); }

.btn {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  width: fit-content; margin-left: auto;
  background: var(--accent); color: var(--on-accent); border: none; border-radius: 6px;
  padding: 9px 16px; font-size: 13.5px; font-weight: 600; font-family: var(--font);
  cursor: pointer; transition: background 0.15s; margin-top: 18px;
}
.btn:hover { background: var(--accent-hover); }
/* Στις auth κάρτες (login/enroll) το primary button μένει full-width — εκεί είναι το σωστό pattern·
   στις app σελίδες το width:100% έκανε τα Register/Enable/Save να απλώνονται σε όλη την κάρτα,
   και το δεξί align (margin-left:auto) τα κουμπώνει στη γωνία δράσης της κάρτας. */
.auth-card .btn { width: 100%; padding: 10px 18px; font-size: 14px; }

.code-input { font-family: var(--mono); font-size: 22px !important; letter-spacing: 8px; text-align: center; }

.setup-key {
  font-family: var(--mono); font-size: 15px; letter-spacing: 1.5px; background: var(--content-bg);
  border: 1px dashed var(--border); border-radius: 6px; padding: 12px 14px; text-align: center;
  word-break: break-all; user-select: all; margin: 8px 0; color: var(--text);
}
.enroll-label { font-size: 12.5px; color: var(--text-2); margin: 14px 0 6px; }
.enroll-orkey { margin-top: 18px; }
.qr-box { display: flex; justify-content: center; padding: 12px; background: #fff; border: 1px solid var(--border); border-radius: 8px; }
.qr-box img { display: block; image-rendering: pixelated; }
.kv { font-size: 13px; color: var(--text-2); margin: 4px 0; }
.kv b { color: var(--text); }

/* ===== Auth (login) layout ===== */
body.auth {
  background: radial-gradient(1100px 500px at 20% -10%, rgba(13, 148, 136, 0.25), transparent 60%), var(--sidebar-bg);
  min-height: 100vh; display: flex; align-items: center; justify-content: center;
}
.auth-wrap { width: 100%; max-width: 400px; padding: 24px 16px; }
.auth-brand {
  display: flex; align-items: baseline; gap: 9px; color: #f1f5f9; justify-content: center;
  margin-bottom: 18px; font-size: 21px; font-weight: 600;
}
.auth-brand .ic { width: 24px; height: 24px; color: #2dd4bf; align-self: center; }
.auth-brand em { font-style: normal; font-weight: 400; font-size: 12.5px; color: var(--sidebar-muted); }
.auth-card { background: var(--card-bg); border-radius: var(--radius-lg); padding: 26px 28px 28px; box-shadow: var(--shadow-lg); }
.auth-card h1 { margin: 0 0 4px; font-size: 17px; }
.auth-card .sub { margin: 0 0 8px; font-size: 13.5px; color: var(--text-2); }
.auth-foot { text-align: center; margin-top: 14px; }
.auth-foot a { color: var(--sidebar-muted); font-size: 12.5px; text-decoration: none; }
.auth-foot a:hover { color: #e2e8f0; }

/* ===== Distribution bars (w-0..w-100 — CSP: όχι inline width) ===== */
.w-0 { width: 0%; } .w-5 { width: 5%; } .w-10 { width: 10%; } .w-15 { width: 15%; }
.w-20 { width: 20%; } .w-25 { width: 25%; } .w-30 { width: 30%; } .w-35 { width: 35%; }
.w-40 { width: 40%; } .w-45 { width: 45%; } .w-50 { width: 50%; } .w-55 { width: 55%; }
.w-60 { width: 60%; } .w-65 { width: 65%; } .w-70 { width: 70%; } .w-75 { width: 75%; }
.w-80 { width: 80%; } .w-85 { width: 85%; } .w-90 { width: 90%; } .w-95 { width: 95%; }
.w-100 { width: 100%; }

/* ===== KPI cards v2 (icon + value + sub) — τα παλιά kpi markup δουλεύουν ως έχουν ===== */
.kpi-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 14px; margin-bottom: 16px; }
.kpi {
  background: var(--card-bg); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 14px 16px; box-shadow: var(--shadow); position: relative; overflow: hidden;
  display: flex; flex-direction: column; gap: 4px; min-width: 0;
}
a.kpi { text-decoration: none; color: inherit; transition: border-color 0.15s, transform 0.1s; }
a.kpi:hover { border-color: var(--accent); transform: translateY(-1px); }
.kpi-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.kpi-label { font-size: 12px; color: var(--text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.kpi-ico { color: var(--text-3); display: flex; }
.kpi-ico .ic { width: 16px; height: 16px; }
.kpi-value { font-size: 27px; font-weight: 650; line-height: 1.1; font-variant-numeric: tabular-nums; }
.kpi-sub { font-size: 11.5px; color: var(--text-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.kpi-ok .kpi-value, .kpi-ok .kpi-ico { color: var(--accent); }
.kpi-bad .kpi-value, .kpi-bad .kpi-ico { color: var(--danger); }
.kpi-warn .kpi-value, .kpi-warn .kpi-ico { color: var(--warn-tx); }

/* ===== Two-column helper (παλιό layout — μένει λειτουργικό) ===== */
.col2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.col2 .card + .card { margin-top: 0; }

/* ===== Bar list (κατανομές) ===== */
.barlist { display: flex; flex-direction: column; gap: 9px; margin-top: 12px; }
.bar-row { display: grid; grid-template-columns: 130px 1fr 40px; align-items: center; gap: 10px; font-size: 13px; }
.bar-label { color: var(--text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.bar-track { background: var(--content-bg); border-radius: 99px; height: 8px; overflow: hidden; }
.bar-fill { display: block; height: 100%; background: linear-gradient(90deg, var(--accent), #2dd4bf); border-radius: 99px; }
.bar-count { text-align: right; font-family: var(--mono); font-size: 12px; color: var(--text); }
.bar-row.is-latest .bar-label { color: var(--accent); font-weight: 600; }

/* ===== Donut (SVG — stroke-dasharray ως attribute, CSP-safe) ===== */
.donut-wrap { display: flex; flex-direction: column; align-items: center; gap: 6px; min-width: 0; }
.donut { width: 96px; height: 96px; }
.donut-bg { fill: none; stroke: var(--content-bg); stroke-width: 4.4; }
.donut-val { fill: none; stroke-width: 4.4; stroke-linecap: round; transform: rotate(-90deg); transform-origin: 50% 50%; }
.donut-ok .donut-val { stroke: var(--accent); }
.donut-bad .donut-val { stroke: var(--danger); }
.donut-warn .donut-val { stroke: #f59e0b; }
.donut-num { font-size: 11px; font-weight: 650; fill: var(--text); font-family: var(--font); }
.donut-cap { font-size: 12px; color: var(--text-2); text-align: center; }
.donut-foot { text-align: center; margin-top: 6px; }
.donut-row { display: flex; justify-content: space-around; align-items: flex-start; gap: 10px; padding: 8px 0 2px; flex-wrap: wrap; }

/* ===== Sparkbars (activity chart, SVG rects — CSP-safe) ===== */
.spark { width: 100%; height: 120px; display: block; margin-top: 10px; }
.spark-bar { fill: var(--accent); opacity: 0.85; rx: 1.4; }
.spark-bar:hover { opacity: 1; }
.spark-bar.is-zero { fill: var(--border); opacity: 0.6; }
.spark-x { display: flex; justify-content: space-between; font-size: 11px; color: var(--text-3); margin-top: 4px; }
.legend { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; }

/* ===== Area chart (διαθεσιμότητα στόλου) — I30 =====
   ONE height, two consumers: the plot and the Y-axis column next to it. If they ever disagree every
   number on the axis points at the wrong gridline, so neither hard-codes it — both read --chart-h. */
.chart { --chart-h: 190px; --chart-lh: 11px; position: relative; display: grid; grid-template-columns: 40px minmax(0, 1fr); column-gap: 8px; margin-top: 10px; }
/* +11px of height and -5.5px of offset = each label CENTERED on its gridline (half a line above the
   first, half below the last). Without it `space-between` hangs the top label half off the plot. */
.chart-y { grid-column: 1; grid-row: 1; height: calc(var(--chart-h) + var(--chart-lh)); margin-top: calc(var(--chart-lh) / -2);
           display: flex; flex-direction: column; justify-content: space-between; align-items: flex-end;
           font-size: var(--chart-lh); line-height: var(--chart-lh); color: var(--text-3); font-family: var(--mono); }
.chart-svg { grid-column: 2; grid-row: 1; min-width: 0; }
.chart-x { grid-column: 2; grid-row: 2; display: flex; justify-content: space-between; font-size: var(--chart-lh); line-height: var(--chart-lh); color: var(--text-3); margin-top: 6px; }
.chart-ranges { margin-top: 2px; }
.area { width: 100%; height: var(--chart-h); display: block; overflow: visible; }
.area-grid { stroke: var(--border); stroke-width: 1; vector-effect: non-scaling-stroke; }
.area-base { stroke: var(--text-3); opacity: 0.5; }
.area-fill { fill: var(--accent); opacity: 0.14; }
.area-band { fill: var(--accent); opacity: 0.10; }   /* min–max inside a bucket: the dip an average would hide */
.area-line { fill: none; stroke: var(--accent); stroke-width: 1.8; vector-effect: non-scaling-stroke; }
.area-total { fill: none; stroke: var(--text-3); stroke-width: 1.2; stroke-dasharray: 4 3; opacity: 0.7; vector-effect: non-scaling-stroke; }
/* The tooltip carrier. `fill: transparent` and NOT `fill: none`: none is not hit-testable, so the
   <title> would never open — the column would be invisible in both senses. */
.area-hit { fill: transparent; }
.area-hit:hover { fill: var(--text); opacity: 0.07; }

.chart-legend { display: flex; gap: 16px; flex-wrap: wrap; margin-top: 10px; font-size: 12px; color: var(--text-2); }
.cl::before { content: ''; display: inline-block; width: 14px; height: 3px; border-radius: 2px; margin-right: 6px; vertical-align: middle; background: var(--accent); }
.cl-band::before { height: 10px; opacity: 0.3; }
.cl-total::before { background: none; height: 0; border-top: 2px dashed var(--text-3); }

/* The summary numbers under the chart (average / lowest / highest / now). */
.cstats { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 14px; }
.cstat { flex: 1 1 130px; border: 1px solid var(--border); border-radius: 8px; padding: 8px 11px; background: var(--content-bg); }
.cstat b { display: block; font-size: 19px; font-weight: 650; color: var(--text); font-family: var(--mono); }
.cstat span { display: block; font-size: 11.5px; color: var(--text-3); margin-top: 2px; }
.cstat-ok b { color: var(--accent); }
/* Four tiles inside a 4-col card: the default 130px basis fits three per line, so the fourth wraps
   alone and the block reads as 3+1. A 45% basis forces an even 2x2. Scoped to the modifier so the
   full-width counter cards (lifecycle, accounts) keep flowing as many tiles per line as they fit. */
.cstats-2 .cstat { flex: 1 1 45%; }
.cstat-bad b { color: var(--danger); }
.chart-note { font-size: 11.5px; margin-top: 8px; }

/* Hover tooltip (portal.js takes over the SVG <title>). Positioned through CSSOM, never an inline
   style attribute - style-src 'self' blocks the attribute, not el.style.* (same route as spaces.js). */
.chart-tip { position: absolute; left: 0; top: 0; z-index: 6; pointer-events: none; opacity: 0;
             /* --content-bg, NOT --card-bg: the tooltip floats over a card, and in BOTH themes
                card-bg is the exact colour of what it floats over - a box that is invisible except
                for its border. Same surface the .cstat tiles use, so it reads as one family. */
             transition: opacity .1s ease-out; background: var(--content-bg); border: 1px solid var(--border);
             border-radius: 9px; padding: 8px 11px; box-shadow: 0 6px 20px rgba(0,0,0,.18); white-space: nowrap; }
.chart-tip.on { opacity: 1; }
.ct-when { font-size: 11.5px; color: var(--text-3); }
.ct-main { font-size: 14.5px; font-weight: 650; color: var(--text); font-family: var(--mono); margin-top: 2px; }
.ct-sub  { font-size: 11.5px; color: var(--text-2); font-family: var(--mono); margin-top: 2px; }
.donut-sub { font-size: 13px; font-weight: 650; color: var(--text); font-family: var(--mono); }

/* ===== Χάρτης τοποθεσιών (world + zoom/pan) ===== */
.map-card { position: relative; }
.map-wrap { position: relative; }
/* A map is not a document: dragging it means pan, never "select these letters". Without this the
   browser turns every drag into a text selection and the site names light up blue mid-pan.
   On the SVG, NOT on .map-pin: it then covers the pin labels, the pin numbers and anything textual
   added to a map later - .map-label (country names) already carried its own copy, which is exactly
   why country names never highlighted while site names did. */
/* No .map-gr any more: every country is drawn the same (user decision 2026-08-11). Greece only ever
   had its own rule because it was the single 10m country on a 110m world. */
/* Internal borders at the SAME weight and colour as the coastline, so every country is outlined
   identically - which is what Greece already looked like, and is how political maps are drawn.
   (An earlier version made these fainter "so the map does not read as a net"; the uniform look was
   the explicit choice, 2026-08-11.) */
/* Country names. pointer-events:none is not cosmetic: without it a label swallows the click that
   was meant to pan the map, or to place a site (R235). */
/* ⚠ Everything here is in WORLD UNITS, not pixels: these live inside the viewBox. One unit is
   ~31 km, the initial Greece view is ~44 units across, so 0.55 renders around 11 px. map-zoom.js
   recomputes it for whatever initial view a page actually uses; this value is the fallback for
   the wallboard, which draws a static map and never loads that script. */
html[data-theme="dark"] .map-fill { fill: #1b2532; }
html[data-theme="dark"] .map-coast { stroke: #3a4a5d; }
html[data-theme="dark"] .map-border { stroke: #3a4a5d; }   /* = .map-coast: identical treatment */
/* The halo must match the land, not the card, or the text sits in a light box on a dark map. */
html[data-theme="dark"] .map-label { fill: #8b9bb0; stroke: #1b2532; }
/* One custom property per tone, so the dot and the auto-geo variant swap fill/stroke without
   repeating four colours twice. Custom properties inherit from the <a> into the shapes. */
/* SOLID, not translucent: the old 45% fills turned a cluster of sites into one smear. The ring is
   the card colour, so two dots that touch still read as two. */
/* The ring is non-scaling, so it is a fixed pixel width against a dot that got ~30% smaller
   (gm_map_pin_radius, 2026-08-12). At 2px it was half the radius of a single-machine dot - that
   reads as a donut, not as a point. 1.5 keeps two touching dots separable without becoming the
   thing you see. */
.map-legend { display: flex; gap: 14px; margin-top: 10px; font-size: 12px; color: var(--text-2); flex-wrap: wrap; }
.map-legend .dot { margin-right: 4px; }
.dot-warn { background: #f59e0b; }

/* ===== Κάτοψη (floorplan) ===== */
.fp-wrap { display: grid; grid-template-columns: 1fr 280px; gap: 16px; align-items: start; }
.fp-stage-card { overflow: visible; }
.fp-stage { position: relative; user-select: none; }
.fp-stage.placing { cursor: crosshair; }
.fp-img { display: block; width: 100%; height: auto; border-radius: var(--radius); }
/* Χώρος ΧΩΡΙΣ εικόνα υποβάθρου: κενός καμβάς με πλέγμα (η κάτοψη είναι προαιρετική). */
.fp-canvas {
  width: 100%; aspect-ratio: 16 / 10; border-radius: var(--radius);
  background:
    repeating-linear-gradient(0deg, transparent, transparent 39px, var(--border-soft) 39px, var(--border-soft) 40px),
    repeating-linear-gradient(90deg, transparent, transparent 39px, var(--border-soft) 39px, var(--border-soft) 40px),
    var(--card-bg-2);
}
.fp-pin { position: absolute; transform: translate(-50%, -100%); display: flex; flex-direction: column; align-items: center; cursor: pointer; z-index: 5; touch-action: none; }
.fp-pin .fp-dot { width: 14px; height: 14px; border-radius: 50%; border: 2.5px solid #fff; box-shadow: 0 1px 4px rgba(0,0,0,0.4); }
.fp-pin.online .fp-dot { background: var(--accent); box-shadow: 0 0 0 4px rgba(13,148,136,0.25), 0 1px 4px rgba(0,0,0,0.4); }
.fp-pin.offline .fp-dot { background: var(--danger); }
.fp-pin .fp-name { font-size: 10.5px; font-weight: 600; background: var(--card-bg); color: var(--text); border: 1px solid var(--border); border-radius: 99px; padding: 0 6px; margin-top: 2px; white-space: nowrap; box-shadow: var(--shadow); }
.fp-pin.flash .fp-dot { animation: fp-flash 0.4s ease 3; }
@keyframes fp-flash { 50% { transform: scale(1.7); } }
.fp-side { display: flex; flex-direction: column; gap: 16px; }
.fp-side .card + .card { margin-top: 0; }
.fp-list { display: flex; flex-direction: column; gap: 2px; max-height: 300px; overflow-y: auto; }
.fp-item { font-size: 13px; padding: 5px 8px; border-radius: 6px; }
.fp-item.clickable { cursor: pointer; }
.fp-item.clickable:hover, .fp-item:hover { background: var(--hover-row); }
.fp-item.placing { background: var(--accent-soft); outline: 1px dashed var(--accent); }
.fp-empty { padding: 4px 8px; }
.fp-balloon {
  position: absolute; z-index: 30; min-width: 220px; max-width: 300px;
  background: var(--card-bg); border: 1px solid var(--border); border-radius: 10px;
  box-shadow: var(--shadow-lg); padding: 10px 12px; font-size: 12.5px;
}
.fp-b-head { display: flex; align-items: center; gap: 4px; margin-bottom: 6px; font-size: 13px; }
.fp-b-row { display: flex; justify-content: space-between; gap: 10px; padding: 2px 0; }
.fp-b-row span { color: var(--text-3); }
.fp-b-row b { font-weight: 550; text-align: right; word-break: break-word; }
.fp-b-link { display: inline-block; margin-top: 7px; color: var(--accent); text-decoration: none; font-weight: 600; font-size: 12.5px; }
.fp-menu {
  position: fixed; z-index: 60; min-width: 190px; background: var(--card-bg);
  border: 1px solid var(--border); border-radius: 10px; box-shadow: var(--shadow-lg); padding: 5px;
}
.fp-m-item { display: block; width: 100%; text-align: left; background: none; border: none; cursor: pointer;
  padding: 7px 10px; font-size: 13px; font-family: var(--font); color: var(--text); border-radius: 6px; text-decoration: none; }
.fp-m-item:hover { background: var(--hover-row); }
.fp-m-item.danger { color: var(--danger); }
.fp-m-sep { height: 1px; background: var(--border-soft); margin: 4px 6px; }
.fp-toast {
  position: fixed; top: 70px; right: 20px; z-index: 80; background: var(--card-bg); color: var(--text);
  border: 1px solid var(--accent); border-left: 4px solid var(--accent); border-radius: 8px;
  padding: 10px 14px; font-size: 13px; box-shadow: var(--shadow-lg); opacity: 0; transform: translateY(-6px);
  transition: opacity 0.25s, transform 0.25s; max-width: 340px;
}
.fp-toast.bad { border-color: var(--danger); border-left-color: var(--danger); }
.fp-toast.show { opacity: 1; transform: none; }
.fp-toast a { color: var(--accent); font-weight: 600; }
@media (max-width: 1100px) { .fp-wrap { grid-template-columns: 1fr; } }

/* ===== Wallboard / TV mode (#4) ===== */
body.wb { background: var(--content-bg); height: 100vh; overflow: hidden; display: flex; flex-direction: column; }
.wb-head { display: flex; align-items: center; justify-content: space-between; padding: 14px 30px; background: var(--card-bg); border-bottom: 1px solid var(--border); flex-shrink: 0; }
.wb-brand { display: flex; align-items: baseline; gap: 10px; font-size: 22px; font-weight: 700; color: var(--text); }
.wb-brand .ic { width: 26px; height: 26px; color: var(--accent); align-self: center; }
.wb-brand em { font-style: normal; font-weight: 500; font-size: 16px; color: var(--text-2); }
.wb-status { display: flex; align-items: center; gap: 22px; font-size: 17px; color: var(--text); }
.wb-live { display: flex; align-items: center; gap: 6px; font-weight: 600; }
.wb-clock { font-variant-numeric: tabular-nums; font-weight: 700; font-size: 20px; }
.wb-exit { color: var(--text-3); text-decoration: none; font-size: 20px; padding: 2px 10px; border-radius: 6px; }
.wb-exit:hover { color: var(--danger); background: var(--content-bg); }
.wb-stage { flex: 1; position: relative; overflow: hidden; }
.wb-panel { position: absolute; inset: 0; padding: 28px 40px 54px; display: flex; flex-direction: column; opacity: 0; pointer-events: none; transition: opacity 0.5s ease; }
.wb-panel.active { opacity: 1; pointer-events: auto; }
.wb-ptitle { margin: 0 0 22px; font-size: 15px; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; color: var(--text-3); }
.wb-kpis { display: grid; grid-template-columns: repeat(3, 1fr); gap: 22px; }
.wb-kpi { background: var(--card-bg); border: 1px solid var(--border); border-radius: 16px; padding: 24px 30px; box-shadow: var(--shadow); }
.wb-kpi-v { font-size: clamp(40px, 6vw, 70px); font-weight: 750; line-height: 1; font-variant-numeric: tabular-nums; }
.wb-kpi-l { font-size: 18px; color: var(--text-2); margin-top: 8px; }
.wb-kpi.ok .wb-kpi-v { color: var(--accent); }
.wb-kpi.bad .wb-kpi-v { color: var(--danger); }
.wb-kpi.warn .wb-kpi-v { color: var(--warn-tx); }
.wb-donuts { display: flex; gap: 70px; justify-content: center; margin-top: 30px; }
.wb-donuts .donut { width: 160px; height: 160px; }
.wb-donuts .donut-cap { font-size: 17px; }
.wb-donuts .donut-sub { font-size: 26px; margin-top: 2px; }
.wb-map { flex: 1; min-height: 0; display: flex; }
.wb-map .map-svg { width: 100%; height: 100%; }
.wb-cols { display: grid; grid-template-columns: 1.4fr 1fr; gap: 30px; flex: 1; min-height: 0; }
/* The two columns were always equal height (grid cells); what was wrong is that their CONTENT sat
   at the top of a 891px column and left ~590px of void underneath on a 1080p screen. Two fixes, and
   neither of them invents data: the type grows to what a TV actually needs, and whatever slack is
   left is SPLIT above and below instead of dumped at the bottom. `justify-content: center` is the
   same answer #077 gave for the same problem on the machine page (`.kv-fill`) - rows keep their own
   rhythm, the leftover is shared. */
.wb-col { display: flex; flex-direction: column; justify-content: center; min-height: 0; }
/* 🔴 `body.wb` IS LOAD-BEARING on the next four rules, not decoration (#203). `.wb-tbl` and
   `.wb-alert` are single-class selectors (0,1,0) - and so are the GENERIC `.tbl` (font-size 13.5px)
   and `.warn` (13px) that sit LOWER in this same file. Equal specificity means the later rule wins,
   so the TV sizes declared here had never applied to a single table cell or alert: the wallboard
   was rendering 13.5px text for a screen read from four metres away, and nobody could see the rule
   was dead because the numbers looked plausible next to each other. `body.wb` makes them 0,2,0 and
   the outcome stops depending on where in the file the next generic rule gets pasted. */
/* Sizes ride on `vh`, not on fixed px, and that is not polish: at 1920x1080 fixed 24px/18px filled
   the panel nicely, and at 1366x768 the SAME numbers pushed the last job row and the last alert out
   of the column - clipped, with `overflow:hidden` making the loss invisible. A wallboard that
   silently drops its last line is worse than one with a void, because nothing on screen says a line
   is missing. clamp() gives the TV its big type and the small screen a complete list. */
/* I37 - the caveat line under the KPI tiles. Scoped `body.wb` and sized in clamp(min, vh, max)
   like every other wallboard rule: a plain `.wb-note` would sit at the SAME specificity as the
   generic rules further down this file and lose to source order, which is exactly how the TV
   rules were dead for a month (#203). Fixed px would fill at 1080p and silently clip at 768p. */
body.wb .wb-note { margin: 14px 0 0; text-align: center; font-size: clamp(13px, 1.9vh, 21px); color: var(--text-3); }
body.wb .wb-tbl { font-size: clamp(14px, 2.2vh, 24px); margin-top: 0; }
body.wb .wb-tbl td { padding: clamp(8px, 1.7vh, 18px) 16px; }
.wb-alerts { display: flex; flex-direction: column; justify-content: center; gap: clamp(6px, 1.3vh, 14px); overflow: hidden; }
body.wb .wb-alert { display: flex; align-items: center; gap: 14px; font-size: clamp(13px, 2vh, 22px); padding: clamp(8px, 1.7vh, 18px) 20px; border-radius: 12px; background: var(--card-bg); border: 1px solid var(--border); }
body.wb .wb-alert .ic { width: clamp(18px, 2.4vh, 26px); height: clamp(18px, 2.4vh, 26px); flex-shrink: 0; }
body.wb .wb-alert b { font-size: clamp(17px, 2.6vh, 28px); }
.wb-alert.bad { border-left: 5px solid var(--danger); }
.wb-alert.warn { border-left: 5px solid #f59e0b; }
.wb-alert.ok { border-left: 5px solid var(--accent); }
/* Availability panel on the wallboard (I30): same helper, TV proportions. Only the two chart
   variables change - the axis alignment follows them, which is exactly why they are variables. */
.wb-avail { flex: 1; min-height: 0; display: flex; flex-direction: column; justify-content: center; }
.wb-avail .chart { --chart-h: min(46vh, 420px); --chart-lh: 18px; grid-template-columns: 64px minmax(0, 1fr); column-gap: 14px; }
.wb-avail .chart-legend { font-size: 18px; gap: 30px; margin-top: 18px; }
.wb-avail .cl::before { width: 22px; height: 4px; margin-right: 9px; }
.wb-avail .cstats { gap: 18px; margin-top: 24px; }
.wb-avail .cstat { padding: 14px 20px; border-radius: 14px; flex: 1 1 180px; }
.wb-avail .cstat b { font-size: clamp(26px, 3vw, 42px); }
.wb-avail .cstat span { font-size: 16px; margin-top: 5px; }
.wb-avail .chart-note { font-size: 16px; margin-top: 14px; }
.wb-dots { position: absolute; bottom: 16px; left: 0; right: 0; display: flex; justify-content: center; gap: 10px; z-index: 5; }
.wb-navdot { width: 12px; height: 12px; border-radius: 50%; border: none; background: var(--border); cursor: pointer; padding: 0; transition: width 0.3s, background 0.3s; }
.wb-navdot.on { background: var(--accent); width: 30px; border-radius: 6px; }

/* Route polyline (per-machine trail) */

/* Mapping forms (map.php) */
/* One line (user decision 2026-08-12). nowrap is deliberate rather than "it happens to fit": the
   fields are a single sentence - this IP, called this, at this point - and wrapping broke it into
   what looked like unrelated questions. The widths below add up to ~840px, so it holds well below
   the narrowest desktop this layout targets. */
.site-form { display: flex; gap: 8px; align-items: center; flex-wrap: nowrap; }
.site-form input[type="text"] { width: 200px; flex: 0 0 auto; }

/* R235 — a site carries its own point, so the coordinates are editable everywhere the site is.
   Narrow on purpose: they sit beside the other fields in the form and inside a table cell.
   ⚠ THE ATTRIBUTE IN THE SELECTOR IS LOAD-BEARING, not decoration. The generic
   `input[type="number"] { width: 100% }` further down is 0,1,1 while a bare `.coord` is 0,1,0, so
   the class LOST and these fields were full-width everywhere - which is why the declaration form
   stacked into five rows instead of one line, and why `td .coord` never narrowed the table either.
   `.coord[type="number"]` is 0,2,0: it wins on specificity, not on being written later, so moving
   these rules around cannot quietly break it again. */
.coord[type="number"] { width: 104px; }
/* Narrower inside a row, and the row's pick button is icon-only: with the full label and wider
   inputs the locations table pushed the PAGE into horizontal scroll (measured: 1358px against a
   1329px viewport). The declaration form above keeps the worded button - it has the room. */
td .coord[type="number"] { width: 78px; margin-right: 4px; }
/* Pick mode: the whole map becomes a target, so say so with the cursor. `crosshair` is the only
   honest affordance here - a pointer would promise navigation, which is exactly what is suspended. */
.js-map-pick.active { color: var(--accent); font-weight: 600; }
/* The value changed without the field being focused, which is easy to miss - so it says so briefly.
   Animation only; no layout shift, and it degrades to plain text if animations are off. */
.coord.just-set { animation: coord-set 1.2s ease-out 1; }
@keyframes coord-set { from { background: var(--warn-bg); } to { background: transparent; } }
@media (prefers-reduced-motion: reduce) { .coord.just-set { animation: none; } }
/* (.site-form select was here: it styled the city dropdown, which no longer exists. Removed with
   it - a rule with no element left to match is not harmless, it is a false clue about the markup.) */

/* ===== Tables ===== */
.tbl { width: 100%; border-collapse: collapse; font-size: 13.5px; margin-top: 10px; }
.tbl th { text-align: left; font-weight: 600; color: var(--text-2); font-size: 12px; padding: 8px 12px; border-bottom: 1px solid var(--border); white-space: nowrap; }
.tbl th a { color: inherit; text-decoration: none; }
.tbl th a:hover { color: var(--accent); }
.tbl td { padding: 9px 12px; border-bottom: 1px solid var(--border-soft); }
.tbl tbody tr:last-child td { border-bottom: none; }
.tbl-rows tbody tr:hover { background: var(--hover-row); }
.tbl .clickable { cursor: pointer; }
.tbl a { color: var(--accent); text-decoration: none; }
.tbl a:hover { text-decoration: underline; }
.tbl a.strong, .tbl a.strong:hover { color: var(--text); font-weight: 600; }
/* Agent log message. `AgentLogs.Msg` is NVARCHAR(1000) but all THREE places that show logs used to
   cut it at 70-90 chars, so the full text was reachable NOWHERE — and the coalescing line
   ("Repeated N more time(s) since <ISO>: <the original message>") is exactly the one that never fit.
   On the pages you open in order to READ logs it now wraps in full; the dashboard card keeps the
   short form (it is a summary) and carries the whole message in `title`.
   pre-wrap keeps agent-side line breaks; break-word is for the unbroken tokens (ISO timestamps,
   paths) that would otherwise push the table sideways. */
.log-msg { white-space: pre-wrap; word-break: break-word; min-width: 260px; }
/* The structured half of a log event (LogEvent.Ctx): exception types, transport bucket, and the
   correlation id that ties one job or one self-update attempt together. Under the prose and quieter
   than it - you read the sentence, you filter by the bag. */
.log-ctx { display: block; margin-top: 4px; font-family: var(--mono); font-size: 11.5px;
  word-break: break-all;
  /* THE FADE IS GONE (2026-08-19). It used to be `color-mix(--text-3 78%, transparent)`, which
     read 2.29 in light and 2.72 in dark - and after --text-3 was corrected for AA it still only
     reached 3.30 / 3.28. Measured across the whole range: with the corrected tokens NO mix below
     100% passes 4.5 in both themes, because dark's --text-3 now sits exactly on 4.50 and any fade
     eats into it. So the hierarchy stops being carried by contrast.
     It does not need to be: this line is already 11.5px mono on its own row under 13.5px prose -
     smaller, different typeface, separate line. That is three signals of "secondary" that cost no
     legibility, against one that made the text harder to read for everyone.
     ⚠ Light was historically the weaker theme here; it no longer is (5.12 vs dark's 4.50) - check
     BOTH if this is ever touched again. */
  color: var(--text-3); }
.mono { font-family: var(--mono); font-size: 12px; }
/* A long identifier in a table cell must WRAP, not push the table out of its card. IPv6 is the
   case that exposes it: colons are not break opportunities, so an address like
   2a02:587:b1f:d000:297b:c2e:80e0:b6e5 is one unbreakable 36-character word - measured at 291 px,
   which took the per-machine trail table 174 px past the right edge of its card and cut off the
   "Last seen" column.
   `anywhere`, NOT `break-word`: only `anywhere` shrinks the cell's MIN-CONTENT width, and the
   min-content width is what the table layout uses to decide it does not fit. `break-word` wraps the
   text but leaves the column just as wide, so the overflow survives.
   Scoped to table cells: an inline .mono (a UUID beside a copy button) has no column to break. */
.tbl td.mono { overflow-wrap: anywhere; }
.muted { color: var(--text-3); }
/* R106: disconnected χρήστης (agent WTS) — gray + italic ώστε να ξεχωρίζει από τους active. */
.user-disc { color: var(--text-3); font-style: italic; }
.right { text-align: right; }
.card.no-pad { padding: 0; overflow: hidden; }
.card.no-pad .tbl { margin-top: 0; }
.card.no-pad .tbl th { padding-top: 12px; padding-bottom: 12px; background: var(--card-bg-2); }
.card.no-pad > h2 { padding: 14px 16px 0; }  /* #4: τίτλος σε no-pad card (π.χ. dashboard recent jobs) — padding ενώ ο πίνακας μένει flush */

/* I38 (user request 20/08): NO dashboard card out-grows the reference height. A card with more
   data scrolls INSIDE itself instead of stretching its whole grid row - the same idea as
   `.tl-scroll` on the machine card, which is the example the request pointed at.

   416px is MEASURED, not chosen: it is what the Locations card renders at on the real dashboard
   at 1920px. (It is a STRETCHED height - Locations' own content is 105px and its row-mate
   "Recent jobs" is what sets 416 - but it is the height on the screen the request was about.)

   🔴 `.card.dash-cap`, NOT `.dash-cap`: `.card.no-pad` is (0,2,0) and sets `overflow:hidden`,
      so a single-class rule would lose on specificity and do nothing - the #203 trap, again.

   The cap sits on the CARD and the body takes whatever is left via flex, so the arithmetic
   (header + margins + padding, and it differs between padded and no-pad cards) is never written
   down anywhere and therefore cannot go stale. */
.card.dash-cap { display: flex; flex-direction: column; max-height: var(--dash-cap-h, 416px); }
/* 🔴 `min-height: 0` is load-bearing: a flex child defaults to `min-height: auto`, which refuses
   to shrink below its content - the scrollbar would never appear and the card would grow anyway. */
.card.dash-cap > .dash-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto;
  scrollbar-width: thin; scrollbar-color: var(--border) transparent; }
.card.dash-cap > .dash-scroll::-webkit-scrollbar { width: 8px; }
.card.dash-cap > .dash-scroll::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
.card.dash-cap > .dash-scroll::-webkit-scrollbar-thumb:hover { background: var(--text-3); }
/* A no-pad card puts its padding on the header; the scroller must not re-introduce a gap. */
.card.no-pad.dash-cap > h2 { margin-bottom: 10px; }
/* The header row stays put while the rows move under it. `.card.no-pad .tbl th` already carries an
   opaque background (var(--card-bg-2)), which sticky needs - a transparent one would let the rows
   show through it. */
.card.dash-cap > .dash-scroll .tbl th { position: sticky; top: 0; z-index: 1; }
.card.empty { text-align: center; padding: 36px; }
.cell-main { display: flex; align-items: center; gap: 9px; min-width: 0; }
.cell-main .ic { width: 16px; height: 16px; color: var(--text-3); }
.cell-sub { display: block; font-size: 11.5px; color: var(--text-3); }
/* Live users (21/08): the continuation rows of one person. The indent is what says "this machine
   belongs to the name above" - without it the group reads as separate people with no names. */
.tbl td.live-cont { padding-left: 26px; }
/* "on N PCs" sits opposite the person's name and belongs to that line, so it takes the name's weight
   (`.tbl a.strong`, 600) - the size was already shared. Muted colour stays: same font, lower voice. */
.tbl td.live-pcs { font-weight: 600; }
/* The name row of a group. Without this it gets the same rule under it as every other row and the
   person is cut off from the machines that belong to them - the border would separate the group
   from ITSELF. The rule under the group's LAST machine is what separates one person from the next. */
.tbl tr.live-head td { border-bottom: none; padding-bottom: 2px; }
/* I36 - the who-is-on-it badges inside a Live computers row. Its own rule rather than
   .hero-chips: that one carries a hero's spacing and would drag it into a table cell. */

/* ===== Badges / dots / chips ===== */
.badge { display: inline-block; font-size: 11.5px; font-weight: 600; padding: 2px 9px; border-radius: 99px; white-space: nowrap; }
.badge-ok { background: var(--badge-ok-bg); color: var(--badge-ok-tx); }
.badge-bad { background: var(--badge-bad-bg); color: var(--badge-bad-tx); }
.badge-wait { background: var(--badge-wait-bg); color: var(--badge-wait-tx); }
.badge-run { background: var(--badge-run-bg); color: var(--badge-run-tx); }
.badge-muted { background: var(--badge-mut-bg); color: var(--badge-mut-tx); }
.dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; vertical-align: middle; }
.dot-ok { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.dot-bad { background: var(--danger); }
.dot-label { font-size: 12.5px; font-weight: 500; vertical-align: middle; }
.dot-label-ok { color: var(--accent); }
.dot-label-bad { color: var(--danger); }
/* Αποκλεισμένο (revoked/pending/removed): ούτε πράσινο ούτε κόκκινο — μιλά, αλλά το διώχνουμε. */
.dot-wait { background: var(--badge-wait-tx); }
.dot-label-wait { color: var(--badge-wait-tx); }

/* Version chip (up-to-date / outdated) */
.vchip { display: inline-flex; align-items: center; gap: 4px; font-size: 11px; font-weight: 600; padding: 1px 8px; border-radius: 99px; margin-left: 6px; vertical-align: middle; }
.vchip .ic { width: 11px; height: 11px; }
.vchip-ok { background: var(--badge-ok-bg); color: var(--badge-ok-tx); }
.vchip-out { background: var(--badge-wait-bg); color: var(--badge-wait-tx); }

/* ===== Toolbar (search + filter chips) ===== */
.toolbar { display: flex; align-items: center; gap: 14px; margin-bottom: 14px; flex-wrap: wrap; }
.search-form { display: flex; gap: 8px; flex: 1; min-width: 240px; }
.search-form input[type="text"] { flex: 1; }
.btn-sm { display: inline-flex; align-items: center; gap: 6px; background: var(--accent); color: var(--on-accent); border: none; border-radius: 6px; padding: 8px 14px; font-size: 13px; font-weight: 600; cursor: pointer; text-decoration: none; white-space: nowrap; }
.btn-sm:hover { background: var(--accent-hover); }
.btn-sm.ghost { background: transparent; color: var(--text-2); border: 1px solid var(--border); }
.btn-sm.ghost:hover { color: var(--accent); border-color: var(--accent); background: transparent; }
/* I12: καταστροφική ενέργεια (απόσυρση) — διακριτική μέχρι να την κοιτάξεις, κόκκινη όταν την πλησιάσεις. */
.btn-sm.ghost.danger { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 40%, transparent); }
.btn-sm.ghost.danger:hover { color: #fff; background: var(--danger); border-color: var(--danger); }
.btn-sm .ic { width: 14px; height: 14px; }
.chips { display: flex; gap: 6px; flex-wrap: wrap; }
/* The drilled-in row ALWAYS starts its own line. `.toolbar` is a wrapping flex row, so without
   `flex-basis: 100%` these chips simply continued after the areas - three groups on one line, with
   the left border showing up as a stray vertical sliver between them.
   Indented, no border: the offset alone says "inside the area above", and a rule that only ever
   appeared mid-line was decorating a hierarchy that the layout was not expressing. */
.chips-sub { flex-basis: 100%; margin-left: 14px; }
.chip { font-size: 12.5px; color: var(--text-2); text-decoration: none; border: 1px solid var(--border); border-radius: 99px; padding: 5px 13px; white-space: nowrap; }
.chip:hover { border-color: var(--accent); color: var(--accent); }
.chip-active { background: var(--accent); color: var(--on-accent); border-color: var(--accent); }
.chip b { font-weight: 650; margin-left: 4px; }
.count { font-size: 12.5px; color: var(--text-3); margin-left: auto; }

/* ===== Pager ===== */
.pager { display: flex; gap: 5px; margin: 16px 0; justify-content: center; }
.pg { min-width: 32px; text-align: center; padding: 6px 10px; font-size: 13px; border: 1px solid var(--border); border-radius: 6px; color: var(--text-2); text-decoration: none; }
.pg:hover { border-color: var(--accent); color: var(--accent); }
.pg-active { background: var(--accent); color: var(--on-accent); border-color: var(--accent); }
.pg-off { opacity: 0.4; }

/* ===== Tenant switcher ===== */
.tenant-switch { margin: 0; }
.tenant-switch select { font-family: var(--font); font-size: 12.5px; color: var(--text); border: 1px solid var(--border); border-radius: 6px; padding: 5px 9px; background: var(--card-bg); cursor: pointer; max-width: 200px; }
.tenant-switch select:focus { outline: none; border-color: var(--accent); }

/* ===== Detail pages / hero ===== */
.detail-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
.detail-title { margin: 0 0 2px; font-size: 20px; font-weight: 600; }

.hero {
  background: var(--card-bg); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow); padding: 18px 22px; margin-bottom: 16px;
  display: flex; align-items: center; justify-content: space-between; gap: 18px; flex-wrap: wrap;
}
.hero-id { display: flex; align-items: center; gap: 14px; min-width: 0; }
.hero-ico { width: 44px; height: 44px; border-radius: 10px; background: var(--accent-soft); color: var(--accent); display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.hero-ico .ic { width: 22px; height: 22px; }
.hero-name { margin: 0; font-size: 20px; font-weight: 650; line-height: 1.2; }
.hero-chips { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-top: 5px; }
.hero-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* Το hero δεν είναι .card, οπότε το `.card select` δεν το πιάνει και μένει άστυλο. Padding/μέγεθος
   ταιριαστά με το .btn-sm (8px 14px / 13px) που στέκεται δίπλα του — αλλιώς το select βγαίνει 4px
   κοντύτερο και η σειρά των ενεργειών φαίνεται στραβή. */
.hero-actions select { font-family: var(--font); font-size: 13px; color: var(--text); border: 1px solid var(--border); border-radius: 6px; padding: 8px 10px; background: var(--card-bg); cursor: pointer; }
.hero-actions select:focus { outline: none; border-color: var(--accent); }
.hero-actions form.inline { display: flex; align-items: center; gap: 6px; }

/* ===== Tabs (server-side ?tab=) ===== */
.tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--border); margin-bottom: 16px; }
.tab { padding: 9px 16px; font-size: 13.5px; font-weight: 550; color: var(--text-2); text-decoration: none; border-bottom: 2px solid transparent; margin-bottom: -1px; }
.tab:hover { color: var(--accent); }
.tab-active { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; }
.tab b { font-weight: 650; margin-left: 5px; font-size: 12px; color: var(--text-3); }
.tab-active b { color: var(--accent); }

.card-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
.card-grid .card + .card { margin-top: 0; }
/* ⚠ line-height in WHOLE PIXELS, not the inherited 1.5 ratio. 13 x 1.5 = 19.5, and a card heading
   with a fractional height puts EVERYTHING below it inside the card on a half pixel. On a screen
   with devicePixelRatio 1 that is half a physical pixel, and small bold text rendered across two
   device pixels reads as blurry - which is exactly how it surfaced: the map's pin labels looked
   fuzzy while the map itself (a canvas, resampled anyway) looked fine. Same reason for h2 below. */
.card-grid h3, .card h3 { margin: 0 0 10px; font-size: 13px; line-height: 20px; font-weight: 600; color: var(--text-2); display: flex; align-items: center; gap: 7px; }
.card h3 .ic { width: 14px; height: 14px; color: var(--text-3); }
.kv-row { display: flex; justify-content: space-between; gap: 12px; padding: 5px 0; font-size: 13.5px; border-bottom: 1px solid var(--border-soft); align-items: center; }
.kv-row:last-child { border-bottom: none; }
.kv-k { color: var(--text-3); flex-shrink: 0; }
.kv-v { color: var(--text); text-align: right; word-break: break-word; min-width: 0; }
.meta-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px 20px; }
.meta-grid .kv-k { display: block; font-size: 11.5px; margin-bottom: 3px; }
.meta-grid .kv-v { display: block; text-align: left; font-size: 14px; }

/* Copy button (click-to-copy) */
.copy-btn {
  background: none; border: 1px solid var(--border); border-radius: 5px; padding: 2px 5px;
  color: var(--text-3); cursor: pointer; display: inline-flex; vertical-align: middle; margin-left: 6px;
  transition: color 0.15s, border-color 0.15s;
}
.copy-btn .ic { width: 12px; height: 12px; }
.copy-btn:hover { color: var(--accent); border-color: var(--accent); }
.copy-btn.copied { color: var(--on-accent); background: var(--accent); border-color: var(--accent); }
.copy-btn .ic-check { display: none; }
.copy-btn.copied .ic-check { display: block; }
.copy-btn.copied .ic-copy { display: none; }

/* ===== Timeline ===== */
.tl { list-style: none; margin: 10px 0 0; padding: 0; position: relative; }
.tl::before { content: ""; position: absolute; left: 7px; top: 6px; bottom: 6px; width: 2px; background: var(--border-soft); border-radius: 2px; }
.tl-item { position: relative; padding: 0 0 14px 26px; }
.tl-item:last-child { padding-bottom: 2px; }
.tl-dot { position: absolute; left: 2px; top: 4px; width: 12px; height: 12px; border-radius: 50%; background: var(--card-bg); border: 3px solid var(--text-3); }
.tl-ok .tl-dot { border-color: var(--accent); }
.tl-bad .tl-dot { border-color: var(--danger); }
.tl-warn .tl-dot { border-color: #f59e0b; }
.tl-label { font-size: 13px; font-weight: 600; }
.tl-desc { font-size: 12.5px; color: var(--text-2); }
.tl-when { font-size: 11.5px; color: var(--text-3); font-variant-numeric: tabular-nums; }
/* Timeline: «έξυπνο» cap — scroll μόνο όταν τα events ξεπερνούν το ύψος (με τον καιρό μεγαλώνει). */
.tl-scroll { max-height: 320px; overflow-y: auto; margin-top: 4px; scrollbar-width: thin; scrollbar-color: var(--border) transparent; }
.tl-scroll .tl { margin-top: 6px; }
.tl-scroll::-webkit-scrollbar { width: 8px; }
.tl-scroll::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
.tl-scroll::-webkit-scrollbar-thumb:hover { background: var(--text-3); }

/* ===== Alerts panel (dashboard) ===== */
.alist { display: flex; flex-direction: column; margin-top: 8px; }
.alist-row { display: flex; align-items: center; gap: 10px; padding: 8px 2px; border-bottom: 1px solid var(--border-soft); font-size: 13px; text-decoration: none; color: inherit; }
.alist-row:last-child { border-bottom: none; }
a.alist-row:hover { background: var(--hover-row); border-radius: 6px; }
.alist-ico { width: 28px; height: 28px; border-radius: 7px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.alist-ico .ic { width: 14px; height: 14px; }
.alist-ico.bad { background: var(--danger-bg); color: var(--danger-tx); }
.alist-ico.warn { background: var(--warn-bg); color: var(--warn-tx); }
.alist-ico.ok { background: var(--badge-ok-bg); color: var(--badge-ok-tx); }
/* Title over subtitle, not beside it. Both were plain <span>, i.e. INLINE, so they ran together
   with no separator at all ("Agents behind the latest versionThey will self-update...").
   Making the wrapper a flex column is what gives them their own lines; it also fixes a second,
   quieter bug: text-overflow:ellipsis NEVER applies to an inline box, so the nowrap on .alist-t
   only ever clipped, it never showed the ellipsis it asked for. min-width:0 on both the wrapper
   and the title is what actually lets a flex child shrink far enough to ellipsise. */
.alist-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.alist-t { min-width: 0; font-weight: 550; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* The subtitle is allowed to wrap: it explains WHY the row is here, and clipping the explanation
   to keep rows a uniform height trades the point of the row for tidiness. break-word guards the
   long unbroken tokens that show up in these strings (paths, versions). */
.alist-s { font-size: 11.5px; color: var(--text-3); line-height: 1.35; overflow-wrap: break-word; }
.alist-n { font-family: var(--mono); font-size: 12px; color: var(--text-2); white-space: nowrap; }

/* ===== NIC table (computer → Network tab) ===== */
.nic-badge { display: inline-block; font-size: 10.5px; font-weight: 600; padding: 1px 7px; border-radius: 99px; background: var(--badge-mut-bg); color: var(--badge-mut-tx); }
.nic-badge.dhcp { background: var(--badge-run-bg); color: var(--badge-run-tx); }

/* ===== Job output ===== */
.out-h { margin: 14px 0 6px; font-size: 12.5px; font-weight: 600; color: var(--text-2); }
.output { background: var(--output-bg); color: var(--output-tx); font-family: var(--mono); font-size: 12.5px; line-height: 1.55; padding: 14px 16px; border-radius: 8px; overflow-x: auto; white-space: pre-wrap; word-break: break-word; max-height: 460px; margin: 0; }
.output-err { color: #fca5a5; }
.output-script { color: #a5d6ff; }
.clixml { margin-top: 6px; }
.clixml summary { cursor: pointer; font-size: 12.5px; color: var(--text-3); padding: 4px 0; }
.clixml summary:hover { color: var(--text-2); }
.clixml .output { margin-top: 8px; }

/* ===== Forms / actions (job create, users, installer) ===== */
textarea { width: 100%; padding: 10px 12px; font-size: 13px; font-family: var(--mono); border: 1px solid var(--border); border-radius: 6px; background: var(--card-bg); color: var(--text); resize: vertical; line-height: 1.55; }
.card select { width: 100%; padding: 9px 12px; font-size: 14px; font-family: var(--font); border: 1px solid var(--border); border-radius: 6px; background: var(--card-bg); color: var(--text); }
input[type="number"] { width: 100%; padding: 9px 12px; font-size: 14px; border: 1px solid var(--border); border-radius: 6px; background: var(--card-bg); color: var(--text); }
textarea:focus, .card select:focus, input[type="number"]:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px rgba(13, 148, 136, 0.15); }

.form-row3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
.form-row3 label { margin-top: 8px; }
/* A cap, because these rows hold SHORT values - an email, a version, three timeouts - and the cards
   they live in are as wide as the screen. At 1850px each field was ~600px: a text box six times the
   length of what anyone types reads as an error, not as generosity. Below the cap nothing changes. */
.form-row3 { max-width: 1080px; }
/* The submit as the last column, bottom-aligned with the inputs (no spacer label needed). */
.form-row3.with-action { grid-template-columns: 1.3fr 1.1fr 1.1fr auto; align-items: end; }

.btn-row { display: flex; gap: 10px; margin-top: 18px; flex-wrap: wrap; align-items: center; }
.btn-row form { margin: 0; }
.btn-inline { display: inline-flex; align-items: center; gap: 7px; background: var(--accent); color: var(--on-accent); border: none; border-radius: 6px; padding: 10px 18px; font-size: 14px; font-weight: 600; font-family: var(--font); cursor: pointer; text-decoration: none; }
.btn-inline:hover { background: var(--accent-hover); }
.btn-ghost { background: transparent; color: var(--text-2); border: 1px solid var(--border); }
.btn-ghost:hover { background: var(--content-bg); color: var(--text); border-color: var(--text-3); }
.btn-danger { background: var(--danger); }
.btn-danger:hover { background: #b91c1c; }
.btn-primary { background: var(--accent); color: var(--on-accent); }
.btn-inline .dl { display: none; }

.warn { color: var(--warn-tx); background: var(--warn-bg); border-radius: 6px; padding: 9px 12px; font-size: 13px; margin: 0 0 12px; }
.steps { margin: 8px 0 0; padding-left: 20px; line-height: 1.9; font-size: 14px; color: var(--text-2); }
.confirm-card { border-left: 3px solid var(--danger); }
.ok-card { border-left: 3px solid var(--accent); }
.temp-card { border-left: 3px solid #d97706; background: var(--warn-bg); }

.link-btn { background: none; border: none; color: var(--accent); font-size: 12.5px; cursor: pointer; padding: 4px 6px; font-family: var(--font); }
.link-btn:hover { text-decoration: underline; }
.link-btn.danger { color: var(--danger); }
form.inline { display: inline; margin: 0; }
.nowrap { white-space: nowrap; }
.bulkbar { margin: 0 0 12px; }

/* ===== Responsive ===== */
@media (max-width: 1400px) {
  .kpi-grid { grid-template-columns: repeat(3, 1fr); }
  .g-3, .g-4, .g-5 { grid-column: span 6; }
  .g-7, .g-8, .g-9 { grid-column: span 12; }
  /* Undo the 2x2 placement: below this width the four cards are a single stacked column, and a card
     still pinned to `grid-row: 2` would leave a hole where row 1 used to be. */
  .grid-2x2 > .card { grid-column: auto / span 12; grid-row: auto; }
  .hide-lg { display: none; }
}
@media (max-width: 860px) {
  body.app { flex-direction: column; }
  .sidebar { width: 100%; height: auto; position: static; flex-direction: row; align-items: center; padding: 10px 12px; gap: 8px; }
  .brand { padding: 0 8px 0 0; }
  .nav { flex-direction: row; overflow-x: auto; }
  .nav-item em { display: none; }
  .nav-item span { white-space: nowrap; }
  /* Was `display: none` while this held the "portal v2" label. It now holds the ONLY route to the
     wallboard, and a tablet propped next to a screen is a real way to open it - so it stays, as one
     more chip at the end of the horizontal nav row. */
  .sidebar-foot { padding: 0; flex-shrink: 0; }
  .sidebar-tv span { white-space: nowrap; }
  .content { padding: 16px; }
  .user-meta { display: none; }
  .top-search { display: none; }
  /* Horizontal scroll on phones, on EVERY page (measured 8-175px depending on the title's width):
     the topbar is one nowrap row, and its right-hand group alone needs ~412px on a 375px screen -
     tenant switcher 110 + refresh 94 + theme 32 + language 36 + user 48 + logout 32, plus gaps.
     Nothing is dropped to fix it: the title takes its own line and the controls WRAP onto as many
     lines as they need. Hiding a control instead would trade a visible annoyance for an invisible
     one - the tenant switcher is how a SuperAdmin changes organisation, and it is the widest item.
     ⚠ `position: static` stays: a sticky bar that is now two or three lines tall would eat the
     screen it is supposed to leave for the content. */
  .topbar { position: static; flex-wrap: wrap; row-gap: 10px; padding: 10px 16px; }
  .topbar h1 { flex: 1 1 100%; }
  .topbar-right { width: 100%; flex-wrap: wrap; gap: 8px; }
  /* The select is the one element whose width comes from DATA (the longest organisation name), so
     it is the one that has to be told it may shrink - otherwise a new customer with a long name
     silently brings the overflow back. */
  .tenant-switch select { max-width: 45vw; }
  /* The site-declaration form is `nowrap` by USER DECISION (2026-08-12): the fields are one
     sentence - this IP, called this, at this point - and wrapping broke it into what looked like
     unrelated questions. That decision was made about a desktop, where the ~840px fit. On a 375px
     phone the same row is 561px of horizontal scroll, so here - and ONLY here - it wraps. The
     desktop rule above is untouched. */
  /* Wide tables scroll INSIDE their card instead of pushing the page sideways. Three tables cannot
     be made to fit 375px by hiding columns without deleting information someone needs (enrollment
     473px even after `.hide-md`, the map's sites 676px, versions 409px) - so the choice is where the
     scroll lives, not whether it exists. Inside the card it is discoverable and local; on the
     document it moves the whole layout, including the header, and every page inherits it.
     ⚠ Only under this breakpoint: on desktop these tables fit and `display:block` would cost them
     `width:100%`. The sticky `th` is already `position:static` here, so nothing detaches. */
  .card .tbl { display: block; overflow-x: auto; }
  .site-form { flex-wrap: wrap; }
  .site-form input[type="text"], .site-form .coord[type="number"] { width: 100%; flex: 1 1 140px; }
  .card.no-pad .tbl th,
  .card.dash-cap > .dash-scroll .tbl th { position: static; }
  .kpi-grid { grid-template-columns: repeat(2, 1fr); }
  .col2, .card-grid { grid-template-columns: 1fr; }
  .meta-grid, .form-row3 { grid-template-columns: 1fr; }
  /* ⚠ `.with-action` is (0,2,0) and would otherwise BEAT the single-column rule above (0,1,0) - the
     same specificity trap as #203. Measured before this line: the email field rendered 31px wide on
     a phone and the page scrolled sideways. The override has to carry the class too. */
  .form-row3.with-action { grid-template-columns: 1fr; align-items: stretch; }
  .g-3, .g-4, .g-5, .g-6, .g-7, .g-8, .g-9 { grid-column: span 12; }
  .hide-md { display: none; }
  .hero { flex-direction: column; align-items: flex-start; }
  /* A phone fits 3 axis stops, not 5: the other two would overlap into an unreadable smear. */
  .chart { --chart-h: 150px; }
  .chart-x span:nth-child(even) { display: none; }

  /* The wallboard is a TV view, but the sidebar offers it on phones too (#204 kept that link
     visible), and there it scrolled 167px sideways: the header is one nowrap row (brand + online +
     offline + clock + exit = 308px of status alone) and the 3-column KPI grid was being clipped by
     `.wb-stage`. Wrap the header, halve the grid. Nothing above this breakpoint changes - the
     television keeps the layout that was designed for it. */
  .wb-head { flex-wrap: wrap; row-gap: 8px; }
  .wb-status { flex-wrap: wrap; row-gap: 6px; }
  .wb-kpis { grid-template-columns: repeat(2, 1fr); gap: 12px; }
  .wb-kpi { padding: 14px 16px; }
  .wb-donuts { gap: 26px; margin-top: 18px; }
  .wb-donuts .donut { width: 110px; height: 110px; }
  .wb-cols { grid-template-columns: 1fr; }
  /* A television has one fixed screen and never scrolls - that is the whole design. A phone cannot
     honour it: at 375x812 two panels are taller than the stage, and `.wb-stage{overflow:hidden}` was
     hiding the remainder with nothing on screen to say so. Let the PANEL scroll here, so what does
     not fit is reachable instead of gone. */
  .wb-panel { overflow-y: auto; }
}

/* ===== Vector-tile map (map.php, MapLibre) — 2026-08-12 =====
   Separate from the .map-* rules, which still serve the SVG map on the wallboard and the per-machine
   trail. The two systems coexist deliberately; sharing rules between them would couple a page that
   must stay dependency-free (the wallboard loads no pan/zoom script) to one that pulls in 1.4 MB of
   MapLibre. */
.map-gl { width: 100%; height: 620px; border-radius: var(--radius); overflow: hidden; background: var(--card-bg-2); }
/* The marker is an <a>, so it keeps real navigation (middle-click, copy link, keyboard). MapLibre
   positions it by writing to element.style, which CSSOM allows - unlike an inline style attribute,
   which `style-src 'self'` would block. */
/* 🔴 THE ANCHORED BOX IS THE DOT, AND ONLY THE DOT. MapLibre centres the marker ELEMENT on the
   coordinate, so while this was a flex row of dot + label the dot sat half a label-width to the
   left of the actual point - a CONSTANT pixel offset, which is invisible when zoomed in and
   catastrophic when zoomed out: measured -54.8 px at every zoom, which at z0 is -32.9 DEGREES of
   longitude. Alexandroupoli rendered over Spain.
   The label is therefore absolutely positioned OUT of the box: the element's size must never
   depend on the text, or the error comes straight back with the next long site name. */
.mgl-pin { position: relative; display: block; width: 11px; height: 11px;
  text-decoration: none; cursor: pointer;
  background: none; padding: 0; margin: 0; border-radius: 0; }
/* ⚠ t- PREFIX, not the bare tone name: `.warn` further up is a global warning-box utility
   (background + padding + radius). A marker carrying the bare class rendered as a yellow rounded
   box around its label. The SVG map used the bare names and never showed it, because SVG ignores
   background and padding - the collision surfaced the moment the pins became DOM elements. The
   reset above is the second line of defence, so a future global utility cannot dress them again. */
.mgl-pin.t-ok   { --pin: var(--accent); }
.mgl-pin.t-warn { --pin: #d97706; }
.mgl-pin.t-bad  { --pin: var(--danger); }
.mgl-pin.t-idle { --pin: var(--text-3); }
.mgl-dot { position: absolute; inset: 0; border-radius: 50%; background: var(--pin); box-shadow: 0 0 0 1.5px var(--card-bg); }
.mgl-pin.auto .mgl-dot { background: var(--card-bg); box-shadow: 0 0 0 1.5px var(--pin); }
/* The halo is what keeps a name readable over both sea and city fill without a background box,
   which would cover the map it is describing. */
/* The trail's stop number, centred inside its own dot. Absolute like the label, for the same
   reason: nothing may change the size of the anchored box. */
.mgl-num { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  font-weight: 700; color: #fff; line-height: 1; pointer-events: none; }
/* ⚠ The number follows the DOT, not a fixed colour. A hollow (auto-geo) dot is filled with
   --card-bg, which is #ffffff in the light theme - so a white number on it was WHITE ON WHITE and
   every stop on the trail lost its number. It survived review because the check measured that the
   number FITS inside the dot: geometry, not contrast. Every trail pin in real data is auto-geo,
   so this was not an edge case - it was all of them. */
.mgl-pin.auto .mgl-num { color: var(--pin); }
/* Absolute, so it sits BESIDE the dot without ever entering the anchored box. transform here is
   safe: MapLibre writes its positioning transform on the marker element, not on this child. */
.mgl-label { position: absolute; left: 15px; top: 50%; transform: translateY(-50%);
  white-space: nowrap; font-size: 12px; font-weight: 600; color: var(--text);
  text-shadow: 0 0 3px var(--card-bg), 0 0 3px var(--card-bg), 0 0 3px var(--card-bg); }
.mgl-pin:hover .mgl-label { color: var(--pin); }
/* Pick mode (R235): the whole map becomes a target, so the cursor says so. `crosshair` is the only
   honest affordance - a pointer would promise navigation, which is exactly what is suspended. */
.map-gl.picking canvas { cursor: crosshair; }
.map-gl.picking .mgl-pin { pointer-events: none; }
