/* v2.2.1: privacy-friendly font load via bunny.net (a Google Fonts mirror
 * with no tracking, no cookies, GDPR-friendly). Falls back gracefully if
 * the host can't reach bunny.net — system font stack picks up the slack.
 *
 * Inter: UI font, modern sans designed for screens. Stylistic alternates
 *        cv02/cv03/cv04/cv11 already enabled in body for the better "1",
 *        "a", "g" glyphs.
 * JetBrains Mono: monospaced font for technical identifiers (device IDs,
 *        hashes, paths, command output, code).
 */
/* v5.6.x perf: the font sheets (inter-jetbrains.css + industrial.css) are now
   parallel <link>s in index.html — an @import here serialized them behind
   styles.css' own download+parse, delaying first text paint. Keep them OUT
   of this file. */

/* CMDB active tab — moved out of inline <style> injection (CSP L1). */
.cmdb-tab-btn.active {
  background: rgba(59, 126, 255, 0.12);
  border-color: var(--accent);
  color: var(--accent-ink);
}

  /* v6.0.0 "ClarityMatters" — THE palette (chosen-design tokens). Dark is the
     default; body.light carries the light set; named themes override both. */
  :root {
    --bg: #0c0f13;
    --surface: #161b22;
    --surface2: #1b212a;
    --border: #242b34;
    --accent: #3b7eff;
    --accent2: #7ea9ff;
    /* WCAG AA text-on-accent (item #62): #3b7eff only clears 4.5:1 against
       black (5.61:1), not white (3.74:1) -- every theme below sets its own,
       computed the same way (contrast vs black and white, pick the winner). */
    --accent-contrast: #000;
    /* v6.4.2 a11y: the REVERSE case. --accent-contrast is ink ON an accent
       fill; --accent-ink is the accent used AS ink (.c-accent, doc links,
       active tab labels, badge text). The raw accent fails AA that way on our
       own surfaces -- worst case the shipped amber on a light theme, 2.15:1 --
       so every palette carries an ink variant that clears 4.5:1 against its
       own --bg/--surface/--surface2 AND against --accent-soft composited over
       each of them. Accent FILLS keep using --accent, so the brand colour is
       unchanged wherever it is a background or a border.
       tests/test_v642_css.py recomputes every theme x accent pair. */
    --accent-ink: #5a92ff;
    --accent2-ink: #7ea9ff;
    --text: #e8edf3;
    --muted: #7f8b98;
    /* status colours = chosen-design --ok/--crit/--warn */
    --green: #4ea87a;
    --green-dim: #166534;
    --red: #e5645f;
    --red-dim: #7f1d1d;
    --amber: #e0a83a;
    --orange: #f97316;   /* severity "high" / ERROR — themeable; inherited by themes that don't override it */
    --purple: #7c3aed;
    /* v6.0.0 ClarityMatters: the design's system sans (chosen-design --sans) */
    --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
    /* warm accent (fail2ban/attention chrome) + recessed text-well ground */
    --warm: #f5a623; --warm-soft: rgba(245,166,35,0.16); --warm-edge: rgba(245,166,35,0.55);
    /* v6.4.2 a11y (SC 1.4.3): ink for text sitting on its OWN 15%-tinted badge
       background rather than on --surface. Measured, not eyeballed: --muted on
       the grey tint is 4.27:1 dark / 3.79:1 light, and --green is 3.55:1 light
       — all below the 4.5 minimum. These two clear it in both themes. */
    --green-on-tint: #5aae83;   /* >=4.5:1 on its tint over --surface AND --surface2 */
    --red-on-tint: #ffb8b4;     /* ditto. v6.4.3: --red itself is 4.34:1 on the
                                   red tint — below AA for the 10px badges that
                                   use it, in EVERY theme (nord was 2.60). */
    --muted-on-tint: #8b95a1;   /* >=4.5:1 on its tint over --surface AND --surface2 */
    --well-bg: var(--bg);
  }
  body.light {
    --bg: #eef1f5;
    --surface: #ffffff;
    --surface2: #eef1f5;
    --border: #d9dee5;
    --text: #141922;
    --muted: #6a7787;
    --green: #2f8f5b;
    --green-on-tint: #267249;   /* >=4.5:1 on its tint over --surface AND --surface2 */
    --red-on-tint: #96231c;     /* ditto */
    --muted-on-tint: #57626f;   /* >=4.5:1 on its tint over --surface AND --surface2 */
    --red: #d33f39;
    --amber: #c67e12;
    --warm: #c2710a; --warm-soft: rgba(194,113,10,0.12); --warm-edge: rgba(194,113,10,0.50);
    --accent-ink:#0055fa; --accent2-ink:#0052f6;
  }
  /* (v4.3.0: the old body.light header rgba override is gone — the header now
     uses opaque var(--bg), which is theme- and light-mode-correct by itself.) */
  /* Accent presets moved BELOW the data-theme blocks (see ~"v4.1: full themes")
     so a chosen accent overrides the active theme's default accent. */
  .accent-swatch { width: 24px; height: 24px; border-radius: 50%; border: 2px solid var(--border); cursor: pointer; padding: 0; }
  .accent-swatch.sel { border-color: var(--text); box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px currentColor; }
  /* v4.3.0 a11y: visible keyboard-focus ring everywhere. :focus-visible fires
     for keyboard navigation only, so mouse users see no change; elements with
     their own outline:none + focus styling (e.g. .sidebar-search) win on
     specificity, which is intended. */
  :focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
  body.light .journal-wrap { background: #f8fafc; color: #334155; }

  /* ── v4.1: full themes (data-theme on <body>) ──────────────────────────────
     Each theme is a complete palette, not just an accent. Dark themes set
     data-theme only; light-type themes ALSO get the `.light` class from JS so
     the structural light overrides (header bg, grid lines, journal) apply.
     Declared AFTER body.light and BEFORE the accent presets, so a theme's
     palette beats the default light palette and a chosen accent beats the
     theme's default accent. `dark`/`midnight` = :root; `light` = body.light. */
  body[data-theme="nord"] {
    --bg:#2e3440; --surface:#3b4252; --surface2:#434c5e; --border:#4c566a;
    --text:#eceff4; --muted:#aab0bd; --accent:#88c0d0; --accent2:#8fbcbb;
    --green:#a3be8c; --green-dim:#4a5a3a; --red:#bf616a; --red-dim:#5a2f33;
    --red-on-tint:#ffb8b4;
    --amber:#ebcb8b; --purple:#b48ead;
    --accent-soft:rgba(136,192,208,0.12); --accent-edge:rgba(136,192,208,0.30);
    --accent-contrast:#000;
    --accent-ink:#b5d8e2; --accent2-ink:#bcd7d6;
  }
  body[data-theme="dracula"] {
    --bg:#282a36; --surface:#2f3240; --surface2:#383b4a; --border:#44475a;
    --text:#f8f8f2; --muted:#989dae; --accent:#bd93f9; --accent2:#ff79c6;
    --green:#50fa7b; --green-dim:#1d5a2e; --red:#ff5555; --red-dim:#6e2222;
    --red-on-tint:#ffb8b4;
    --amber:#f1fa8c; --purple:#bd93f9;
    --accent-soft:rgba(189,147,249,0.14); --accent-edge:rgba(189,147,249,0.32);
    --accent-contrast:#000;
    --accent-ink:#ceaefb; --accent2-ink:#ff9dd5;
  }
  body[data-theme="gruvbox"] {
    --bg:#1d2021; --surface:#282828; --surface2:#32302f; --border:#3c3836;
    --text:#ebdbb2; --muted:#a89984; --accent:#fabd2f; --accent2:#fe8019;
    --green:#b8bb26; --green-dim:#4f5320; --red:#fb4934; --red-dim:#6b2018;
    --amber:#fabd2f; --purple:#d3869b;
    --accent-soft:rgba(250,189,47,0.12); --accent-edge:rgba(250,189,47,0.32);
    --accent-contrast:#000;
    --accent-ink:#fabd2f; --accent2-ink:#fe9641;
  }
  body[data-theme="catppuccin"] {
    --bg:#1e1e2e; --surface:#24273a; --surface2:#313244; --border:#363a4f;
    --text:#cdd6f4; --muted:#9399b2; --accent:#cba6f7; --accent2:#f5c2e7;
    --green:#a6e3a1; --green-dim:#3e5a3c; --red:#f38ba8; --red-dim:#6b2f3e;
    --amber:#f9e2af; --purple:#cba6f7;
    --accent-soft:rgba(203,166,247,0.13); --accent-edge:rgba(203,166,247,0.30);
    --accent-contrast:#000;
    --accent-ink:#cba6f7; --accent2-ink:#f5c2e7;
  }
  body[data-theme="tokyo"] {
    --bg:#1a1b26; --surface:#1f2335; --surface2:#24283b; --border:#2f3549;
    --text:#c0caf5; --muted:#838bae; --accent:#7aa2f7; --accent2:#bb9af7;
    --green:#9ece6a; --green-dim:#3d5230; --red:#f7768e; --red-dim:#6b2733;
    --amber:#e0af68; --purple:#bb9af7;
    --accent-soft:rgba(122,162,247,0.13); --accent-edge:rgba(122,162,247,0.30);
    --accent-contrast:#000;
    --accent-ink:#7ba3f7; --accent2-ink:#bb9af7;
  }
  body[data-theme="rosepine"] {
    --bg:#191724; --surface:#1f1d2e; --surface2:#26233a; --border:#2a273f;
    --text:#e0def4; --muted:#908caa; --accent:#c4a7e7; --accent2:#ebbcba;
    --green:#9ccfd8; --green-dim:#2f4f55; --red:#eb6f92; --red-dim:#6b2a3c;
    --amber:#f6c177; --purple:#c4a7e7;
    --accent-soft:rgba(196,167,231,0.13); --accent-edge:rgba(196,167,231,0.30);
    --accent-contrast:#000;
    --accent-ink:#c4a7e7; --accent2-ink:#ebbcba;
  }
  body[data-theme="solarized"] {
    --bg:#002b36; --surface:#073642; --surface2:#0a4250; --border:#0f4b5a;
    --text:#b3c0c0; --muted:#899fa4; --accent:#268bd2; --accent2:#2aa198;
    --green:#859900; --green-dim:#3a4400; --red:#dc322f; --red-dim:#5e1716;
    --amber:#b58900; --purple:#6c71c4;
    --accent-soft:rgba(38,139,210,0.13); --accent-edge:rgba(38,139,210,0.32);
    --accent-contrast:#000;
    --accent-ink:#7ebce8; --accent2-ink:#36cbbf;
  }
  body[data-theme="oceanic"] {
    --bg:#0f1c24; --surface:#152733; --surface2:#1b313f; --border:#23404f;
    --text:#cde3ec; --muted:#78919d; --accent:#22b8cf; --accent2:#3ddbd9;
    --green:#37b67a; --green-dim:#1c4f39; --red:#e85d6f; --red-dim:#5e2630;
    --amber:#e0a458; --purple:#9d7cd8;
    --accent-soft:rgba(34,184,207,0.13); --accent-edge:rgba(34,184,207,0.32);
    --accent-contrast:#000;
    --accent-ink:#23bcd3; --accent2-ink:#3ddbd9;
  }
  body[data-theme="solarized-light"] {
    --bg:#fdf6e3; --surface:#fbf3d9; --surface2:#f3ead0; --border:#e6dcc0;
    --text:#3f5258; --muted:#606e6e; --accent:#268bd2; --accent2:#2aa198;
    --green:#6f7d00; --green-dim:#d3d9a0; --red:#dc322f; --red-dim:#f0c0bf;
    --amber:#a07700; --purple:#6c71c4;
    --accent-soft:rgba(38,139,210,0.10); --accent-edge:rgba(38,139,210,0.28);
    --accent-contrast:#000;
    --accent-ink:#1c6599; --accent2-ink:#1c6b65;
  }
  body[data-theme="nord-light"] {
    --bg:#eceff4; --surface:#ffffff; --surface2:#e5e9f0; --border:#d8dee9;
    --text:#2e3440; --muted:#6b7589; --accent:#5e81ac; --accent2:#81a1c1;
    --green:#4a7a2f; --green-dim:#c8d8b8; --red:#bf616a; --red-dim:#e8c4c8;
    --amber:#9a6f00; --purple:#9d7cb0;
    --accent-soft:rgba(94,129,172,0.10); --accent-edge:rgba(94,129,172,0.28);
    --accent-contrast:#000;
    --accent-ink:#456386; --accent2-ink:#416284;
  }
  body[data-theme="paper"] {
    --bg:#fafafa; --surface:#ffffff; --surface2:#f4f4f5; --border:#e4e4e7;
    --text:#18181b; --muted:#71717a; --accent:#2563eb; --accent2:#3b82f6;
    --green:#16a34a; --green-dim:#bbf7d0; --red:#dc2626; --red-dim:#fecaca;
    --amber:#d97706; --purple:#7c3aed;
    --accent-soft:rgba(37,99,235,0.08); --accent-edge:rgba(37,99,235,0.26);
    --accent-contrast:#fff;
    --accent-ink:#195aea; --accent2-ink:#0a5ce2;
  }
  /* ── v6.0.0 "ClarityMatters" design tokens (chosen-design.html) ──────────
     New tokens the Clarity re-skin adds on top of the existing palette:
       --hair / --hair-2  hairline borders / hover wash (alpha, so they sit on
                          any surface)
       --side / --side-ink  sidebar chrome (slightly offset from --bg/--text)
       --font-ui          the design's system sans stack. Scoped to migrated
                          surfaces during phase 1; becomes the body font when
                          the global frame surface migrates.
     Dark values are the defaults; body.light carries the light set; a named
     theme (data-theme) derives them from its own palette so every theme keeps
     a coherent sidebar. */
  :root {
    --hair: rgba(255,255,255,.09);
    --hair-2: rgba(255,255,255,.05);
    --side: #10151b;
    --side-ink: #aeb9c6;
    --font-ui: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
    /* v6.4.2: elevation. Every lifted surface — modal, drawer, dropdown,
       popover, toast — used to carry a literal rgba(0,0,0,.30..40) at 16-40px
       blur. On the dark themes those are invisible-by-design depth. On the four
       light themes (Daylight, Paper, Solarized Light, Nord Light) the SAME 32%
       black over a near-white page reads as a dirty grey wash rather than a
       lift. There was no --shadow token to fix it in one place and none of the
       body.light overrides touched box-shadow.
       -1 popovers/tooltips, -2 dropdowns/toasts, -3 modals/drawers. */
    --shadow-1: 0 4px 14px rgba(0,0,0,.28);
    --shadow-2: 0 8px 24px rgba(0,0,0,.32);
    --shadow-3: 0 12px 40px rgba(0,0,0,.36);
    --shadow-edge: 6px 0 30px rgba(0,0,0,.30);
  }
  body.light {
    --hair: rgba(18,28,40,.11);
    --hair-2: rgba(18,28,40,.06);
    --side: #f1f4f8;
    --side-ink: #3a4756;
    /* Much lower alpha, slightly tighter blur — on a light background a shadow
       reads as depth at a fraction of the opacity a dark one needs. */
    --shadow-1: 0 2px 8px rgba(18,28,40,.08);
    --shadow-2: 0 6px 16px rgba(18,28,40,.10);
    --shadow-3: 0 10px 28px rgba(18,28,40,.13);
    --shadow-edge: 4px 0 20px rgba(18,28,40,.11);
  }
  /* Any explicit theme: derive the Clarity chrome from the theme's own palette
     (unique properties — no clash with the per-theme blocks above). */
  body[data-theme] {
    --hair: var(--border);
    --hair-2: var(--surface2);
    --side: var(--bg);
    --side-ink: var(--text);
  }

  /* Theme-picker card preview swatches. */
  .theme-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(150px,1fr)); gap:10px; }
  .theme-card {
    border:1px solid var(--border); border-radius:10px; padding:10px;
    background:var(--surface2); cursor:pointer; text-align:left;
    color:var(--text); font: inherit; transition: border-color .15s, transform .1s;
  }
  .theme-card:hover { border-color:var(--accent); }
  .theme-card.sel { border-color:var(--accent); box-shadow:0 0 0 2px var(--accent-edge, var(--accent)); }
  .theme-card .tc-name { font-size:13px; font-weight:600; margin-bottom:8px; display:flex; align-items:center; justify-content:space-between; }
  .theme-card .tc-tag { font-size:10px; color:var(--muted); font-weight:500; text-transform:uppercase; letter-spacing:.4px; }
  .theme-card .tc-swatches { display:flex; gap:4px; }
  .theme-card .tc-dot { width:18px; height:18px; border-radius:5px; border:1px solid rgba(255,255,255,0.08); }

  /* v3.14.0 (#46): per-user accent presets — declared AFTER the data-theme
     blocks so a chosen accent overrides the theme's default accent. */
  /* WCAG AA text-on-accent (item #62): a chosen accent preset overrides
     --accent-contrast too -- all 5 presets clear 4.5:1 against black (the
     tightest, violet, is 4.96:1; white would fail all 5, worst 2.15:1). */
  body[data-accent="emerald"] { --accent: #10b981; --accent-soft: rgba(16,185,129,0.10); --accent-edge: rgba(16,185,129,0.30); --accent-contrast: #000; }
  body[data-accent="violet"]  { --accent: #8b5cf6; --accent-soft: rgba(139,92,246,0.10); --accent-edge: rgba(139,92,246,0.30); --accent-contrast: #000; }
  body[data-accent="amber"]   { --accent: #f59e0b; --accent-soft: rgba(245,158,11,0.10); --accent-edge: rgba(245,158,11,0.30); --accent-contrast: #000; }
  body[data-accent="rose"]    { --accent: #f43f5e; --accent-soft: rgba(244,63,94,0.10);  --accent-edge: rgba(244,63,94,0.30); --accent-contrast: #000; }
  body[data-accent="cyan"]    { --accent: #06b6d4; --accent-soft: rgba(6,182,212,0.10);  --accent-edge: rgba(6,182,212,0.30); --accent-contrast: #000; }

  /* v6.4.2 a11y: the accent-as-ink variant of each preset (see --accent-ink in
     :root). A preset replaces --accent on EVERY theme, so one value per preset
     has to clear 4.5:1 on the darkest and lightest palette of its family — the
     dark set is solved against Nord's #434c5e (our lightest dark surface), the
     light set against Nord Light's #e5e9f0 (our darkest light surface).
     `body.light[data-accent=…]` is (0,2,1) and so beats both the preset rules
     above it and the per-theme blocks; the dark rules are (0,1,1) but come
     after the data-theme blocks, which is how the preset wins there. */
  body[data-accent="emerald"] { --accent-ink: #14e5a0; }
  body[data-accent="violet"]  { --accent-ink: #cebafb; }
  body[data-accent="amber"]   { --accent-ink: #f9c265; }
  body[data-accent="rose"]    { --accent-ink: #faaebb; }
  body[data-accent="cyan"]    { --accent-ink: #34ddf9; }
  body.light[data-accent="emerald"] { --accent-ink: #0a704e; }
  body.light[data-accent="violet"]  { --accent-ink: #6b2ff3; }
  body.light[data-accent="amber"]   { --accent-ink: #8a5906; }
  body.light[data-accent="rose"]    { --accent-ink: #bf0b2a; }
  body.light[data-accent="cyan"]    { --accent-ink: #046c7e; }

  .theme-btn {
    background: transparent;
    border: 1px solid var(--border);
    color: var(--muted);
    padding: 6px 10px;
    border-radius: 6px;
    font-size: 16px;
    cursor: pointer;
    transition: background 0.2s, border-color 0.2s, color 0.2s, transform 0.2s;
    line-height: 1;
  }
  .theme-btn:hover { border-color: var(--accent); color: var(--accent-ink); }
  /* v6: topbar controls are flat hairline buttons (chosen-design language) */
  header .btn-icon, header .theme-btn {
    background: none;
    border: 1px solid transparent;
    border-radius: 6px;
    color: var(--muted);
    box-shadow: none;
  }
  header .btn-icon:hover, header .theme-btn:hover {
    background: var(--hair-2);
    border-color: transparent;
    color: var(--text);
  }
  .update-banner {
    background: rgba(245,158,11,0.08);
    border-bottom: 1px solid rgba(245,158,11,0.25);
    padding: 10px 24px;
    font-size: 13px;
    color: var(--amber);
    text-align: center;
    position: relative;
    z-index: 99;
  }
  .update-banner a { color: var(--amber); font-weight: 600; text-decoration: underline; }
  .update-banner .update-steps-btn {
    background: transparent;
    border: 1px solid rgba(245,158,11,0.4);
    color: var(--amber);
    font: inherit; font-size: 12px;
    padding: 2px 10px; margin-left: 10px;
    cursor: pointer; border-radius: 4px;
  }
  .update-banner .update-steps-btn:hover { background: rgba(245,158,11,0.12); }
  .update-banner #update-steps {
    max-width: 560px; margin: 8px auto 0; text-align: left;
  }
  .update-banner #update-steps code {
    display: block; background: rgba(0,0,0,0.35);
    border: 1px solid rgba(245,158,11,0.2);
    padding: 8px 12px; border-radius: 4px;
    font-family: var(--font-mono, monospace); font-size: 12px;
    color: var(--text); white-space: pre-wrap; word-break: break-all;
  }
  .tag-pill {
    display: inline-block;
    padding: 2px 8px;
    border-radius: 20px;
    font-size: 11px;
    font-weight: 500;
    background: rgba(59,126,255,0.1);
    color: var(--accent-ink);
    border: 1px solid rgba(59,126,255,0.2);
    margin-right: 4px;
    margin-top: 4px;
  }
  .btn-update {
    background: rgba(34,197,94,0.1);
    border: 1px solid rgba(34,197,94,0.3);
    color: var(--green);
    padding: 9px 12px;
    border-radius: 8px;
    font-size: 13px;
    font-weight: 500;
    cursor: pointer;
    transition: background 0.2s, border-color 0.2s, color 0.2s, transform 0.2s;
    display: flex;
    align-items: center;
    gap: 5px;
    font-family: var(--font);
  }
  .btn-update:hover { background: rgba(34,197,94,0.2); border-color: var(--green); }
  .btn-update svg { width: 14px; height: 14px; }
  .btn-upgrade {
    background: rgba(124,58,237,0.1);
    border: 1px solid rgba(124,58,237,0.3);
    color: #a78bfa;
    padding: 9px 12px;
    border-radius: 8px;
    font-size: 13px;
    font-weight: 500;
    cursor: pointer;
    transition: background 0.2s, border-color 0.2s, color 0.2s, transform 0.2s;
    display: flex;
    align-items: center;
    gap: 5px;
    font-family: var(--font);
  }
  .btn-upgrade:hover { background: rgba(124,58,237,0.2); border-color: var(--purple); }
  .btn-upgrade svg { width: 14px; height: 14px; }
  .btn-shutdown {
    background: rgba(239,68,68,0.1);
    border: 1px solid rgba(239,68,68,0.3);
    color: var(--red);
    padding: 9px 12px;
    border-radius: 8px;
    font-size: 13px;
    font-weight: 500;
    cursor: pointer;
    transition: background 0.2s, border-color 0.2s, color 0.2s, transform 0.2s;
    display: flex;
    align-items: center;
    gap: 5px;
    font-family: var(--font);
  }
  .btn-shutdown:hover { background: rgba(239,68,68,0.2); border-color: var(--red); }
  .btn-shutdown svg { width: 14px; height: 14px; }
  .btn-reboot {
    background: rgba(245,158,11,0.1);
    border: 1px solid rgba(245,158,11,0.3);
    color: var(--amber);
    padding: 9px 12px;
    border-radius: 8px;
    font-size: 13px;
    font-weight: 500;
    cursor: pointer;
    transition: background 0.2s, border-color 0.2s, color 0.2s, transform 0.2s;
    display: flex;
    align-items: center;
    gap: 5px;
    font-family: var(--font);
  }
  .btn-reboot:hover { background: rgba(245,158,11,0.2); border-color: var(--amber); }
  .btn-reboot svg { width: 14px; height: 14px; }
  .cmd-badge {
    display: inline-block; padding: 2px 8px; border-radius: 4px;
    font-size: 11px; font-weight: 600; font-family: var(--font-mono);
  }
  .cmd-badge.shutdown           { background: rgba(239,68,68,0.1); color: var(--red-on-tint); }
  .cmd-badge.reboot             { background: rgba(245,158,11,0.1); color: var(--amber); }
  .cmd-badge.update             { background: rgba(34,197,94,0.1); color: var(--green); }
  .cmd-badge.wol                { background: rgba(124,58,237,0.1); color: #a78bfa; }
  .cmd-badge.upgrade_packages   { background: rgba(34,197,94,0.1); color: var(--green); }
  .cmd-badge.upgrade_and_reboot { background: rgba(245,158,11,0.1); color: var(--amber); }
  /* v6: flat hairline chip (was a rounded purple "bubbly" pill). */
  .group-badge {
    display: inline-flex; align-items: center; gap: 4px;
    padding: 2px 8px; border-radius: 6px; font-size: 11px;
    font-weight: 500; background: var(--hair-2); color: var(--text);
    border: 1px solid var(--hair); margin-right: 4px; margin-top: 4px;
  }
  .notes-tip {
    display: inline-block;
    font-size: 12px;
    margin-left: 6px;
    cursor: pointer;
    opacity: 0.6;
    vertical-align: middle;
    transition: opacity 0.15s, transform 0.15s;
    position: relative;
  }
  .notes-tip:hover { opacity: 1; transform: scale(1.2); }
  .notes-tip::after {
    content: attr(title);
    position: absolute;
    bottom: calc(100% + 6px);
    left: 50%;
    transform: translateX(-50%);
    background: var(--surface2);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 6px 10px;
    font-size: 12px;
    font-family: var(--font);
    color: var(--text);
    white-space: pre-wrap;
    max-width: 260px;
    width: max-content;
    box-shadow: var(--shadow-1);
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.15s;
    z-index: 50;
    word-break: break-word;
  }
  .notes-tip:hover::after { opacity: 1; }
  .form-textarea {
    width: 100%; background: var(--bg); border: 1px solid var(--border); border-radius: 8px;
    padding: 11px 14px; font-size: 13px; color: var(--text); outline: none;
    transition: border-color 0.2s; font-family: var(--font); resize: vertical; min-height: 80px;
  }
  .form-textarea:focus { border-color: var(--accent); }
  .apikey-value {
    font-family: var(--font-mono); font-size: 12px; background: var(--bg); border: 1px solid var(--border);
    border-radius: 6px; padding: 8px 12px; word-break: break-all; color: var(--green);
  }
  .batch-bar {
    display: none; background: rgba(59,126,255,0.08); border: 1px solid rgba(59,126,255,0.2);
    border-radius: 10px; padding: 12px 16px; margin-bottom: 16px;
    align-items: center; gap: 12px; flex-wrap: wrap;
  }
  .batch-bar.visible { display: flex; }
  .missed-badge {
    display: inline-flex; align-items: center; gap: 4px;
    padding: 2px 7px; border-radius: 10px; font-size: 10px; font-weight: 600;
    background: rgba(245,158,11,0.1); color: var(--amber); border: 1px solid rgba(245,158,11,0.2);
    margin-left: 4px;
  }
  * { box-sizing: border-box; margin: 0; padding: 0; }
  body {
    background: var(--bg);
    color: var(--text);
    font-family: var(--font);
    min-height: 100vh;
    overflow-x: hidden;
  }
  /* (v6: the always-on decorative grid (body::before) + ambient glow
     (body::after) are gone — the design ground is plain var(--bg). The
     opt-in body[data-bg] patterns still work; .container keeps its
     z-index:1 — see the stacking-context note there.) */
  .container {
    /* v2.2.5: 1100 → 1300. The dashboard's data-density grew with the
     * minimal devices table + Home tile grid + drift fleet table; 1100
     * forced too much horizontal cramping on standard 1920-wide
     * monitors. 1300 fits 4 dashboard tiles comfortably and still
     * leaves margin on FHD.
     * v4.1.0: 1300 → 1400 — reclaim ~100px of the wide right-hand gutter
     * so boxes/tables breathe across the whole UI; still leaves margin on
     * 1920 and the responsive caps below handle narrower viewports. */
    max-width: 1400px;
    margin: 0 auto;
    padding: 0 24px;
    position: relative;
    z-index: 1;
  }
  /* ── v6.0.0 "ClarityMatters" topbar (chosen-design .topbar): spans only
     the MAIN column (the full-height sidebar owns the left edge), 48px,
     hairline bottom, breadcrumb left + health/controls right. ── */
  header {
    border-bottom: 1px solid var(--hair);
    padding: 0;
    position: sticky;
    top: 0;
    /* v4.3.0 perf: opaque var(--bg), NO backdrop-filter. The old
       rgba(...,0.85)+blur(12px) forced Firefox to re-blur the page under
       this always-visible sticky bar on every scroll/repaint frame (Chrome
       caches the layer; Firefox doesn't) — a top contributor to "sluggish
       in Firefox". Don't reintroduce backdrop-filter here. */
    background: var(--bg);
    z-index: 100;
    /* lockstep with .sidebar width (56px collapsed, 0 on the mobile drawer) */
    margin-left: 248px;
  }
  .header-inner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    max-width: 100%;
    padding: 0 18px;
    height: 48px;
  }
  /* topbar breadcrumb — "Group › Page", painted by _paintCrumb */
  .crumb {
    font-family: var(--font-ui);
    font-size: 12px;
    color: var(--muted);
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .crumb b { color: var(--text); font-weight: 560; }
  .logo {
    display: flex;
    align-items: center;
    gap: 12px;
    flex-shrink: 0;
  }
  /* v2.0: header logo as a clickable <a>. Reset link styles so it
     doesn't pick up underline/blue from the global anchor rules. */
  .logo-link { text-decoration: none; color: inherit; cursor: pointer; }
  .logo-link:hover { opacity: 0.85; }
  .logo-img {
    width: 36px;
    height: 36px;
    border-radius: 8px;
    display: block;
    /* The PNG is square 1:1 — fit it cleanly into the 36px slot. */
    object-fit: contain;
  }
  .logo-icon {
    width: 36px;
    height: 36px;
    background: linear-gradient(135deg, var(--accent), #7c3aed);
    border-radius: 8px;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .logo-icon svg { width: 20px; height: 20px; }
  .logo-text { font-size: 16px; font-weight: 700; letter-spacing: -0.5px; }
  .logo-text span { color: var(--accent-ink); }
  /* v6: the brand lives in the sidebar (chosen-design .brand) */
  .sidebar .brand { display: flex; align-items: center; gap: 9px; padding: 6px 8px 10px; }
  .sidebar .brand .logo-img { width: 24px; height: 24px; border-radius: 6px; }
  .sidebar .brand .logo-text { font-size: 13px; color: var(--text); letter-spacing: -0.01em; }
  .brand-v {
    margin-left: auto; font-family: var(--font-mono); font-size: 9.5px;
    color: var(--muted); border: 1px solid var(--hair); padding: 2px 6px;
    border-radius: 5px; white-space: nowrap;
  }
  .brand-v:empty { display: none; }
  .header-right {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-shrink: 0;
  }
  /* A 390px phone shares the bar with the menu button and the breadcrumb. The
     control cluster is six 46px buttons (seven when quiet hours are on) and a
     10px gap, 320-376px, so the whole document scrolled sideways on every page.
     Tighter padding and gaps bring it to about 250px. */
  @media (max-width: 480px) {
    .header-inner { padding: 0 10px; gap: 8px; }
    .header-right { gap: 2px; }
    header .btn-icon, header .theme-btn { padding-left: 8px; padding-right: 8px; }
  }
  .status-bar {
    font-size: 12px;
    color: var(--muted);
    display: flex;
    align-items: center;
    gap: 6px;
  }
  .status-dot {
    display: inline-block; /* so width/height apply on <span> uses (devices table), not just <div> */
    width: 6px; height: 6px;
    border-radius: 50%;
    background: var(--green);
    /* v5.6.x perf: NO infinite pulse. There is one status dot per ONLINE device;
       Chrome promotes every element running a compositable animation to its own
       layer and must produce a frame ~every 16ms while any run — so N online dots
       = N always-animating layers keeping the compositor busy at idle (Layerize
       storm + ~20% idle GPU in a trace), starving scroll. The green colour already
       conveys "online"; liveness is shown by the last-seen timestamp instead. */
  }
  /* v3.14.0 perf: opacity-only pulse. Animating box-shadow forced a full-page
     repaint EVERY frame for every online status dot — a continuous paint storm
     (profiled at ~10s of Paint over a 20s idle capture). opacity is
     GPU-composited, so the pulse no longer triggers paint/raster/layout. */
  @keyframes pulse-green {
    0%,100% { opacity: 1; }
    50%     { opacity: 0.45; }
  }
  @media (prefers-reduced-motion: reduce) {
    .status-badge-dot { animation: none; }
    /* v6.3.0 (UX wave 10): every informative-motion addition goes still */
    .page.active, .status-dot, tr.alert-crit, tr.row-flash,
    tbody.first-paint tr { animation: none; }
    .rp-ring .rp-ring-fg { transition: none; }
  }
  /* ── v6.0.0 "ClarityMatters" sidebar (chosen-design.html .side/.nav) ────
     Flat rail: --side ground, hairline right edge, sentence-case group
     headers, accent-soft active row with a short accent bar. This REPLACED
     the Industrial machined-nav overrides (deleted, not layered over). */
  .sidebar {
    position: fixed;
    left: 0;
    top: 0;
    bottom: 0;
    /* v3.2.0: 200 → 220; v4.7.0: 220 → 240 (longest labels un-ellipsised);
       v6.0.0: 240 → 248 = the chosen-design rail. Keep .app-content in step. */
    width: 248px;
    background: var(--side);
    border-right: 1px solid var(--hair);
    color: var(--side-ink);
    font-family: var(--font-ui);
    padding: 10px 8px 0;
    z-index: 90;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 1px;
    transition: padding-top .2s ease;
  }
  .sidebar-label {
    /* design-language eyebrow (chosen-design .h): tiny mono small-caps */
    font-family: var(--font-mono);
    font-size: 9.5px;
    font-weight: 400;
    text-transform: uppercase;
    letter-spacing: .14em;
    color: var(--muted);
    padding: 8px 10px 5px;
  }
  .nav-btn {
    background: transparent;
    border: none;
    color: var(--side-ink);
    padding: 5px 9px;
    border-radius: 6px;
    font-size: 13px;
    cursor: pointer;
    transition: background 0.15s, border-color 0.15s, color 0.15s;
    font-family: inherit;
    white-space: nowrap;
    text-align: left;
    width: 100%;
    display: flex;
    align-items: center;
    gap: 9px;
  }
  .nav-btn:hover { background: var(--hair-2); }
  .nav-btn.active {
    background: var(--accent-soft);
    color: var(--text);
    font-weight: 560;
    position: relative;
  }
  /* short accent bar on the active row's left (chosen-design .pg.on::before) */
  .nav-btn.active::before {
    content: "";
    position: absolute;
    left: -9px;
    top: 6px;
    bottom: 6px;
    width: 2.5px;
    background: var(--accent);
    border-radius: 2px;
  }
  .nav-btn svg { width: 16px; height: 16px; flex-shrink: 0; }

  /* v3.4.2: sidebar search box + results dropdown (v6: chosen-design .search) */
  .sidebar-search-wrap { position: relative; padding: 2px 4px 8px; }
  .sidebar-search {
    width: 100%;
    background: var(--surface);
    border: 1px solid var(--hair);
    color: var(--text);
    padding: 7px 10px;
    border-radius: 8px;
    font-size: 12px;
    font-family: inherit;
    box-sizing: border-box;
  }
  .sidebar-search::placeholder { color: var(--muted); }
  .sidebar-search:focus { outline: none; border-color: var(--accent); }
  .sidebar-search-results {
    position: absolute; left: 4px; right: 4px; top: 100%;
    background: var(--surface);
    border: 1px solid var(--hair);
    border-radius: 8px;
    margin-top: 4px;
    padding: 4px;
    z-index: 60;
    box-shadow: var(--shadow-2);
    max-height: 320px;
    overflow-y: auto;
  }
  .sidebar-search-hit {
    display: block; width: 100%; text-align: left;
    background: transparent; border: none; color: var(--text);
    padding: 7px 10px; border-radius: 6px;
    font-size: 13px; font-family: inherit; cursor: pointer;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .sidebar-search-hit:hover, .sidebar-search-hit.active { background: var(--hair-2); }
  .sidebar-search-empty { color: var(--muted); font-size: 12px; padding: 7px 10px; }
  /* No room for the box when the sidebar is collapsed to icons. */
  body.sidebar-collapsed .sidebar-search-wrap { display: none; }

  /* v2.0: collapsible sidebar groups. Trades a flat 18-item list for
     a more browsable 5–6 item top level + grouped sub-items. The toggle
     button styles like the section labels (small caps, muted) when
     collapsed and like a regular nav-btn when active so it's clear it
     does something. Chevron rotates to indicate state. State persists
     in localStorage (sidebar.<group>.expanded). */
  /* v6: domain header rows (chosen-design .dhdr) — sentence case, icon left,
     chevron right; the whole row is the hover/click target. */
  .sidebar-group { display: flex; flex-direction: column; gap: 1px; margin-top: 1px; }
  .sidebar-group-toggle {
    background: transparent;
    border: none;
    color: var(--side-ink);
    padding: 7px 9px;
    font-size: 12px;
    font-weight: 560;
    text-transform: none;
    letter-spacing: normal;
    cursor: pointer;
    text-align: left;
    width: 100%;
    display: flex;
    align-items: center;
    gap: 9px;
    font-family: inherit;
    border-radius: 7px;
  }
  .sidebar-group-toggle:hover { background: var(--hair-2); color: var(--side-ink); }
  .sidebar-group-toggle svg { width: 16px; height: 16px; flex-shrink: 0; color: var(--muted); }
  /* Grey fold-out (disclosure) chevron between a parent group and its items. */
  .sidebar-group-toggle .chevron {
    margin-left: auto;
    transition: transform 0.18s ease;
    width: 12px; height: 12px;
    color: var(--muted);
    opacity: 0.7;
  }
  .sidebar-group-toggle:hover .chevron { opacity: 1; }
  .sidebar-group.collapsed .chevron { transform: rotate(-90deg); }
  /* v4.1.0 → v6: the icon tints (.nav-alert/.nav-warn > svg) are gone — the
     flat design keeps icons neutral and colours the count text instead (JS
     still sets the classes; they're just unstyled now). */
  /* Per-group "needs attention" count (offline / monitors-down / CVEs) —
     chosen-design .n pill: hairline outline, mono digits; red text because
     ours is an attention count, not a page count. Shown only when > 0. */
  .sidebar-group-toggle .nav-group-badge {
    margin-left: auto;
    margin-right: 6px;
    min-width: 16px; height: 16px; line-height: 15px;
    padding: 0 6px; border-radius: 999px;
    font-family: var(--font-mono); font-variant-numeric: tabular-nums;
    font-size: 9.5px; font-weight: 400; text-align: center;
    background: none; color: var(--red);
    border: 1px solid var(--hair);
  }
  .sidebar-group-items {
    display: flex;
    flex-direction: column;
    gap: 1px;                 /* tighter — subcategories sit closer together */
    overflow: hidden;
    max-height: 1000px;
    transition: max-height 0.18s ease-out;
    /* Thin rail (chosen-design .kids): a 1px hairline runs down the left of
       the children, joining a parent group to its items. The children indent
       past it; collapsing hides it with the rest. */
    margin-left: 9px;
    padding: 2px 0 6px 8px;
    border-left: 1px solid var(--hair);
  }
  .sidebar-group.collapsed .sidebar-group-items {
    max-height: 0;
    padding-top: 0;
    padding-bottom: 0;
    border-left-color: transparent;
  }
  .sidebar-group .nav-btn { padding: 5px 9px; font-size: 13px; }
  .sidebar-group .nav-btn svg { width: 14px; height: 14px; }
  /* The active row's accent bar (.nav-btn.active::before, -9px) lands exactly
     on the group rail — no extra rules needed (the old inset box-shadow +
     margin-left:-1px hack is gone). */

  /* v4.1.0: dashboard title row with a small inline Customize button. */
  .page-title-row { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
  .page-title-row + .page-subtitle { margin-top: -13px; }
  .page-title-row .page-title { margin: 0; }
  .dash-customize-btn { margin-left: auto; flex: none; font-size: 12px; padding: 4px 10px; }

  /* ── v4.1.0: collapse/hide the top bar ────────────────────────────────── */
  body.banner-collapsed header { display: none; }
  /* (v6: the sidebar is full-height with its own brand row — hiding the
     topbar no longer affects it.) */
  #banner-restore {
    position: fixed; top: 8px; right: 12px; z-index: 200;
    width: 30px; height: 30px; display: none; align-items: center;
    justify-content: center; border-radius: 8px; cursor: pointer;
    background: var(--surface); border: 1px solid var(--border); color: var(--muted);
    box-shadow: var(--shadow-1);
  }
  #banner-restore:hover { color: var(--text); }
  body.banner-collapsed #banner-restore { display: inline-flex; }

  /* ── v4.1.0: decorative backgrounds (body[data-bg]); subtle, theme-aware,
     pure-CSS so they're CSP-safe. They sit behind the surface cards. ────── */
  body[data-bg] { background-attachment: fixed; }
  /* When a custom background is chosen, suppress the always-on default
     grid (body::before) and ambient top glow (body::after) so the chosen
     pattern stands alone instead of layering over a second grid. */
  body[data-bg]::before, body[data-bg]::after { background: none; }
  body[data-bg="grid"] {
    background-image:
      linear-gradient(var(--border) 1px, transparent 1px),
      linear-gradient(90deg, var(--border) 1px, transparent 1px);
    background-size: 34px 34px; background-position: -1px -1px;
  }
  body[data-bg="dots"] {
    background-image: radial-gradient(var(--border) 1.2px, transparent 1.4px);
    background-size: 22px 22px;
  }
  body[data-bg="gradient"] {
    background-image:
      radial-gradient(900px 520px at 12% -8%, rgba(99,102,241,0.16), transparent 60%),
      radial-gradient(820px 520px at 100% 8%, rgba(16,185,129,0.12), transparent 60%),
      radial-gradient(700px 600px at 60% 100%, rgba(236,72,153,0.10), transparent 60%);
  }
  body[data-bg="mesh"] {
    background-image:
      radial-gradient(380px 380px at 18% 22%, rgba(74,158,255,0.12), transparent 70%),
      radial-gradient(420px 420px at 82% 30%, rgba(168,85,247,0.10), transparent 70%),
      radial-gradient(360px 360px at 50% 88%, rgba(34,197,94,0.10), transparent 70%);
  }
  body[data-bg="topo"] {
    background-image:
      repeating-radial-gradient(circle at 30% 18%, transparent 0 26px, var(--border) 26px 27px);
    opacity: 1;
  }
  body[data-bg="topo"]::before { content: none; }
  body[data-bg="graph"] {
    background-image:
      linear-gradient(var(--border) 1px, transparent 1px),
      linear-gradient(90deg, var(--border) 1px, transparent 1px),
      linear-gradient(var(--border) 1px, transparent 1px),
      linear-gradient(90deg, var(--border) 1px, transparent 1px);
    background-size: 110px 110px, 110px 110px, 22px 22px, 22px 22px;
    background-position: -1px -1px;
  }
  body[data-bg="diagonal"] {
    background-image: repeating-linear-gradient(45deg, var(--border) 0 1px, transparent 1px 17px);
  }
  body[data-bg="crosshatch"] {
    background-image:
      repeating-linear-gradient(45deg, var(--border) 0 1px, transparent 1px 19px),
      repeating-linear-gradient(-45deg, var(--border) 0 1px, transparent 1px 19px);
  }
  body[data-bg="circuit"] {
    background-image:
      linear-gradient(var(--border) 1px, transparent 1px),
      linear-gradient(90deg, var(--border) 1px, transparent 1px),
      radial-gradient(rgba(74,158,255,0.22) 2px, transparent 2.6px);
    background-size: 42px 42px;
    background-position: -1px -1px, -1px -1px, 0 0;
  }
  body[data-bg="blueprint"] {
    background-image:
      linear-gradient(rgba(74,158,255,0.20) 1px, transparent 1px),
      linear-gradient(90deg, rgba(74,158,255,0.20) 1px, transparent 1px);
    background-size: 30px 30px; background-position: -1px -1px;
  }
  body[data-bg="glow"] {
    /* A single stage spotlight from top-centre + a darkening edge vignette —
       unlike Mesh's scattered multi-colour blobs. */
    background-image:
      radial-gradient(900px 600px at 50% 0%, rgba(74,158,255,0.18), transparent 55%),
      radial-gradient(140% 125% at 50% 32%, transparent 52%, rgba(0,0,0,0.30));
  }
  /* background picker swatches reuse .theme-card; give each a mini preview */
  .bg-card { min-height: 46px; }
  .bg-prev-none      { background: var(--surface2); }
  .bg-prev-grid      { background-image: linear-gradient(var(--border) 1px,transparent 1px),linear-gradient(90deg,var(--border) 1px,transparent 1px); background-size: 10px 10px; }
  .bg-prev-dots      { background-image: radial-gradient(var(--border) 1px,transparent 1.3px); background-size: 8px 8px; }
  .bg-prev-gradient  { background-image: radial-gradient(40px 30px at 20% 0,rgba(99,102,241,0.5),transparent),radial-gradient(40px 30px at 100% 30%,rgba(16,185,129,0.4),transparent); }
  .bg-prev-mesh      { background-image: radial-gradient(20px 20px at 25% 30%,rgba(74,158,255,0.6),transparent),radial-gradient(22px 22px at 80% 60%,rgba(168,85,247,0.5),transparent); }
  .bg-prev-topo      { background-image: repeating-radial-gradient(circle at 40% 30%,transparent 0 7px,var(--border) 7px 8px); }
  .bg-prev-graph     { background-image: linear-gradient(var(--border) 1px,transparent 1px),linear-gradient(90deg,var(--border) 1px,transparent 1px); background-size: 14px 14px; }
  .bg-prev-diagonal  { background-image: repeating-linear-gradient(45deg,var(--border) 0 1px,transparent 1px 7px); }
  .bg-prev-crosshatch{ background-image: repeating-linear-gradient(45deg,var(--border) 0 1px,transparent 1px 8px),repeating-linear-gradient(-45deg,var(--border) 0 1px,transparent 1px 8px); }
  .bg-prev-circuit   { background-image: linear-gradient(var(--border) 1px,transparent 1px),linear-gradient(90deg,var(--border) 1px,transparent 1px),radial-gradient(rgba(74,158,255,0.6) 1.4px,transparent 1.8px); background-size: 12px 12px; }
  .bg-prev-blueprint { background-image: linear-gradient(rgba(74,158,255,0.5) 1px,transparent 1px),linear-gradient(90deg,rgba(74,158,255,0.5) 1px,transparent 1px); background-size: 9px 9px; }
  .bg-prev-glow      { background-image: radial-gradient(36px 26px at 50% 0,rgba(74,158,255,0.85),transparent 70%),radial-gradient(130% 120% at 50% 40%,transparent 45%,rgba(0,0,0,0.45)); }
  /* (v6.0.0: the old "sidebar eyecandy" — inset accent bar, accent-tinted
     active icons, group dividers, hover slide — is gone; the Clarity sidebar
     block near the top of this file owns the whole look now.) */

  /* v2.0: documentation page card styling. <details> elements give us
     free expand/collapse without any JS, and they're right-click /
     Find-in-page friendly. The summary uses a custom marker (chevron)
     to match the sidebar group toggles for visual consistency. */
  .doc-card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    overflow: hidden;
    transition: border-color 0.15s;
  }
  .doc-card[open] { border-color: var(--accent); }
  /* v6.4.3: NOT display:flex. The flex+gap was there to sit the ▸ marker
     beside the text, and it works for a one-word summary — but every one of
     these 89 summaries is PROSE containing <strong>/<em>/<code>, and a flex
     container turns each inline element and each text run between them into
     its OWN flex item. The result was the summary shattered into narrow
     vertical columns with 10px gutters, worst on the longest one (the
     "What's new" card). Block layout lets the prose flow; the marker is
     positioned out of flow, which also puts it on the FIRST LINE of a
     multi-line summary instead of vertically centred against the whole
     paragraph. padding-left matches .doc-body's 32px so the summary text and
     the body text share a left edge. */
  .doc-card summary {
    padding: 14px 18px 14px 32px;
    position: relative;
    cursor: pointer;
    list-style: none;
    font-size: 13px;
    color: var(--text);
    display: block;
    user-select: none;
  }
  .doc-card summary::-webkit-details-marker { display: none; }
  .doc-card summary::before {
    content: '▸';
    color: var(--muted);
    font-size: 12px;
    transition: transform 0.15s;
    position: absolute;
    left: 18px;
    top: 14px;
  }
  .doc-card[open] summary::before { transform: rotate(90deg); }
  .doc-card summary:hover { background: var(--surface2); }
  .doc-card .doc-body {
    padding: 4px 18px 18px 32px;
    color: var(--text);
    line-height: 1.65;
    font-size: 13px;
  }
  .doc-card .doc-body p { margin: 8px 0; }
  .doc-card .doc-body code {
    background: var(--surface2);
    padding: 2px 6px;
    border-radius: 3px;
    font-size: 12px;
  }
  .doc-card .doc-body pre {
    background: var(--surface2);
    padding: 10px 14px;
    border-radius: 6px;
    overflow-x: auto;
    margin: 10px 0;
    font-size: 12px;
    line-height: 1.5;
  }
  .doc-card .doc-body pre code { background: none; padding: 0; }
  .doc-card .doc-body table {
    border-collapse: collapse;
    margin: 10px 0;
    font-size: 13px;
  }
  .doc-card .doc-body th,
  .doc-card .doc-body td {
    border: 1px solid var(--border);
    padding: 4px 12px;
    text-align: left;
  }
  .doc-card .doc-body th {
    background: var(--surface2);
    font-weight: 600;
  }
  .doc-card.hidden { display: none; }
  /* Global hide utility. Toggled from JS via classList for the sidebar-search
     results dropdown, the SLA-targets editor, etc. (Previously only
     `.doc-card.hidden` existed, so a bare `class="hidden"` element rendered as
     an empty box instead of being hidden.) */
  .hidden { display: none !important; }
  /* v3.12.0: storage-backend card (Settings → Advanced). Class-based styling so
     no inline style="" attributes are needed (CSP style-src 'self'). */
  .sb-controls { display: flex; gap: 8px; align-items: center; margin-top: 10px; flex-wrap: wrap; }
  .sb-warn { color: #c2410c; margin-top: 4px; }
  .sb-result { white-space: pre-wrap; margin-top: 10px; max-height: 240px; overflow: auto; }
  .retention-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 10px; margin-top: 10px; }
  .retention-grid label { display: flex; flex-direction: column; gap: 4px; font-size: 13px; }
  .retention-grid .form-input { width: 100%; }
  .retention-controls { display: flex; gap: 8px; align-items: center; margin-top: 12px; flex-wrap: wrap; }
  .exposure-banner { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
  .exposure-banner .exposure-banner-text { flex: 1 1 280px; }
  .exposure-banner .btn-icon { flex: 0 0 auto; white-space: nowrap; }
  /* Section-style cards that hold a heading + form/content (not a bare table)
     need inner padding so text/buttons don't butt against the rounded border. */
  .table-card.card-padded { padding: 12px 16px 16px; }
  .table-card.card-padded > .page-subtitle:first-child { margin-top: 0; }
  .settings-search-hint { font-size: 12px; color: var(--muted); margin: 8px 0 10px; }
  .role-perm-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 6px 14px; }
  .role-perm-grid .click-row-6 { white-space: nowrap; }
  .exposure-actions { display: flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
  .row-hidden { display: none !important; }
  /* Match .btn-icon metrics exactly (padding 8px 12px, 13px, 1px border, 8px
     radius) so a toolbar filter box is the SAME height as the buttons beside it
     and centres cleanly. input.<class> outranks the later generic .form-input. */
  input.page-filter {
    height: 34px; padding: 0 12px; font-size: 13px; width: auto; max-width: 320px;
    flex: 0 1 280px; margin: 0; box-sizing: border-box;
  }
  /* standalone filter boxes (above a list, not in a button row) keep a gap
     below — the margin:0 above would otherwise swallow the mb-8 utility. */
  input.page-filter.mb-8 { margin-bottom: 10px; display: block; }
  /* standalone placements (above a list, not in a button row) add their own
     bottom gap via the mb-8 utility */
  input.select-filter {
    height: 34px; padding: 0 12px; font-size: 13px; width: 100%; max-width: 360px;
    margin: 0 0 4px; box-sizing: border-box;
  }
  /* v5.1.1: the type-to-filter box must NEVER render accent-on-accent (blue text
   * on a blue background). Pin a solid, readable surface + text in every state and
   * skin — !important to beat the industrial `.form-input{background:well-bg!important}`
   * and any browser focus/autofill default. -webkit-text-fill-color also forces the
   * typed-text colour through Chrome's autofill overlay. */
  input.select-filter,
  input.select-filter:focus {
    background: var(--bg) !important;
    color: var(--text) !important;
    -webkit-text-fill-color: var(--text);
    caret-color: var(--accent);
  }
  input.select-filter:-webkit-autofill {
    -webkit-box-shadow: 0 0 0 1000px var(--bg) inset !important;
    -webkit-text-fill-color: var(--text) !important;
  }
  /* v5.6.0: force bright, readable text in the Check-catalog inputs/select in
     every state + skin (some industrial/focus combos dimmed the typed text). */
  #custom-checks-modal input,
  #custom-checks-modal input:focus,
  #custom-checks-modal select,
  #custom-checks-modal option,
  #custom-checks-modal optgroup,
  #custom-checks-modal textarea {
    color: var(--text) !important;
    -webkit-text-fill-color: var(--text) !important;
    caret-color: var(--accent);
  }
  /* Sticky-header scroll for long routing / per-event tables: the column
     header row stays pinned while the body scrolls, so Kind / Needs Attention /
     Recent Activity / Alerts / Webhook stay readable no matter the scroll. */
  /* v4.3.0 perf: contain:paint on the table scroll wrappers isolates their
   * repaints (sticky thead + row churn) from the rest of the page — Firefox
   * otherwise widens invalidation past the scroller. Visually a no-op: these
   * wrappers already clip via overflow:auto. */
  .sticky-head-scroll { max-height: 460px; overflow: auto; background: var(--surface); border: 1px solid var(--border); border-radius: 8px; contain: paint; }
  .sticky-head-scroll > table { width: 100%; border-collapse: collapse; margin: 0; }
  .sticky-head-scroll thead th { position: sticky; top: 0; z-index: 3; background: var(--surface); box-shadow: inset 0 -1px 0 var(--border); }
  .table-card.sticky-head-scroll { overflow: auto; }
  /* CMDB asset modal: fixed dialog height so switching tabs (Properties /
     Documentation / Credentials / Hardware / Contracts & contacts / SNMP)
     doesn't resize and re-centre the window. The active pane scrolls inside. */
  #cmdb-asset-modal > .modal { height: min(88vh, 760px); max-width: var(--modal-w-wide); display: flex; flex-direction: column; overflow: hidden; }
  /* Ticket detail modal — same proportions as the CMDB asset modal: fixed
     window, header + footer pinned, the body scrolls inside. */
  #ticket-detail-modal > .modal { height: min(88vh, 760px); max-width: var(--modal-w-wide); display: flex; flex-direction: column; overflow: hidden; }
  #ticket-detail-modal #tk-detail-body { flex: 1; overflow-y: auto; min-height: 0; }
  /* Sub-ticket rows are indented under their master in the ticket tables. */
  .tk-child-row > td:first-child { padding-left: 22px; }
  /* Conversation: chat-style bubbles — outbound (us) right, inbound (customer)
     left, internal notes a dashed amber aside. */
  #tk-conversation .tk-msg { max-width: 82%; padding: 8px 10px; border-radius: 8px; margin-bottom: 8px; }
  #tk-conversation .tk-msg-meta { font-size: 11px; color: var(--muted); margin-bottom: 3px; }
  #tk-conversation .tk-msg-body { font-size: 13px; white-space: pre-wrap; word-break: break-word; }
  #tk-conversation .tk-msg-in  { background: rgba(127,127,127,0.10); border: 1px solid var(--border); margin-right: auto; }
  #tk-conversation .tk-msg-out { background: rgba(59,130,246,0.12); border: 1px solid var(--accent); margin-left: auto; }
  #tk-conversation .tk-msg-note { background: rgba(255,193,7,0.10); border: 1px dashed #d4a017; margin-right: auto; }
  /* v5.4.1: ticket attachment chips (under each message + staged-for-send row). */
  .tk-att-row { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
  .tk-att-chip { display: inline-flex; align-items: center; gap: 4px; font-size: 12px;
    padding: 2px 6px; border: 1px solid var(--border); border-radius: 6px;
    background: rgba(127,127,127,0.08); max-width: 100%; }
  .tk-att-chip .tk-att-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 220px; }
  /* v5.4.1: small ticket marker before a device hostname (Devices page). */
  .dev-ticket-ic { display: inline-flex; vertical-align: middle; margin-right: 4px; color: var(--accent-ink); }
  /* v5.4.1 (H6): first-run onboarding tour (CSP-safe; built via DOM + .style). */
  .tour-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.45); z-index: 1200; }
  .tour-target { position: relative; z-index: 1201; box-shadow: 0 0 0 3px var(--accent); border-radius: 6px; }
  .tour-pop { position: fixed; z-index: 1202; width: 300px; max-width: 92vw; background: var(--surface, #fff);
    color: var(--text, #16181d); border: 1px solid var(--border); border-radius: 10px; padding: 14px 16px;
    box-shadow: var(--shadow-3); }
  .tour-pop-title { font-size: 16px; font-weight: 700; margin-bottom: 6px; }
  .tour-pop-body { font-size: 13px; line-height: 1.5; margin-bottom: 12px; }
  .tour-pop-row { display: flex; align-items: center; gap: 6px; }
  .tour-pop-meta { font-size: 12px; color: var(--muted); margin-right: auto; }
  /* v5.4.1 (H3): skip-to-content link — off-screen until keyboard-focused. */
  .skip-link { position: absolute; left: -9999px; top: 0; z-index: 1000;
    background: var(--accent); color: var(--accent-contrast, #fff); padding: 8px 14px; border-radius: 0 0 6px 0;
    font-size: 13px; text-decoration: none; }
  .skip-link:focus { left: 0; }
  .fl-wrap { flex-wrap: wrap; }
  .fl-1 { flex: 1; min-width: 0; }
  .ellipsis { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  #storage-maint-output { white-space: pre-wrap; font-family: var(--font-mono); font-size: 13px; }
  #cmdb-asset-modal > .modal > [id^="cmdb-tab-"] { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
  /* v6.4.3: the Tickets box must not be shrunk to a sliver.
     The modal above is a FIXED-HEIGHT flex column, the tab panel below takes
     `flex: 1 1 auto`, and this box specified no flex at all — so it inherited
     the default flex-shrink:1. Normally `min-height:auto` would stop a flex
     item collapsing below its content, but that protection does not apply to a
     scroll container, and .scroll-cap sets overflow-y:auto. Result: 110px of
     content rendered in 38px, i.e. the "tiny scroll window". `flex: none`
     makes it size to its content, still capped at .scroll-cap's 340px. */
  #cmdb-asset-modal > .modal > #cmdb-asset-tickets { flex: none; }
  /* v5.0.0: the Properties tab is a responsive 2-column grid so the short
   * inputs (Asset ID, Server function, …) sit half-width instead of spanning
   * the whole wider modal. Wide blocks — interfaces editor, the read-only info
   * panel, the decommissioned toggle — span both columns. Collapses to one
   * column automatically when the modal is narrow (auto-fit minmax). */
  #cmdb-tab-props { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 0 20px; align-items: start; }
  #cmdb-tab-props > .form-group { min-width: 0; }
  #cmdb-tab-props > .cmdb-fullspan,
  #cmdb-tab-props > .cmdb-decomm-row { grid-column: 1 / -1; }
  /* v3.13.0: same fixed-size treatment for the Host Configuration modal — its
   * sections (Repos / Netplan / … / Cron) vary a lot in height, so switching
   * tabs used to resize and re-centre the window. The active .hc-panel now
   * flex-grows and scrolls inside a stable-height modal; header/tabs and the
   * Save row stay put. */
  #host-config-modal > .modal { height: min(88vh, 760px); display: flex; flex-direction: column; overflow: hidden; }
  #host-config-modal > .modal > .hc-panel { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
  .muted-row { opacity: 0.55; }
  /* v3.12.0: top-right account menu + My Account page */
  .acct-menu { position: relative; }
  /* v6: account footer (chosen-design .side-foot). Sticky inside the sidebar
     scroll so it stays visible; the dropdown opens upward from here. */
  .side-foot {
    position: sticky;
    bottom: 0;
    margin-top: auto;
    background: var(--side);
    border-top: 1px solid var(--hair);
    padding: 8px 2px 10px;
  }
  .side-foot .acct-btn {
    width: 100%;
    gap: 9px;
    padding: 5px 7px;
    font-family: inherit;
    font-size: 12px;
    color: var(--side-ink);
  }
  .side-foot .acct-btn:hover { background: var(--hair-2); }
  .side-foot .acct-btn > svg { margin-left: auto; color: var(--muted); }
  .side-foot .acct-avatar-sm { width: 22px; height: 22px; font-size: 10px; }
  .side-foot .acct-dropdown { top: auto; bottom: calc(100% + 6px); left: 0; right: 0; }
  .acct-btn { display: inline-flex; align-items: center; gap: 6px; background: none; border: 0; cursor: pointer; color: inherit; padding: 4px 6px; border-radius: 6px; }
  .acct-btn:hover { background: rgba(127,127,127,0.12); }
  .acct-avatar-sm { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; border-radius: 50%; background: var(--accent); color: var(--accent-contrast, #fff); font-size: 12px; font-weight: 600; overflow: hidden; }
  .acct-avatar-sm img { width: 100%; height: 100%; object-fit: cover; }
  .acct-name { font-size: 13px; max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* v5.6.x fix (#18): was background:var(--card, #1e1e24) — but --card is never
     defined anywhere, so it always fell back to the hard dark #1e1e24; in light
     mode the panel stayed dark with dark (inherited) text = unreadable. Use the
     light-aware --surface2 (defined for every theme + body.light). */
  .acct-dropdown { position: absolute; right: 0; top: calc(100% + 6px); min-width: 180px; background: var(--surface); color: var(--text); border: 1px solid var(--hair); border-radius: 8px; box-shadow: var(--shadow-2); z-index: 1000; padding: 4px; }
  .acct-dropdown-item { display: flex; align-items: center; gap: 8px; width: 100%; text-align: left; background: none; border: 0; color: inherit; padding: 8px 10px; border-radius: 6px; cursor: pointer; font-size: 13px; }
  .acct-dropdown-item:hover { background: rgba(127,127,127,0.14); }
  .acct-profile { display: flex; gap: 16px; align-items: center; flex-wrap: wrap; }
  .acct-avatar { display: inline-flex; align-items: center; justify-content: center; width: 72px; height: 72px; border-radius: 50%; background: var(--accent); color: var(--accent-contrast, #fff); font-size: 28px; font-weight: 600; overflow: hidden; }
  .acct-avatar img { width: 100%; height: 100%; object-fit: cover; }
  .acct-profile-meta { display: flex; flex-direction: column; gap: 8px; }
  .doc-card kbd {
    background: var(--surface2);
    border: 1px solid var(--border);
    border-radius: 3px;
    padding: 1px 5px;
    font-size: 11px;
    font-family: var(--font);
  }
  .app-layout {
    display: flex;
    min-height: calc(100vh - 60px);
  }
  .app-content {
    /* keep in lockstep with .sidebar width (v6.0.0: 248px) */
    margin-left: 248px;
    flex: 1;
    min-width: 0;
  }
  .header-nav { display: none; }
  .logout-btn {
    background: transparent;
    border: 1px solid var(--border);
    color: var(--muted);
    padding: 6px 14px;
    border-radius: 6px;
    font-size: 13px;
    cursor: pointer;
    transition: background 0.2s, border-color 0.2s, color 0.2s, transform 0.2s;
    font-family: var(--font);
  }
  .logout-btn:hover { border-color: var(--accent); color: var(--accent-ink); }
  .page { display: none; }
  .page.active { display: block; }
  main { padding: 48px 0; }
  /* v6 (chosen-design .setpane h2/.sub): compact 19px page heading with the
     description inline right under it. The subtitle pulls itself up to sit
     3px below the title; title-only pages keep the 16px content gap. */
  .page-title {
    margin: 0 0 16px;
    font-size: 19px;
    font-weight: 640;
    letter-spacing: -0.01em;
  }
  .page-subtitle {
    color: var(--muted);
    font-size: 13px;
    margin: 0 0 18px;
  }
  /* directly under the title (or title row) it pulls up to sit 3px below it
     (16px title margin − 13px); elsewhere it spaces itself normally */
  .page-title + .page-subtitle { margin-top: -13px; }
  /* v7.1.0: a heading for a section that sits between cards on a page
     (Containers → Image updates). It was a 16px .page-title — a third heading
     size between the 19px page title and the 13px card heading. Same size and
     weight as a card heading now; the subtitle under it pulls up the same way. */
  .subsection-title { margin: 0 0 16px; font-size: 13px; font-weight: 620; }
  .subsection-title + .page-subtitle { margin-top: -13px; }
  .stats-row {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 16px;
    margin-bottom: 40px;
  }
  .stat-card {
    background: var(--surface);
    border: 1px solid var(--hair);
    border-radius: 11px;
    padding: 20px 24px;
    display: flex;
    align-items: center;
    gap: 16px;
  }
  .stat-icon {
    width: 40px; height: 40px;
    border-radius: 10px;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
  }
  .stat-icon.blue  { background: rgba(59,126,255,0.15); color: var(--accent-ink); }
  .stat-icon.green { background: rgba(34,197,94,0.15);  color: var(--green); }
  .stat-icon.red   { background: rgba(239,68,68,0.15);  color: var(--red-on-tint); }
  .stat-icon svg { width: 20px; height: 20px; }
  .stat-value { font-size: 28px; font-weight: 700; line-height: 1; }
  .stat-label { font-size: 12px; color: var(--muted); margin-top: 4px; }
  .section-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 20px;
  }
  /* v6 (chosen-design .grp .gt): 13px card/section heading */
  .section-title { font-size: 13px; font-weight: 620; }
  .btn-primary {
    /* v6.0.1: content-sized by default — a lone primary button no longer grows
       to span its flex row / card. The modal Cancel+Save split re-adds flex:1
       just inside .modal-actions (below), so modals are unchanged. */
    flex: 0 0 auto;
    background: var(--accent);
    color: var(--accent-contrast, #fff);
    border: none;
    padding: 8px 14px;
    border-radius: 7px;
    font-size: 13px;
    font-weight: 560;
    cursor: pointer;
    transition: background 0.2s;
    font-family: var(--font);
  }
  .btn-primary:hover { background: var(--accent2); }
  .btn-secondary {
    background: transparent;
    border: 1px solid var(--hair);
    color: var(--text);
    padding: 8px 14px;
    border-radius: 7px;
    font-size: 13px;
    cursor: pointer;
    transition: background 0.2s, border-color 0.2s, color 0.2s;
    font-family: var(--font);
  }
  .btn-secondary:hover { background: var(--hair-2); }
  /* v6.4.1: .btn-icon svg has always been pinned to 14px, but .btn-primary /
     .btn-secondary had no width rule — so the markup attribute won and the
     glyphs drifted (13px on 12 buttons, 15px on one, 14px on the rest),
     visibly mismatched where they sit side by side in one row. Pinning it
     here fixes every existing site and stops the markup drifting again. */
  /* v7.0.2: `flex: 0 0 auto`, the same reason .btn-icon svg carries it — these
     two became `display: inline-flex` below, so their <svg> is a flex ITEM at
     the default `flex-shrink: 1`. Width is a basis the algorithm may shrink
     below, not a floor: measured on a crowded row, these icons rendered 8.2px
     and 9.2px against the 14px declared right here. */
  .btn-primary svg, .btn-secondary svg { width: 14px; height: 14px; flex: 0 0 auto; }
  /* v6.4.3: the icon/label gap, done the way .btn-icon just above already
     does it — one idiom, not two.
     What this replaces: `.btn-primary svg:not(:only-child) { margin-right: 5px }`,
     which matched ZERO of the 195 icon+label buttons in the app. A label is a
     TEXT node, so the svg IS the only ELEMENT child and :not(:only-child) is
     never true. The comment on it described the intent perfectly ("only when
     the svg has a text sibling"), which is exactly why nobody re-checked it
     for three releases; it also claimed to cover .btn-icon, which it did not.
     inline-flex, not flex: keeps these inline like a <button>'s default, where
     .btn-icon is free to be block-level. justify-content is load-bearing —
     `.modal-actions button { flex: 1 }` at <=720px stretches these, and
     without it the label left-jams inside the widened box. */
  .btn-primary, .btn-secondary {
    display: inline-flex; align-items: center; justify-content: center; gap: 5px;
  }
  .btn-icon {
    background: transparent;
    border: 1px solid var(--hair);
    color: var(--muted);
    padding: 8px 12px;   /* keeps input.page-filter's 34px toolbar-height contract */
    border-radius: 7px;
    font-size: 13px;
    cursor: pointer;
    transition: background 0.2s, border-color 0.2s, color 0.2s;
    display: flex; align-items: center; gap: 5px;
    font-family: var(--font);
  }
  .btn-icon:hover { background: var(--hair-2); color: var(--text); }
  /* v7.0.0: `flex: 0 0 auto` — an icon must never be squeezed.
     `.btn-icon` is a flex row, so its <svg> is a flex ITEM and inherits the
     default `flex-shrink: 1`. On a crowded toolbar that overflows, the icon
     is the thing that gives: measured on the Network map page, this exact
     button rendered its icon at 12.3px against 14px for the byte-identical
     button on Virtualization, which also pushed the icon-to-label gap to
     9.6px against the 5px standard. Width alone is not a floor for a flex
     item — it is a basis the algorithm may shrink below.
     Found only after fixing the 500 that made this page unreachable: the
     rendered gates had never been able to measure it. */
  .btn-icon svg { width: 14px; height: 14px; flex: 0 0 auto; }
  /* v7.0.0: pin the CONTENT height to the icon's 14px, so a button whose label
     has no icon is exactly as tall as its neighbours that do.
     Measured before this: in one Alerts action row, the seven buttons with an
     icon rendered 22px and `Resolve`, `Incident` and `Ticket` — which carry no
     icon — rendered 20px, giving a row of ten buttons two different heights.
     Same 2px step on Checks, CVE and Scripts action cells, and the reason is
     mechanical: the box was `svg 14px + padding` for one and `text line box
     ~12px + padding` for the other. `line-height` makes the text box 14px too,
     so the two agree at every size variant without hard-coding a height per
     variant. */
  .btn-icon { line-height: 14px; }
  /* v6.4.1: .btn-icon is `display:flex`, i.e. BLOCK-level, so two of them as
     bare siblings in a table cell stacked vertically instead of sitting in a
     row — the literal spaces between them are inert between flex boxes. Most
     visible in the alert inbox, where an action cell can hold seven buttons.
     Table cells are the only place they appear unwrapped (elsewhere a flex
     container like .user-actions already lays them out). */
  td > .btn-icon { display: inline-flex; vertical-align: middle; }
  td > .btn-icon + .btn-icon { margin-left: 6px; }
  /* v6.4.1: at <=720px `.modal-actions button { flex: 1 }` stretches every
     button; .btn-icon has no justify-content, so its label jammed hard-left
     while its <button>-default siblings stayed centred. */
  .modal-actions > .btn-icon { justify-content: center; }
  /* v6.0.1 (item 10): compact button size for dense rows / table action cells. */
  .btn-sm { padding: 5px 16px; font-size: 12px; min-height: 0; }
  .btn-xs { padding: 3px 13px; font-size: 11px; min-height: 0; }
  .enroll-btn {
    background: var(--accent);
    color: var(--accent-contrast, #fff);
    border: none;
    padding: 8px 18px;
    border-radius: 8px;
    font-size: 13px;
    font-weight: 500;
    cursor: pointer;
    transition: background 0.2s, border-color 0.2s, color 0.2s, transform 0.2s;
    display: flex;
    align-items: center;
    gap: 5px;
    font-family: var(--font);
  }
  .enroll-btn:hover { background: var(--accent2); transform: translateY(-1px); }
  .enroll-btn svg { width: 14px; height: 14px; }
  .devices-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
    gap: 20px;
  }
  /* Full-width "+N more devices" notice when the card grid is capped on a
     very large fleet — spans every column so it reads as a footer, not a card. */
  .device-card-more {
    grid-column: 1 / -1;
    text-align: center;
    padding: 14px;
    border: 1px dashed var(--border);
    border-radius: 12px;
  }
  .device-card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 14px;
    padding: 24px;
    transition: border-color 0.2s, transform 0.2s;
    position: relative;
    overflow: visible;
    /* v5.6.x perf: isolate each card's layout/style recalc from its siblings so a
       single card update (status change, density toggle, dropdown open) doesn't
       relayout the whole grid. NOT `paint` — cards host overflowing ⋯ dropdowns. */
    contain: layout style;
  }
  /* v1.11.5: density modes for the devices grid. The container gets a
     `dens-<mode>` class; descendants restyle accordingly. Compact halves
     padding and shrinks fonts; spacious goes the other way. The dropdown
     menu and modals are unaffected — only the card chrome itself. */
  .devices-grid.dens-compact { gap: 8px; }
  .devices-grid.dens-compact .device-card { padding: 12px 14px; border-radius: 10px; }
  .devices-grid.dens-compact .device-header { margin-bottom: 8px; }
  .devices-grid.dens-compact .device-icon { width: 32px; height: 32px; border-radius: 7px; }
  .devices-grid.dens-compact .device-name { font-size: 13px; }
  .devices-grid.dens-compact .device-hostname { font-size: 11px; }
  .devices-grid.dens-compact .device-meta { gap: 6px; }
  .devices-grid.dens-compact .device-meta .meta-label { font-size: 10px; }
  .devices-grid.dens-compact .device-meta .meta-value { font-size: 12px; }
  .devices-grid.dens-compact .status-badge { padding: 2px 6px; font-size: 10px; }

  .devices-grid.dens-spacious { gap: 24px; }
  .devices-grid.dens-spacious .device-card { padding: 32px; }
  .devices-grid.dens-spacious .device-name { font-size: 16px; }
  .devices-grid.dens-spacious .device-icon { width: 52px; height: 52px; }

  /* v1.11.7: Minimal density on the Devices grid renders as a real
     <table> instead of a flex-row override on each card. The v1.11.6
     attempt couldn't keep columns aligned because each card was its
     own flex container — variable cell widths made "Online" not appear
     under "Online" across rows. Tables are the correct primitive here:
     each <tr> shares the same <td> structure, and the browser handles
     column alignment for us. The table also gets sortable headers via
     tableCtl, matching the other category pages. */
  .devices-grid.dens-minimal {
    display: block;
    padding: 0;
    margin: 0;
  }
  .devices-minimal-wrap {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    /* v1.11.8: was `overflow: hidden`, which clipped the dropdown menu
       on rows near the right edge or bottom of the table. The rounded
       corners worked because of `border-radius` alone — overflow:hidden
       was extra insurance that turned out to be too aggressive. The
       dropdown must escape the wrap when it pops, so we accept that the
       very corners might briefly show a non-rounded artifact for that
       rare case where the menu opens and the row backgound colour
       extends to the wrap edge. The thead has its own background so
       there's no visible artifact at the top corners; the bottom corners
       only matter when the menu is open and its z-index puts it
       above the wrap edge anyway. */
    overflow: visible;
  }
  /* v1.11.8: keep the rounded corners by clipping the table's own edges.
     Browsers don't honour border-radius on <table> directly without
     border-collapse:separate, so we apply it to the outer <thead> and
     last <tr> instead. */
  .devices-minimal-table thead tr:first-child th:first-child { border-top-left-radius: 10px; }
  .devices-minimal-table thead tr:first-child th:last-child  { border-top-right-radius: 10px; }
  .devices-minimal-table tbody tr:last-child td:first-child  { border-bottom-left-radius: 10px; }
  .devices-minimal-table tbody tr:last-child td:last-child   { border-bottom-right-radius: 10px; }
  .devices-minimal-table {
    width: 100%;
    /* v1.11.9: table-layout:fixed forces the browser to honour the
       column widths we set on <th> rather than auto-stretching cells
       to fit the longest content. Without this, long OS strings like
       "Debian GNU/Linux 12 (bookworm)" pushed the table past the
       container's right edge — visible as the table being 5–15px
       wider than the stats row above it. With fixed layout, our
       max-widths and ellipsis on <td> finally take effect. */
    table-layout: fixed;
    border-collapse: collapse;
    font-size: 13px;
  }
  .devices-minimal-table thead th {
    background: var(--surface2);
    color: var(--muted);
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    text-align: left;
    padding: 10px 12px;
    border-bottom: 1px solid var(--border);
    white-space: nowrap;
  }
  .devices-minimal-table tbody tr {
    border-bottom: 1px solid var(--border);
    transition: background 0.12s;
  }
  .devices-minimal-table tbody tr:last-child { border-bottom: none; }
  .devices-minimal-table tbody tr:hover {
    background: rgba(59,126,255,0.04);
  }
  /* v6.4.3: de-emphasise an offline row by COLOUR, not by fading the whole
     row. `opacity: 0.7` here dropped the already-muted cells inside it — the
     host and last-seen columns are .hint — from 4.98:1 to 3.11:1. You cannot
     dim the dimmest text on the page and stay AA.
     Dropping the full-contrast cells to --muted is a 14.69:1 -> 4.98:1 fall,
     a far bigger perceptual change than a 30% fade, and it still passes. The
     cells that were ALREADY muted are left alone, so nothing double-dims.
     Non-text (the status dot, glyphs) may still fade — WCAG 1.4.3 governs
     text, and the dot carries an aria-label besides.
     Recessing the row background to var(--bg) was the other candidate and is
     rejected: it helps the dark theme (5.53:1) and BREAKS the light one
     (4.03:1), because there --bg moves toward the text rather than away. */
  .devices-minimal-table tbody tr.offline td,
  .devices-minimal-table tbody tr.offline td a { color: var(--muted); }
  .devices-minimal-table tbody tr.offline :is(svg, .status-dot, img) {
    opacity: 0.7;
  }
  .devices-minimal-table tbody td {
    padding: 8px 12px;
    vertical-align: middle;
    /* v1.11.9: with table-layout:fixed on the table, each <td> is sized
       to its column header's width; overflow:hidden clips overflow.
       v3.3.x: text-overflow is CLIP, not ellipsis. Under table-layout:
       fixed a too-narrow container shrinks every column proportionally,
       and Chrome renders the "…" glyph for ANY clipped cell — including
       fixed-content ones like the checkbox and the status pill — which
       showed up as mysterious black dots right after the checkbox
       (Chrome only; Firefox clips silently). Clipping with no ellipsis
       kills the dots regardless of which cell is squeezed. 
       long text (OS names) hard-cuts instead of showing "…" — an
       acceptable trade for never rendering stray dots. */
    overflow: hidden;
    text-overflow: clip;
    white-space: nowrap;
  }
  /* v1.12.1: highlight selected rows in minimal mode. Matches the
     batch-select visual treatment in cards mode (subtle blue glow on
     the selected card). Subdued so a 100-device selection isn't
     visually overwhelming. */
  .devices-minimal-table tbody tr.selected {
    background: rgba(99, 179, 237, 0.08);
  }
  .devices-minimal-table tbody tr.selected td {
    border-top: 1px solid rgba(99, 179, 237, 0.3);
    border-bottom: 1px solid rgba(99, 179, 237, 0.3);
  }
  /* slightly tighter row height for the ⋮ (open the drawer) column */
  .devices-minimal-table .dev-actions-cell {
    padding: 4px 8px;
  }
  /* responsive — drop low-priority columns on narrow viewports.
     Order of importance, kept-most → dropped-first:
       Status, Name, [Hostname], [Group], OS, [IP], [Version], [Last seen], Actions
     We start dropping the bracketed ones at 1100px and below. */
  @media (max-width: 1280px) {
    .devices-minimal-table .dev-host-cell,
    .devices-minimal-table th[data-col="hostname"] { display: none; }
  }
  @media (max-width: 1080px) {
    .devices-minimal-table .dev-version-cell,
    .devices-minimal-table th[data-col="version"] { display: none; }
  }
  @media (max-width: 920px) {
    .devices-minimal-table .dev-group-cell,
    .devices-minimal-table th[data-col="group"] { display: none; }
  }
  @media (max-width: 760px) {
    .devices-minimal-table .dev-ip-cell,
    .devices-minimal-table th[data-col="ip"] { display: none; }
  }
  @media (max-width: 620px) {
    .devices-minimal-table .dev-os-cell,
    .devices-minimal-table th[data-col="os"] { display: none; }
  }
  .device-card::before {
    content: '';
    position: absolute;
    top: 0; left: 0; right: 0;
    height: 2px;
    border-radius: 14px 14px 0 0;
  }
  .device-card.online::before  { background: var(--green); }
  .device-card.offline::before { background: var(--red); }
  .device-card:hover { border-color: rgba(59,126,255,0.3); }
  /* v5.0.0: decommissioned assets are greyed out and desaturated. */
  .device-card.decommissioned { opacity: 0.55; filter: grayscale(0.85); }
  .device-card.decommissioned::before { background: var(--muted) !important; }
  .device-card.decommissioned:hover { opacity: 0.8; }
  .decomm-pill { background: rgba(124,135,154,0.16); color: var(--muted);
    border: 1px solid var(--border); }
  tr.decommissioned { opacity: 0.55; filter: grayscale(0.85); }
  /* v5.0.0: NAT/public IP shown as a child of the primary interface. */
  /* v6.4.3: opacity removed — its content is already .c-muted/.meta-sm, and
     the 10px indent is what signals 'child of the row above'. */
  .cmdb-nat-child { padding-left: 10px; margin-top: 2px; }
  .device-header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    margin-bottom: 20px;
  }
  .device-info { display: flex; align-items: center; gap: 14px; }
  .device-icon {
    width: 44px; height: 44px;
    background: var(--surface2);
    border: 1px solid var(--border);
    border-radius: 10px;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    cursor: pointer;
  }
  .device-icon svg { width: 22px; height: 22px; color: var(--muted); }
  .device-name     { font-size: 13px; font-weight: 600; }
  .device-hostname { font-size: 12px; color: var(--muted); margin-top: 2px; font-family: var(--font-mono); }
  .status-badge {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 4px 10px;
    border-radius: 20px;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.5px;
    text-transform: uppercase;
    white-space: nowrap;
  }
  .status-badge.online  { background: rgba(34,197,94,0.1);  color: var(--green); border: 1px solid rgba(34,197,94,0.2); }
  .status-badge.offline { background: rgba(239,68,68,0.1);  color: var(--red-on-tint);   border: 1px solid rgba(239,68,68,0.2); }
  .status-badge-dot { width: 6px; height: 6px; border-radius: 50%; }
  .online  .status-badge-dot { background: var(--green); }   /* v5.6.x perf: no per-device infinite pulse (see .status-dot note) */
  .offline .status-badge-dot { background: var(--red); }
  /* v4.1.0: Devices table online/offline as a bare dot (no text) — sortable via
     the existing status column key. Green pulses (from the base .status-dot),
     red is static. */
  .status-dot.online  { background: var(--green); }
  .status-dot.offline { background: var(--red); animation: none; }
  .dev-status-cell .status-dot { width: 9px; height: 9px; }
  .device-meta {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 12px;
    margin-bottom: 20px;
  }
  .meta-label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; }
  .meta-value { font-size: 13px; font-weight: 500; margin-top: 3px; font-family: var(--font-mono); }
  .patch-badge {
    display: inline-block;
    padding: 1px 6px;
    border-radius: 4px;
    font-size: 11px;
    font-weight: 600;
    font-family: var(--font);
  }
  .patch-badge.warn { background: rgba(245,158,11,0.15); color: var(--amber); }
  .patch-badge.ok   { background: rgba(34,197,94,0.1);   color: var(--green); }
  /* v6.2.0: .crit was used in five places (TAMPER DETECTED, critical-CVE counts,
     disabled users, breached RPO) but never had a rule — so the most severe badge
     in the product rendered as unstyled plain text, indistinguishable from a
     label. The one badge that must never be missable was the only one with no
     colour. */
  /* v6.3.0 (a11y): var(--red) composites to 4.48:1 on this tinted background
     — 0.02 under AA (axe serious, users page). Dedicated color, #62 pattern. */
  .patch-badge.crit { background: rgba(239,68,68,0.15);  color: #ec7a70; }
  .last-seen { font-size: 11px; color: var(--muted); margin-top: 12px; }

  /* v6.4.1: this was one of THREE depth-0 `.empty-state` rules; the last one
     in the file won, so this padding and the compact one below were both
     dead. Consolidated here — the only declaration — with the body anchor
     pinned so an empty state reads the same inside a <td> as in a <div>. */
  .empty-state {
    text-align: center;
    padding: 40px 24px;
    color: var(--muted);
    font-size: 13px;
    grid-column: 1 / -1;
  }
  .empty-icon {
    width: 64px; height: 64px;
    background: var(--surface2);
    border: 1px solid var(--border);
    border-radius: 16px;
    display: flex;
    align-items: center;
    justify-content: center;
    margin: 0 auto 20px;
  }
  .empty-icon svg { width: 28px; height: 28px; color: var(--muted); }
  /* v7.1.0: an empty card's heading is a card heading — 13px/620, not a 16px third size */
  .empty-title { font-size: 13px; font-weight: 620; color: var(--text); margin-bottom: 8px; }
  .empty-text  { font-size: 13px; line-height: 1.6; }
  .modal-overlay {
    position: fixed;
    inset: 0;
    /* v4.3.0 perf: slightly deeper dim instead of backdrop-filter blur —
       a full-viewport blur(4px) made Firefox re-blur everything underneath
       per frame while a modal was open. Don't reintroduce backdrop-filter. */
    background: rgba(0,0,0,0.78);
    z-index: 200;
    display: none;          /* structurally hidden — no FOUC on mobile refresh */
    align-items: center;
    justify-content: center;
  }
  /* v2.8.1: fade-in on open instead of relying on opacity transition from
     display:flex so the modal is truly invisible before JS runs */
  .modal-overlay.active {
    display: flex;
    animation: _modal-fade-in 0.18s ease both;
  }
  @keyframes _modal-fade-in {
    from { opacity: 0; }
    to   { opacity: 1; }
  }
  /* v6.4.1: ONE width scale. Three tiers, no per-modal max-width overrides —
     the CMDB / ticket / storage dialogs each carried their own min(940px,94vw)
     and everything else sat at 680px, so two dialogs opened from the same page
     were visibly different widths. --modal-w-wide is the standard content
     width (the CMDB dialog is the reference); the narrow default stays for
     confirms and short forms, where 940px would be absurd.
     v6.4.2: every tier widened by 20px — the content was reading tight at the
     old widths. Change the tokens, never a per-dialog max-width: the whole
     point of the scale is that two dialogs opened from one page match. The
     vw clamps are unchanged, so narrow viewports are unaffected. */
  :root {
    --modal-w:       500px;
    --modal-w-wide:  min(960px, 94vw);
    --modal-w-xwide: min(1380px, 94vw);
  }
  .modal {
    position: relative;          /* anchor for .modal-close */
    background: var(--surface);
    border: 1px solid var(--hair);
    border-radius: 14px;
    padding: 28px;
    width: 100%;
    max-width: var(--modal-w);
    transform: translateY(20px);
    transition: transform 0.2s;
    max-height: 90vh;
    overflow-y: auto;
  }
  .modal-overlay.active .modal { transform: translateY(0); }
  .modal-wide { max-width: var(--modal-w-wide); }
  /* Dense views like the RouterOS console; capped to the viewport. */
  .modal-xwide { max-width: var(--modal-w-xwide); }
  /* v6.4.1: dismiss control, injected into every dialog by openModal. Sits
     inside the padding box aligned to the dialog's own border rather than
     floating over the content, so a long title never runs underneath it. */
  .modal-close {
    position: absolute;
    top: 14px;
    right: 14px;
    width: 28px;
    height: 28px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: 1px solid transparent;
    border-radius: 8px;
    background: transparent;
    color: var(--muted);
    cursor: pointer;
    z-index: 2;
  }
  .modal-close:hover  { background: var(--bg); color: var(--text); border-color: var(--hair); }
  .modal-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
  /* Keep a long title clear of the button. */
  .modal-title { padding-right: 34px; }
  /* A dialog that manages its own scrolling (flex column + overflow hidden)
     would scroll the button away with the content — pin it to the frame. */
  #cmdb-asset-modal > .modal > .modal-close,
  #ticket-detail-modal > .modal > .modal-close,
  #host-config-modal > .modal > .modal-close { position: absolute; }
  .modal-title    { font-size: 16px; font-weight: 640; margin-bottom: 6px; }
  /* v6.4.2: pre-line, because uiConfirm/uiPrompt set this element with
     textContent and 40 call sites author a "\n\n" to separate the action from
     its consequence ("This cannot be undone. Use Clear resolved if you only
     want to purge resolved."). Without it every one of those collapsed into a
     single run-on grey line, burying the escape hatch mid-sentence. Wrapping is
     unaffected — pre-line still wraps, it only stops eating authored breaks. */
  .modal-subtitle { font-size: 13px; color: var(--muted); margin-bottom: 20px; line-height: 1.5; white-space: pre-line; }
  .pin-display {
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: 20px;
    text-align: center;
    margin-bottom: 24px;
  }
  .pin-label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; }
  .pin-code  { font-family: var(--font-mono); font-size: 36px; font-weight: 700; letter-spacing: 8px; color: var(--accent-ink); }
  .pin-timer { font-size: 12px; color: var(--muted); margin-top: 8px; }
  .pin-countdown { color: var(--amber); font-weight: 600; }
  .modal-actions { display: flex; gap: 10px; margin-top: 24px; }
  /* v6.0.1: the primary action in a modal fills the row beside the auto-width
     Cancel (base .btn-primary is content-sized now). Mobile splits both 50/50
     via the .modal-actions button rule in the max-width:720px block. */
  .modal-actions .btn-primary { flex: 1; }
  .confirm-icon {
    width: 48px; height: 48px;
    border-radius: 12px;
    display: flex; align-items: center; justify-content: center;
    margin-bottom: 16px;
  }
  .confirm-icon.red    { background: rgba(239,68,68,0.1); border: 1px solid rgba(239,68,68,0.3); color: var(--red); }
  .confirm-icon.amber  { background: rgba(245,158,11,0.1); border: 1px solid rgba(245,158,11,0.3); color: var(--amber); }
  .confirm-icon svg { width: 24px; height: 24px; }
  .form-group { margin-bottom: 16px; }
  /* v6: sentence-case labels (chosen-design .lab) — the uppercase eyebrow is gone */
  .form-label {
    font-size: 12px; color: var(--muted); font-weight: 500;
    margin-bottom: 6px; display: block;
  }
  /* v6 (chosen-design .inp): flat hairline field. v7.1.0: single-line fields
     and selects use the UI font at body size, so a filter box and the buttons
     beside it read as one row; multi-line editors (scripts, YAML, keys) keep
     the mono face — see textarea below. */
  .form-input {
    width: 100%;
    background: var(--bg);
    border: 1px solid var(--hair);
    border-radius: 7px;
    padding: 8px 10px;
    font-size: 13px;
    color: var(--text);
    outline: none;
    transition: border-color 0.2s;
    font-family: var(--font);
  }
  textarea.form-input { font-family: var(--font-mono); }
  .form-input:focus { border-color: var(--accent); }
  .form-input::placeholder { color: var(--muted); }
  select.form-input { cursor: pointer; }

  /* v3.13.0: theme EVERY native form control by default, so a control that is
   * missing its .form-input class (or a bare <select>/<input>/<textarea>) still
   * matches the dark theme instead of rendering browser-white. The :not() chain
   * is wrapped in :where() so the whole selector keeps ELEMENT-level specificity
   * (0,0,1) — otherwise the five attribute :not()s would outrank .form-input
   * (0,1,0) and clobber its width:100%, misaligning every settings form.
   * Checkboxes, radios, range, color and file inputs keep their own styling. */
  input:where(:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=color]):not([type=file])),
  select,
  textarea {
    background: var(--bg);
    color: var(--text);
    border: 1px solid var(--hair);
    border-radius: 7px;
    padding: 8px 10px;
    font-size: 13px;
    font-family: var(--font);
    outline: none;
  }
  textarea { font-family: var(--font-mono); }
  /* v6: flat select with a muted chevron (the chamfered dropdown is gone);
     background-IMAGE only, so the field colour above still applies */
  select {
    -webkit-appearance: none;
    appearance: none;
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%237f8b98' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>");
    background-repeat: no-repeat;
    background-position: right 10px center;
    padding-right: 30px;
  }
  /* v6: flat checkboxes — hairline square, accent fill when checked (the
     warm-amber chamfer is gone) */
  input[type="checkbox"] {
    -webkit-appearance: none;
    appearance: none;
    width: 16px; height: 16px; margin: 0;
    flex: none; vertical-align: middle; cursor: pointer;
    background: var(--bg);
    border: 1px solid var(--hair);
    border-radius: 5px;
    position: relative;
    transition: background .12s, border-color .12s;
  }
  input[type="checkbox"]:checked { background: var(--accent); border-color: var(--accent); }
  input[type="checkbox"]:checked::after {
    content: ""; position: absolute; left: 4.5px; top: 1.5px;
    width: 4px; height: 8px;
    border: solid #fff; border-width: 0 2px 2px 0;
    transform: rotate(45deg);
  }
  input:where(:not([type=checkbox]):not([type=radio])):focus,
  select:focus, textarea:focus { border-color: var(--accent); }
  /* v6.4.3 a11y: give the keyboard-focus ring back to text fields.
     The global `:focus-visible { outline: 2px solid var(--accent) }` near the
     top of this file is specificity (0,1,0), and so is `.form-input`, whose
     BASE rule (not its :focus rule) sets `outline: none` — so the later one
     won and every text input, select and textarea in the app focused with
     nothing but a 1px border-colour change. The v4.3.0 comment on the global
     rule anticipated a few opt-outs like .sidebar-search; .form-input is not
     a few, it is essentially every field. Higher specificity AND later source
     order, so both tie-breakers point the same way. `outline: none` stays on
     the base rules: it suppresses the mouse-click ring, which is the whole
     reason :focus-visible exists. */
  .form-input:focus-visible,
  input:where(:not([type=checkbox]):not([type=radio])):focus-visible,
  select:focus-visible, textarea:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
  }
  /* v5.1.1: keep the native <option> list readable on every theme. Without explicit
   * colours, some browsers/OS render the highlighted (selected/hovered) option with
   * the accent colour for BOTH text and background — accent-on-accent (e.g. blue text
   * on the blue selection bar) — so the open dropdown is unreadable. Force a solid
   * surface base + a translucent-accent highlight that keeps the normal text colour. */
  option { background: var(--surface); color: var(--text); }
  option:checked, option:hover { background: var(--accent-edge); color: var(--text); }
  /* v5.1.1: explicit, readable text selection. With no rule, "marking" (selecting)
   * text in a filter box fell back to the browser default and could render the
   * selection bar AND the text both accent-blue (blue-on-blue = invisible). */
  ::selection { background: var(--accent); color: var(--accent-contrast, #fff); -webkit-text-fill-color: var(--accent-contrast, #fff); }
  ::-moz-selection { background: var(--accent); color: var(--accent-contrast, #fff); }
  /* v5.1.1: kill Chrome's autofill paint. On a dark theme an autofilled field
   * (e.g. a value Chrome remembers) renders solid blue with clashing text —
   * "blue-on-blue". Force our surface + text colour for EVERY input, in every
   * state. The 9999s transition defeats Chrome's autofill background animation. */
  input:-webkit-autofill,
  input:-webkit-autofill:hover,
  input:-webkit-autofill:focus,
  input:-webkit-autofill:active,
  textarea:-webkit-autofill,
  select:-webkit-autofill {
    -webkit-box-shadow: 0 0 0 1000px var(--well-bg, var(--bg)) inset !important;
    -webkit-text-fill-color: var(--text) !important;
    caret-color: var(--accent);
    transition: background-color 9999s ease-out 0s;
  }
  input::placeholder, textarea::placeholder { color: var(--muted); }
  select:where(:not(.form-input)) { cursor: pointer; }
  /* Form fields inside a labelled group fill the row (matches .form-input)
   * without forcing width on inline inputs elsewhere (toolbars, steppers) or on
   * narrow inputs. */
  .form-group > input:not([type=checkbox]):not([type=radio]):not(.input-narrow),
  .form-group > select:not(.input-narrow),
  .form-group > textarea { width: 100%; }

  /* Generic .btn (e.g. the software-policy "Add rule" button) had no CSS at all
   * and rendered as a white browser default — match the secondary button. */
  .btn {
    background: transparent;
    border: 1px solid var(--hair);
    color: var(--text);
    padding: 8px 14px;
    border-radius: 7px;
    font-size: 13px;
    cursor: pointer;
    transition: background 0.2s, border-color 0.2s, color 0.2s;
    font-family: var(--font);
  }
  .btn:hover { background: var(--hair-2); }
  /* v3.13.0: off-screen-but-present (a11y) — used for the hidden username field
   * that satisfies the browser's password-form accessibility check. */
  .visually-hidden { position: absolute !important; width: 1px; height: 1px;
    padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0);
    white-space: nowrap; border: 0; }
  /* v3.13.0: Home "ask the fleet" AI omnibox */
  .home-ai-ask { display: flex; gap: 8px; align-items: center; }
  .home-ai-ask > .form-input { flex: 1; }
  .home-ai-ask > .btn-primary { flex: 0 0 auto; white-space: nowrap; }
  /* v3.12.0: one consistent checkbox/radio size + colour across the whole UI
     (they were rendering at assorted browser-default sizes). flex:none keeps
     them from shrinking inside flex rows. Mobile bumps to 20px further down. */
  input[type="checkbox"], input[type="radio"] {
    width: 16px; height: 16px; min-width: 16px;
    accent-color: var(--accent);
    cursor: pointer; margin: 0; vertical-align: middle; flex: none;
  }
  table { width: 100%; border-collapse: collapse; }
  th {
    text-align: left; padding: 10px 12px;
    font-size: 11px; color: var(--muted);
    text-transform: uppercase; letter-spacing: 0.5px;
    border-bottom: 1px solid var(--hair);
    /* v7.1.0: one line per header. A sortable header carries its ↕/▲/▼ glyph
       as a trailing span, and in a squeezed column the glyph wrapped onto a
       line of its own ("SCORE" over "↕", "LAST" over "SEEN"), so header rows
       came out two lines tall on most data tables. */
    white-space: nowrap;
  }
  td { padding: 12px; font-size: 13px; border-bottom: 1px solid var(--hair-2); vertical-align: middle; }
  tr:last-child td { border-bottom: none; }
  /* v3.13.0: project-wide cap — no table card grows past ~15 rows; beyond that
   * it scrolls inside the card with a sticky header. Paginated tables (15/page)
   * fit under the cap so they don't get a redundant inner scrollbar, and their
   * pager renders as a sibling *outside* the card so it's never hidden. This is
   * the single guard that keeps every page short instead of kilometers of rows. */
  .table-card { background: var(--surface); border: 1px solid var(--hair); border-radius: 11px; overflow: auto; max-height: 480px; contain: paint; }  /* v6: design panel radius */
  .table-card thead th { position: sticky; top: 0; background: var(--surface); z-index: 1; box-shadow: inset 0 -1px 0 var(--border); }
  /* v3.13.0: discovery scan results render into a bare div (not a .table-card),
   * so cap it too — a /24 scan can return 254 hosts. */
  #discovery-body { max-height: 620px; overflow: auto; }
  .mon-status {
    display: inline-flex; align-items: center; gap: 6px;
    padding: 3px 9px; border-radius: 20px;
    font-size: 11px; font-weight: 600; letter-spacing: 0.3px;
  }
  .mon-status.up   { background: rgba(34,197,94,0.1);  color: var(--green); border: 1px solid rgba(34,197,94,0.2); }
  .mon-status.down { background: rgba(239,68,68,0.1);  color: var(--red-on-tint);   border: 1px solid rgba(239,68,68,0.2); }
  .mon-status.unk  { background: rgba(100,116,139,0.1); color: var(--muted); border: 1px solid var(--border); }
  .journal-wrap {
    background: #070910;
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: 16px;
    max-height: 360px;
    overflow-y: auto;
    font-family: var(--font-mono);
    font-size: 12px;
    line-height: 1.65;
    color: #94a3b8;
    white-space: pre-wrap;
    word-break: break-all;
  }
  .journal-wrap:empty::after { content: '(no journal data yet)'; color: var(--muted); }
  .sysinfo-row {
    display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 20px;
  }
  .sysinfo-pill {
    background: var(--surface2);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: 8px 14px;
    font-size: 12px;
  }
  .sysinfo-pill .label { color: var(--muted); margin-bottom: 3px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.4px; }
  .sysinfo-pill .value { font-weight: 600; font-family: var(--font-mono); }
  .settings-section {
    background: var(--surface);
    border: 1px solid var(--hair);
    border-radius: 11px;   /* v6: design panel radius */
    padding: 24px;
    margin-bottom: 16px;   /* v4.1.0: align with the 16px card rhythm */
  }
  .settings-section h3 { font-size: 13px; font-weight: 620; margin-bottom: 6px; }
  .settings-section .hint { font-size: 12px; color: var(--muted); margin-bottom: 20px; line-height: 1.5; }
  .settings-row { display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap; }
  .settings-row .form-group { flex: 1; min-width: 160px; margin: 0; }
  /* v3.13.0: a settings-row whose label is a DIRECT child is a single labelled
   * field (audit-forwarding, retention, scheduled backup, …) — stack it
   * label-above-input instead of side-by-side, matching every other form.
   * Multi-field rows nest their labels inside .form-group, so they don't match
   * and keep their horizontal layout. */
  .settings-row:has(> .form-label) { flex-direction: column; align-items: stretch; gap: 6px; }
  .settings-row:has(> .form-label) > .form-label { margin-bottom: 0; }
  /* v5.5.0: a width-capped <select> (mw-160/mw-200) is JS-wrapped in a .rp-ddwrap
     span; that span is a direct flex child of this column-stretch row, so
     align-items:stretch blew the wrapper out to full row width — the capped select
     then sat left-aligned inside a full-width box (and the industrial accent border
     painted across the whole row). Opt ONLY the capped wrappers out of the stretch
     so they hug their select; full-width selects are untouched. */
  .settings-row:has(> .form-label) .rp-ddwrap:has(> .mw-160),
  .settings-row:has(> .form-label) .rp-ddwrap:has(> .mw-200) { align-self: flex-start; }
  /* v6.0.1: a button in one of these column-stretch rows was blown out to full
     row width by align-items:stretch (flex:0 0 auto can't beat a cross-axis
     stretch). Hug the content instead — buttons stay tidy. */
  .settings-row:has(> .form-label) > :is(.btn-primary, .btn-secondary, .btn-icon) { align-self: flex-start; }
  .user-row td:last-child { text-align: right; }
  .user-actions { display: flex; gap: 8px; justify-content: flex-end; }
  .toast-container { position: fixed; bottom: 24px; right: 24px; z-index: 300; display: flex; flex-direction: column; gap: 8px; }
  /* v4.3.0: connectivity banner — fixed, bottom-centred, above toasts.
     Shown only while /api fetches fail at the network level. */
  .net-down-banner {
    position: fixed;
    bottom: 16px;
    left: 50%;
    transform: translateX(-50%);
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 16px;
    background: var(--red-dim);
    border: 1px solid var(--red);
    color: var(--text);
    border-radius: 8px;
    font-size: 13px;
    z-index: 10001;
    box-shadow: var(--shadow-1);
  }
  .net-down-banner svg { flex-shrink: 0; color: var(--red); }
  .toast {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: 14px 18px;
    font-size: 13px;
    display: flex; align-items: center; gap: 10px;
    min-width: 260px;
    transform: translateX(120%);
    transition: transform 0.3s cubic-bezier(0.34,1.56,0.64,1);
    box-shadow: var(--shadow-3);
  }
  .toast.show    { transform: translateX(0); }
  .toast.success { border-left: 3px solid var(--green); }
  .toast.error   { border-left: 3px solid var(--red); }
  .toast.info    { border-left: 3px solid var(--accent); }
  /* v6.4.3: 'warning' was a live toast type with no rule — 17 call sites
     rendered as an unstyled info toast, so a partial success looked
     identical to a clean one. */
  .toast.warning { border-left: 3px solid var(--amber); }
  /* v6.3.1: severity-tint treatment (same idiom as .adv-sev-* / .board-tile). */
  @supports (background: color-mix(in srgb, red 6%, white)) {
    .toast.success { background: color-mix(in srgb, var(--green) 8%, var(--surface)); }
    .toast.error   { background: color-mix(in srgb, var(--red) 8%, var(--surface)); }
    .toast.info    { background: color-mix(in srgb, var(--accent) 8%, var(--surface)); }
    .toast.warning { background: color-mix(in srgb, var(--amber) 8%, var(--surface)); }
  }
  .toast-icon    { font-size: 16px; }
  /* v6.3.0 (UX wave 1): inline action button — the "Undo" pattern */
  .toast-action {
    margin-left: auto;
    background: none;
    border: 1px solid var(--hair);
    border-radius: 7px;
    color: var(--accent-ink);
    font-size: 13px;
    font-weight: 600;
    padding: 4px 10px;
    cursor: pointer;
    flex-shrink: 0;
  }
  .toast-action:hover { background: var(--accent-soft, rgba(94,141,255,0.12)); }
  /* ── v6.3.0 (UX wave 10): informative eye candy ──────────────────────────
     Motion = meaning: only changes/criticals animate; everything sits behind
     the prefers-reduced-motion override further down. */
  .page.active { animation: rp-page-in .12s ease-out; }
  @keyframes rp-page-in { from { opacity: 0; transform: translateY(4px); } }
  .status-dot { animation: rp-breathe 3s ease-in-out infinite; }
  @keyframes rp-breathe { 0%, 100% { opacity: 1; } 50% { opacity: .55; } }
  tr.alert-crit > td:first-child { box-shadow: inset 3px 0 0 var(--red); }
  tr.alert-crit { animation: rp-crit-pulse 1.4s ease-in-out 2; }
  @keyframes rp-crit-pulse { 50% { background: rgba(220, 60, 60, 0.07); } }
  tr.row-flash { animation: rp-row-flash 1.2s ease-out 1; }
  @keyframes rp-row-flash { 0% { background: var(--accent-soft); } 100% { background: transparent; } }
  tbody.first-paint tr { animation: rp-row-in .25s ease-out backwards; }
  tbody.first-paint tr:nth-child(1) { animation-delay: 0ms; }
  tbody.first-paint tr:nth-child(2) { animation-delay: 25ms; }
  tbody.first-paint tr:nth-child(3) { animation-delay: 50ms; }
  tbody.first-paint tr:nth-child(4) { animation-delay: 75ms; }
  tbody.first-paint tr:nth-child(5) { animation-delay: 100ms; }
  tbody.first-paint tr:nth-child(n+6) { animation-delay: 120ms; }
  @keyframes rp-row-in { from { opacity: 0; transform: translateY(3px); } }
  .stat-delta-up { color: var(--red); font-size: 10px; margin-left: 3px; }
  .stat-delta-down { color: var(--green); font-size: 10px; margin-left: 3px; }
  #fleet-pulse { display: inline-block; vertical-align: middle; opacity: .8; }
  .pos-rel { position: relative; }
  .hh-bar-tick { position: absolute; top: -2px; bottom: -2px; width: 2px; background: var(--amber, #d29922); opacity: .65; }
  .hh-bar-tick.crit { background: var(--red); }
  .alert-heat-cell { width: 10px; height: 10px; border-radius: 2px; background: var(--hair); display: inline-block; }
  .alert-heat-cell.heat-l1 { background: var(--accent); opacity: .25; }
  .alert-heat-cell.heat-l2 { background: var(--accent); opacity: .45; }
  .alert-heat-cell.heat-l3 { background: var(--accent); opacity: .7; }
  .alert-heat-cell.heat-l4 { background: var(--accent); opacity: 1; }
  .alert-heat-row { display: flex; gap: 3px; flex-wrap: wrap; align-items: center; }
  .rp-empty-art { display: block; margin: 0 auto 10px; opacity: .5; }
  .rp-ring { transform: rotate(-90deg); }
  .rp-ring circle { fill: none; stroke-width: 3; }
  .rp-ring .rp-ring-bg { stroke: var(--hair); }
  .rp-ring .rp-ring-fg { stroke: var(--accent); transition: stroke-dashoffset .4s ease; }
  /* v6.3.0 (UX wave 9): undo-history popover (right-click the undo arrow) */
  #undo-hist-pop {
    position: fixed; z-index: 960; width: 250px;
    background: var(--surface); border: 1px solid var(--hair);
    border-radius: 11px; box-shadow: var(--shadow-3);
    padding: 10px 12px; max-height: 300px; overflow-y: auto;
  }
  .undo-hist-row {
    display: flex; align-items: center; gap: 7px; width: 100%;
    background: none; border: none; color: var(--text);
    font-size: 13px; padding: 4px 2px; cursor: pointer; text-align: left;
  }
  .undo-hist-row:hover { color: var(--accent-ink); }
  /* v6.3.0 (UX wave 9): sticky first column on scrollable tables */
  .scrollable-table-wrap.sticky-first th:first-child,
  .scrollable-table-wrap.sticky-first td:first-child {
    position: sticky; left: 0; background: var(--surface); z-index: 2;
  }
  [dir="rtl"] .scrollable-table-wrap.sticky-first th:first-child,
  [dir="rtl"] .scrollable-table-wrap.sticky-first td:first-child {
    left: auto; right: 0;
  }
  /* v6.3.0 (UX wave 9): reset-view row + alert-param human-unit hint */
  .tbl-reset-view { margin-top: 8px; width: 100%; justify-content: center; }
  .ap-human { display: block; font-size: 11px; margin-top: 2px; }
  /* v6.3.0 (UX wave 4): column show/hide popover */
  #tbl-cols-pop {
    position: fixed; z-index: 960; width: 220px;
    background: var(--surface); border: 1px solid var(--hair);
    border-radius: 11px; box-shadow: var(--shadow-3);
    padding: 10px 12px; max-height: 340px; overflow-y: auto;
  }
  #tbl-cols-pop.hidden { display: none; }
  .tbl-cols-row { display: flex; align-items: center; gap: 8px; padding: 3px 0; font-size: 13px; cursor: pointer; }
  /* v6.3.0 (UX wave 3): notification center — bell + popover */
  #toast-history-btn { position: relative; }
  #toast-history-btn.has-unread::after {
    content: ''; position: absolute; top: 3px; right: 3px;
    width: 6px; height: 6px; border-radius: 50%; background: var(--accent);
  }
  #toast-history-pop {
    position: fixed; top: 52px; right: 16px; z-index: 950;
    width: 320px; max-width: calc(100vw - 24px);
    background: var(--surface); border: 1px solid var(--hair);
    border-radius: 11px; box-shadow: var(--shadow-3);
    padding: 10px 12px;
  }
  #toast-history-pop.hidden { display: none; }
  .toast-hist-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
  .toast-hist-row {
    display: flex; align-items: baseline; gap: 8px;
    padding: 5px 2px; font-size: 13px;
    border-top: 1px solid var(--hair-2, var(--hair));
  }
  .toast-hist-row:first-child { border-top: none; }
  .toast-hist-ic { flex-shrink: 0; position: relative; top: 2px; }
  .toast-hist-row.th-error .toast-hist-ic { color: var(--red); }
  .toast-hist-row.th-success .toast-hist-ic { color: var(--green); }
  .toast-hist-msg { flex: 1; min-width: 0; overflow-wrap: anywhere; }
  .toast-hist-ts { flex-shrink: 0; font-size: 10px; color: var(--muted); }
  [dir="rtl"] #toast-history-pop { right: auto; left: 16px; }
  [dir="rtl"] #toast-history-btn.has-unread::after { right: auto; left: 3px; }
  /* v6.3.0 (UX wave 3): Alert parameters — modified-from-default markers */
  #settings-pane-alertparams .form-group { position: relative; }
  #settings-pane-alertparams .form-group.ap-modified input {
    border-left: 2px solid var(--accent);
  }
  [dir="rtl"] #settings-pane-alertparams .form-group.ap-modified input {
    border-left: 1px solid var(--border); border-right: 2px solid var(--accent);
  }
  .ap-reset { position: absolute; top: -2px; right: 0; padding: 1px 5px; }
  [dir="rtl"] .ap-reset { right: auto; left: 0; }
  #settings-pane-alertparams.ap-mod-only .form-group:not(.ap-modified) { display: none; }
  #settings-pane-alertparams.ap-mod-only .settings-section:not(:has(.form-group.ap-modified)) { display: none; }
  /* v6.3.0 (UX wave 2): async data-action buttons dim while their handler runs */
  .btn-inflight { opacity: .55; pointer-events: none; cursor: progress; position: relative; }
  /* v6.4.0: visible working indicator — dimming alone reads as "disabled",
     not "busy". A small spinner says work is happening under the hood. */
  .btn-inflight::after {
    content: ''; display: inline-block; width: 10px; height: 10px;
    margin-left: 6px; vertical-align: -1px; border-radius: 50%;
    border: 2px solid currentColor; border-top-color: transparent;
    animation: btn-spin .7s linear infinite;
  }
  @keyframes btn-spin { to { transform: rotate(360deg); } }
  /* v6.4.0: completion pulse — a brief accent flash when the async handler
     settles, so even a handler with no toast visibly "did something". */
  .btn-done { box-shadow: 0 0 0 2px var(--accent); transition: box-shadow .5s ease; }
  @media (prefers-reduced-motion: reduce) {
    .btn-inflight::after { animation: none; }
    .btn-done { box-shadow: none; }
  }
  /* v6.3.0 (UX wave 2): click-to-copy affordance */
  [data-copy] { cursor: copy; }
  /* v6.3.0 (UX wave 2): pager rows-per-page select — compact, pager-scale */
  .table-pager-size { width: auto; padding: 2px 6px; font-size: 11px; }
  /* v6.3.0 (UX wave 1b): topbar undo/redo arrows dim when their stack is empty */
  #topbar-undo[disabled], #topbar-redo[disabled] { opacity: .35; cursor: default; }
  /* v6.3.0 (UX wave 1): failed-load state with inline Retry */
  .error-state {
    display: flex; align-items: center; justify-content: center; gap: 10px;
    padding: 18px 12px; font-size: 13px;
  }
  /* v6.3.0 (UX wave 1): "N of M shown" chip above a filtered table */
  .table-filter-info {
    display: flex; align-items: center; gap: 10px;
    font-size: 12px; color: var(--muted);
    margin: 0 0 6px;
  }
  .refresh-bar { position: fixed; bottom: 0; left: 0; right: 0; height: 2px; background: var(--border); z-index: 50; }
  /* v3.8.0: the countdown bar drains via a single CSS animation per refresh
     cycle (set in startRefreshCycle), not a per-second JS width write. Fewer
     DOM mutations → fewer MutationObserver wakeups for form-filler extensions. */
  /* scaleX is compositor-only — animating width repainted the full-width
   * bar every second on every page (same class as the v3.14.0 pulse storm). */
  .refresh-progress { height: 100%; background: var(--accent); width: 100%;
                      transform-origin: left; transition: transform 1s linear; }
  .login-container {
    display: flex; flex-direction: column;
    align-items: center; justify-content: center;
    min-height: 100vh; padding: 24px;
    gap: 8px;
  }
  .login-card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 16px;
    padding: 40px;
    width: 100%;
    /* v2.0: widened from 400px to 480px so the 280px-wide logo fits with
       comfortable horizontal margin. The logo asset has rounded corners
       and a dark gradient baked in; the surrounding card uses --surface
       which sits right on top of --bg, so the logo's dark fill blends
       into the page rather than looking like a "black box on dark grey
       on darker grey" sandwich. If you ever supply a fully-transparent
       PNG, the card's --surface fill will show through cleanly. */
    max-width: 480px;
  }
  .login-logo  { display: flex; align-items: center; gap: 12px; margin-bottom: 32px; }
  /* v2.0 polish: the logo wrap sits ABOVE the .login-card as a sibling,
     not inside it. No background, no padding, no frame — the logo PNG
     has its own visual treatment baked in (rounded corners, dark
     gradient) and stands on its own against the page --bg. */
  .login-logo-wrap {
    display: flex;
    justify-content: center;
    background: transparent;
    margin: 0;
  }
  .login-logo-img {
    max-width: 320px;
    width: 100%;
    height: auto;
    display: block;
    background: transparent;
  }
  .login-title { font-size: 28px; font-weight: 700; margin-bottom: 6px; }
  .login-sub   { font-size: 13px; color: var(--muted); margin-bottom: 28px; }
  /* v5.0.0: login banner / security notice */
  .login-banner {
    font-size: 13px; line-height: 1.45; white-space: pre-wrap; word-break: break-word;
    background: var(--accent-soft); border: 1px solid var(--border);
    border-radius: 8px; padding: 10px 12px; margin: -16px 0 24px; color: var(--text);
  }
  /* v6.4.3: the four login buttons stacked at three different heights —
     39px (Sign in), 45.5px (the SSO/SAML <a>s) and 41px (the passkey
     <button>), on a card where they sit directly on top of each other. Two
     causes, both here: `border: none` meant the variants that add a 1px
     border grew by 2px, and no line-height meant an <a> inherited the body's
     1.5 while a <button> used `normal`. A transparent border equalises the
     box (.login-btn-oidc is later at equal specificity, so its real border
     still wins) and an explicit line-height stops the element type deciding
     the height. */
  .login-btn {
    width: 100%; background: var(--accent); color: var(--accent-contrast, #fff);
    border: 1px solid transparent; line-height: 1.15;
    padding: 12px; border-radius: 8px; font-size: 13px; font-weight: 600;
    cursor: pointer; transition: background 0.2s; margin-top: 8px;
    font-family: var(--font);
  }
  .login-btn:hover { background: var(--accent2); }
  .login-error { color: var(--red); font-size: 13px; margin-top: 12px; display: none; }
  .login-error.show { display: block; }

  /* v6.4.3: a field marked aria-invalid had NO visual state — the attribute was
   * being set for screen readers and a sighted operator saw nothing at all,
   * which is half of WCAG 2.1 SC 3.3.1 rather than all of it. One rule covers
   * every form: the attribute is already the single source of truth, so this
   * cannot drift out of sync with the JS the way a parallel .is-invalid class
   * would. The ring is intentional as well as the border — a border colour
   * alone fails SC 1.4.1 for anyone who cannot distinguish it. */
  [aria-invalid="true"] {
    border-color: var(--red) !important;
    box-shadow: 0 0 0 2px var(--red-soft);
  }
  [aria-invalid="true"]:focus,
  [aria-invalid="true"]:focus-visible { border-color: var(--red) !important; }
  /* v2.2.7: the 768px sidebar icon-rail block was removed here.
   * It collided with the 720px mobile drawer block below — both
   * applied under 720px, producing a wide drawer full of unlabelled
   * mystery icons. The drawer (≤720px) is now the single mobile
   * layout; 720–768px simply uses the normal desktop layout, which
   * is fine at that width. */
  @media (max-width: 600px) {
    .stats-row { grid-template-columns: 1fr; }
    .devices-grid { grid-template-columns: 1fr; }
    .modal { margin: 16px; padding: 24px; }
  }
  /* ── v1.8.4: Settings tabs + per-event toggle table ────────────────────── */
  .settings-tabs {
    display: flex;
    gap: 0;
    border-bottom: 1px solid var(--border);
    margin-bottom: 20px;
    overflow-x: auto;
  }
  /* v5.6.0: settings tabs styled like the sidebar nav — UPPERCASE, with a
     greyish (--surface2) highlight + white (--text) label on hover/focus/active,
     instead of the old accent underline. */
  .settings-tab {
    padding: 8px 14px;
    background: transparent;
    border: none;
    color: var(--muted);
    cursor: pointer;
    font-size: 12px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    white-space: nowrap;
    transition: color 0.15s, background 0.15s;
  }
  .settings-tab:hover { color: var(--text); background: var(--surface2); }
  .settings-tab.active { color: var(--text); background: var(--surface2); }
  .settings-pane { display: none; }
  .settings-pane.active { display: block; }

  /* v5.6.0: Knowledge base list/article styles. */
  .chip-sm { font-size: 11px; padding: 1px 10px; }
  /* v5.8.0 (B2.2): wrapping row for the 3-2-1 backup-rule leg pills. */
  .flex-wrap-6 { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
  /* v6.4.3: folded onto the page header rule. This was an 11px uppercase
     eyebrow — the idiom styles.css already says, at the .drawer-section-title
     retirement note, not to reintroduce: "Do not reintroduce uppercase eyebrow
     headers." It survived because that note removed one rule rather than the
     class of rule, and nothing forbids the shape. 13px/620, no tracking, no
     transform — the .section-title values. The margin is kept: it is list
     spacing, not skin. */
  .kb-cat-head { font-size: 13px; font-weight: 620;
    color: var(--muted); margin: 12px 0 4px; }
  .kb-cat-head:first-child { margin-top: 0; }
  .kb-item { display: flex; flex-direction: column; gap: 2px; width: 100%;
    text-align: left; background: transparent; border: none; border-radius: 6px;
    padding: 7px 9px; cursor: pointer; color: var(--text); }
  .kb-item:hover { background: var(--surface2, rgba(255,255,255,0.04)); }
  .kb-item-sel { background: var(--surface2, rgba(255,255,255,0.06));
    box-shadow: inset 2px 0 0 var(--accent); }
  .kb-item-title { font-size: 13px; font-weight: 500; }
  .kb-item-tags { display: flex; flex-wrap: wrap; gap: 4px; }
  .kb-view-head { display: flex; justify-content: space-between; align-items: flex-start;
    gap: 12px; margin-bottom: 12px; }
  .kb-view-body { font-size: 13px; line-height: 1.6; }

  .event-toggle-table {
    width: 100%;
    border-collapse: collapse;
  }
  .event-toggle-table tr {
    border-bottom: 1px solid var(--border);
  }
  .event-toggle-table tr:last-child { border-bottom: none; }
  .event-toggle-table td {
    padding: 12px 8px;
    vertical-align: middle;
  }
  .event-toggle-table td:first-child {
    font-family: var(--font-mono);
    font-size: 13px;
    color: var(--accent-ink);
    width: 180px;
  }
  .event-toggle-table td:nth-child(2) {
    font-size: 13px;
    color: var(--muted);
  }
  .event-toggle-table td:last-child {
    width: 60px;
    text-align: right;
  }
  .event-toggle-table .toggle-switch {
    accent-color: var(--accent);
    cursor: pointer;
  }
  .event-toggle-table .event-extra {
    margin-top: 6px;
  }

  /* ── v1.8.3: Calendar + Tasks ─────────────────────────────────────────── */
  .cal-day {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 6px;
    min-height: 92px;
    padding: 6px 6px 4px;
    cursor: pointer;
    transition: background 0.1s;
    display: flex;
    flex-direction: column;
    gap: 2px;
    overflow: hidden;
  }
  .cal-day:hover { background: var(--surface2); }
  /* v6.3.0 (a11y): current-month days get full ink and other-month days keep
     readable muted — the old whole-cell `opacity: .35` dim bottomed out at
     1.7:1 contrast (axe serious), and no opacity value can both de-emphasize
     and clear 4.5:1 when --muted is the base. Hierarchy now comes from
     ink-vs-muted instead of an opacity veil. */
  .cal-day-num { font-size: 13px; font-weight: 500; color: var(--text); }
  .cal-day.today .cal-day-num {
    font-weight: 600;
    background: var(--accent); color: var(--accent-contrast, #fff);
    width: 22px; height: 22px; border-radius: 50%;
    display: flex; align-items: center; justify-content: center;
    font-size: 12px;
  }
  .cal-day.other-month { background: transparent; }
  .cal-day.other-month .cal-day-num { color: var(--muted); font-weight: 400; }
  .cal-event {
    font-size: 11px; padding: 2px 6px; border-radius: 4px;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    color: white; font-weight: 500;
    cursor: pointer;
  }
  /* v4.3.0 perf: hover brightens via precomputed colors, not
     filter:brightness() — CSS filters force an expensive composite pass
     (worst in Firefox). The hover values are each base color +~15% light. */
  .cal-event.color-blue   { background: #3b82f6; }
  .cal-event.color-green  { background: #22c55e; }
  .cal-event.color-amber  { background: #f59e0b; color: #1a1a1a; }
  .cal-event.color-red    { background: #ef4444; }
  .cal-event.color-purple { background: #a855f7; }
  .cal-event.color-teal   { background: #14b8a6; }
  .cal-event.color-slate  { background: #64748b; }
  .cal-event.color-blue:hover   { background: #5e9bf8; }
  .cal-event.color-green:hover  { background: #44d97c; }
  .cal-event.color-amber:hover  { background: #f7b13c; }
  .cal-event.color-red:hover    { background: #f26a6a; }
  .cal-event.color-purple:hover { background: #b876f9; }
  .cal-event.color-teal:hover   { background: #2dd4bf; }
  .cal-event.color-slate:hover  { background: #7c8da3; }

  .kanban-column {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: 12px;
    display: flex;
    flex-direction: column;
    min-height: 400px;
  }
  .kanban-column.drag-over {
    background: var(--surface2);
    border-color: var(--accent);
  }
  .kanban-header {
    font-size: 13px; font-weight: 620; padding: 0 0 10px; display: flex;
    align-items: center; gap: 8px;
  }
  .kanban-count {
    background: var(--surface2); color: var(--muted);
    font-size: 11px; padding: 1px 7px; border-radius: 10px;
    font-weight: 500; letter-spacing: 0;
  }
  .kanban-list {
    display: flex; flex-direction: column; gap: 8px; flex: 1;
    /* box-overflow rule: ~15 cards then scroll (Closed column accumulates) */
    max-height: 340px; overflow-y: auto;
  }
  .kanban-card {
    background: var(--surface2);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 10px 12px;
    cursor: grab;
    user-select: none;
  }
  .kanban-card:hover { border-color: var(--accent); }
  .kanban-card.dragging { opacity: 0.4; cursor: grabbing; }
  .kanban-card-title {
    font-size: 13px; font-weight: 500; color: var(--text);
    margin-bottom: 4px; line-height: 1.35;
  }
  .kanban-card-meta {
    font-size: 11px; color: var(--muted);
    display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
  }
  .kanban-card-device {
    background: var(--accent); color: var(--accent-contrast, #fff);
    padding: 1px 6px; border-radius: 3px;
    font-size: 10px; font-family: var(--font-mono);
  }


/* ═══════════════════════════════════════════════════════════════════════
 * v2.2.1 — Design polish release
 *
 * Adds:
 *   - Refined status colour palette + critical-state pulse
 *   - Skeleton loaders (replaces centered spinners on initial load)
 *   - Sparkline mini-chart styling
 *   - Distro logo styling
 *   - Per-row hover action strip
 *   - ✨ identity extensions (AI button glow, thinking sparkles)
 *   - Typography: improved system font stack with font-feature-settings
 *   - Index dashboard tile styling
 *   - Drift diff renderer
 *
 * All additions respect prefers-reduced-motion. All colours fall back to
 * existing --green/--red/--amber when refined variants aren't used yet.
 * ═══════════════════════════════════════════════════════════════════════ */

:root {
  /* Refined status palette — softer shades for backgrounds, sharper for
   * indicator dots. Existing --green/--amber/--red are kept unchanged so
   * unchanged code keeps working. */
  --green-soft:  rgba(34, 197, 94, 0.12);
  --green-edge:  rgba(34, 197, 94, 0.35);
  --amber-soft:  rgba(245, 158, 11, 0.12);
  --amber-edge:  rgba(245, 158, 11, 0.35);
  --red-soft:    rgba(239, 68, 68, 0.13);
  --red-edge:    rgba(239, 68, 68, 0.40);
  --accent-soft: rgba(59, 126, 255, 0.12);
  --accent-edge: rgba(59, 126, 255, 0.40);

  /* Mono font for technical identifiers (device IDs, hashes, paths, code).
   * Uses a thoughtful system-mono stack; first match wins. JetBrains Mono
   * and Fira Code are common installs on dev laptops; ui-monospace is
   * Apple's modern system mono. */
  /* v6.0.0 ClarityMatters: the design's system mono (chosen-design --mono) */
  --font-mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
}

body.light {
  --green-soft:  rgba(34, 197, 94, 0.10);
  --green-edge:  rgba(34, 197, 94, 0.30);
  --amber-soft:  rgba(245, 158, 11, 0.10);
  --amber-edge:  rgba(245, 158, 11, 0.30);
  --red-soft:    rgba(239, 68, 68, 0.10);
  --red-edge:    rgba(239, 68, 68, 0.30);
  --accent-soft: rgba(59, 126, 255, 0.08);
  --accent-edge: rgba(59, 126, 255, 0.25);
}

/* Better typography on the existing stack. Inter ships with stylistic
 * alternates that fix the awkward default "1", straight-sided "a", etc.
 * cv02/cv03/cv04/cv11 enable those; harmless on systems without Inter. */
body {
  /* Anchor the canonical BODY size once, document-wide. v7.1.0: one body
     size — 13px for prose, table cells, form fields and buttons alike (the
     v6 scale split body text across 12.5, 13 and 13.5). Components with an
     explicit size are unaffected. */
  font-size: 13px;
  line-height: 1.5;
  font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11', 'ss01';
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* v4.9.0 ResolutionMatters #5: crisper kerning/ligatures on high-DPI displays. */
  text-rendering: optimizeLegibility;
}

/* v4.9.0 ResolutionMatters #5: render the inline Lucide-style SVG icons with
 * geometric precision so their strokes/curves stay sharp at every device pixel
 * ratio (the default 'auto' can round curve control points on fractional DPRs). */
svg {
  shape-rendering: geometricPrecision;
}

/* Apply mono font to anywhere code/hash/path-like content lives.
 * Existing inline style="font-family: var(--font-mono)" still works; this is
 * an enhancement for consistency. */
code, pre, .mono, [data-mono], kbd,
.audit-log td:nth-child(4),
.cmd-output, .journal-wrap,
input.form-input[type="text"][data-token],
.device-id, .cve-id, .file-path, .hash-display {
  font-family: var(--font-mono);
  font-feature-settings: 'liga' 0, 'calt' 0;  /* turn off mono ligatures */
}

/* ─── Status pulse: critical-only ──────────────────────────────────────
 * Subtle pulsing dot for critical states. NOT applied to
 * warning states — at 9 devices + 17 monitors + 30 services, a single
 * page can have dozens of yellow dots and pulsing them all would be
 * a visual disaster. */
@keyframes rp-pulse-critical {
  /* v3.14.0 perf: opacity instead of box-shadow — compositor-only, no repaint. */
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.4; }
}
.status-critical, .status-dot.critical {
  animation: rp-pulse-critical 2.4s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
  .status-critical, .status-dot.critical { animation: none; }
}

/* Pill-shaped status badges. Use the soft+edge palette. */
.status-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 9px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 500;
  line-height: 1.4;
  white-space: nowrap;
}
.status-pill.ok       { background: var(--green-soft); color: var(--green);  border: 1px solid var(--green-edge); }
.status-pill.warn     { background: var(--amber-soft); color: var(--amber);  border: 1px solid var(--amber-edge); }
.status-pill.critical { background: var(--red-soft);   color: var(--red);    border: 1px solid var(--red-edge); }
.status-pill.neutral  { background: var(--surface2);   color: var(--muted);  border: 1px solid var(--border); }
.status-pill.info     { background: var(--accent-soft);color: var(--accent-ink); border: 1px solid var(--accent-edge); }
.status-pill::before {
  content: '';
  display: inline-block;
  width: 6px; height: 6px;
  border-radius: 50%;
  background: currentColor;
}

/* Per-event severity dot in the device drawer's Events tab
   (`<span class="activity-dot ${cls}">`, the second EVENT_CLASS consumer).
   This class had NO rule at all, so the span was a zero-size empty inline
   element — every event in that tab looked identical, critical and
   informational alike. Same tokens as .status-pill above; the row is
   `.isl-642` (flex, align-items:flex-start), hence flex-basis + the margin
   that centres the dot on the first 12px/1.5 text line. */
.activity-dot {
  display: inline-block;
  flex: 0 0 auto;
  width: 6px; height: 6px;
  margin-top: 6px;
  border-radius: 50%;
  background: var(--muted);
}
.activity-dot.ok       { background: var(--green); }
.activity-dot.warn     { background: var(--amber); }
.activity-dot.critical { background: var(--red); }
.activity-dot.info     { background: var(--accent); }
.activity-dot.neutral  { background: var(--muted); }

/* ─── Skeleton loaders ─────────────────────────────────────────────────
 * Show the shape of incoming content rather than a centred spinner. The
 * shimmer is the visual cue that data is loading. */
/* v4.3.0 perf: the shimmer is a transform-animated ::after overlay, NOT an
 * animated background-position — background-position re-rasterizes the
 * gradient every frame (notably slow in Firefox), while translateX runs on
 * the compositor. Don't move the animation back onto background-position. */
@keyframes rp-shimmer {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(100%); }
}
/* v6.3.0 (stat-tile eye candy): count-up + sparkline + delta + state tone, applied
   to every .stat-card via one observer. All motion is transform/opacity only and
   respects prefers-reduced-motion (see the reduce block above). */
.stat-card { cursor: default; }
.stat-card[data-action], .stat-card[data-nav] { cursor: pointer; }
/* (hover-lift + transition live in the consolidated .stat-card block lower down) */
.stat-meta { display: flex; align-items: center; gap: 6px; margin-top: 5px; min-height: 16px; }
.stat-spark { display: block; }
.stat-delta { font-size: 10px; font-weight: 600; line-height: 1; }
.stat-delta.sd-good { color: var(--green); }
.stat-delta.sd-bad  { color: var(--red); }
.stat-delta.sd-flat { color: var(--muted); }
/* State-reactive: a "bad" tile (e.g. Offline) turns red + one subtle pulse when > 0. */
.stat-card.stat-alert .stat-value { color: var(--red); }
.stat-card.stat-alert .stat-icon { animation: stat-pulse 1.4s ease-out 2; }
@keyframes stat-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.12); } }
/* Fleet proportion bar (online / offline / unmonitored share). */
.stat-propbar { display: flex; height: 5px; border-radius: 3px; overflow: hidden; background: var(--hair-2); margin: 4px 0 14px; }
.stat-propbar > span { height: 100%; transition: width .5s ease; }
.stat-propbar > .pp-online { background: var(--green); }
.stat-propbar > .pp-offline { background: var(--red); }
.stat-propbar > .pp-unmon { background: var(--muted); opacity: .5; }
@media (prefers-reduced-motion: reduce) {
  .stat-card[data-action]:hover, .stat-card[data-nav]:hover { transform: none; }
  .stat-card.stat-alert .stat-icon { animation: none; }
  .stat-propbar > span { transition: none; }
}

/* v6.3.0: indeterminate progress bar (on-demand backup run feedback). Animates
   transform only (no left/width) so it never triggers layout/paint — see
   test_v430_perfweb. */
.progress-indet { height: 4px; background: var(--hair-2); border-radius: 2px; overflow: hidden; position: relative; margin: 8px 0; }
.progress-indet::after { content: ''; position: absolute; top: 0; left: 0; width: 40%; height: 100%; background: var(--accent); border-radius: 2px; animation: rp-indet 1.1s ease-in-out infinite; }
@keyframes rp-indet { 0% { transform: translateX(-100%); } 100% { transform: translateX(350%); } }
.skeleton {
  position: relative;
  overflow: hidden;
  background: var(--surface2);
  border-radius: 4px;
  display: inline-block;
}
.skeleton::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent 25%, var(--border) 50%, transparent 75%);
  animation: rp-shimmer 1.4s ease-in-out infinite;
  will-change: transform;
}
@media (prefers-reduced-motion: reduce) {
  .skeleton::after { animation: none; }
}
.skeleton-line   { height: 12px; width: 100%; margin: 4px 0; }
.skeleton-line.short { width: 40%; }
.skeleton-line.med   { width: 65%; }
.skeleton-line.long  { width: 90%; }
.skeleton-circle { width: 14px; height: 14px; border-radius: 50%; vertical-align: middle; }
.skeleton-row td  { padding: 12px 10px; }
/* The div-container sibling of .skeleton-row — emitted by _skeletonBlock() for
   modal bodies / cards / lists. Without a rule the wrapper is a bare block and
   its inline-block .skeleton-line children pick up line-box leading, so the gap
   between bars varies with the inherited line-height instead of matching the
   6px rhythm the static skeleton markup in index.html uses. */
.skeleton-block  { display: flex; flex-direction: column; gap: 6px; }
.skeleton-block > .skeleton-line { margin: 0; }
.skeleton-card   { padding: 18px; border: 1px solid var(--border);
                   border-radius: 8px; background: var(--surface); }
@media (prefers-reduced-motion: reduce) {
  .skeleton { animation: none; background: var(--surface2); }
}

/* ─── Sparkline mini-charts ────────────────────────────────────────────
 * 60×16 inline SVG; the path/dot colours are set per-element via
 * stroke/fill so the same sparkline component can show green/amber/red
 * trends. */
.sparkline {
  display: inline-block;
  vertical-align: middle;
  margin-left: 6px;
  opacity: 0.85;
  transition: opacity 0.15s ease;
}
.sparkline:hover { opacity: 1; }
.sparkline path { fill: none; stroke-width: 1.4; stroke-linecap: round; stroke-linejoin: round; }
.sparkline .area { stroke: none; opacity: 0.18; }
/* v6.3.0: the gradient variant carries its own fade in its stops — the flat
   0.18 wash would double-dim it to near-invisible. */
.sparkline .area.grad { opacity: 1; }
.sparkline .dot  { stroke: none; }

/* ─── Distro logos ─────────────────────────────────────────────────────
 * 14×14 inline SVG next to device names. The fill colours are picked
 * to match each distro's brand mark — Ubuntu orange, Debian red,
 * Arch blue, etc. */
.distro-icon {
  display: inline-block;
  width: 14px; height: 14px;
  vertical-align: -2px;
  margin-right: 6px;
  flex-shrink: 0;
}

/* ─── ✨ identity extension ─────────────────────────────────────────────
 * The sparkle button gets a subtle animated glow when AI is reachable.
 * The .ai-thinking state replaces the spinner with three sparkles cycling
 * in opacity. AI-generated content gets a thin gradient accent on its
 * left edge so the operator can see at a glance what came from the model
 * vs what came from their data. */
.ai-btn {
  position: relative;
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease, opacity 0.15s ease;
}
.ai-btn.available::after {
  content: '';
  position: absolute;
  inset: -2px;
  border-radius: inherit;
  background: radial-gradient(circle at center,
    rgba(59, 126, 255, 0.18) 0%,
    transparent 65%);
  opacity: 0;
  /* v5.6.x perf: glow on hover/focus only, NOT an infinite loop. AI buttons
     appear on many pages; an always-running opacity+scale animation with a
     permanent `will-change` held a live compositor layer per button forever,
     keeping the GPU awake at idle. A transition fades it in on interaction —
     same "AI available" affordance, zero idle cost, no permanent layer. */
  transition: opacity 0.2s ease;
  pointer-events: none;
  z-index: -1;
}
.ai-btn.available:hover::after,
.ai-btn.available:focus-visible::after { opacity: 1; }
.ai-btn.local::after {
  /* Local Ollama-style glow — amber tint instead of accent blue */
  background: radial-gradient(circle at center,
    rgba(245, 158, 11, 0.16) 0%,
    transparent 65%);
}
@keyframes rp-ai-glow {
  0%, 100% { opacity: 0;   transform: scale(0.95); }
  50%      { opacity: 1;   transform: scale(1.05); }
}
@media (prefers-reduced-motion: reduce) {
  .ai-btn::after { animation: none; opacity: 0.4; }
}

/* Three-sparkle thinking indicator */
.ai-thinking {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  color: var(--accent-ink);
  font-size: 13px;
}
.ai-thinking .sparkle {
  display: inline-block;
  animation: rp-sparkle-cycle 1.6s ease-in-out infinite;
  will-change: opacity, transform;
}
.ai-thinking .sparkle:nth-child(2) { animation-delay: 0.4s; }
.ai-thinking .sparkle:nth-child(3) { animation-delay: 0.8s; }
@keyframes rp-sparkle-cycle {
  0%, 100% { opacity: 0.3; transform: scale(0.85); }
  50%      { opacity: 1;   transform: scale(1.1); }
}
@media (prefers-reduced-motion: reduce) {
  .ai-thinking .sparkle { animation: none; opacity: 0.85; }
}

/* AI-generated content marker: thin gradient on left edge */
.ai-content {
  position: relative;
  padding-left: 14px;
}
.ai-content::before {
  content: '';
  position: absolute;
  left: 4px;
  top: 6px;
  bottom: 6px;
  width: 2px;
  border-radius: 2px;
  background: linear-gradient(180deg, var(--accent), transparent);
}

/* v6.3.1: hail-mary log sweep modal ("Diagnose from logs") */
.log-sweep-file summary {
  cursor: pointer;
  font-size: 13px;
  padding: 4px 0;
}
.log-sweep-file summary code {
  font-family: var(--font-mono);
  font-size: 11px;
}
.log-sweep-pre {
  max-height: 220px;
  overflow: auto;
  font-family: var(--font-mono);
  font-size: 11px;
  line-height: 1.5;
  background: var(--surface-2, var(--surface));
  border: 1px solid var(--hair);
  border-radius: 8px;
  padding: 8px 10px;
  white-space: pre-wrap;
  word-break: break-word;
  margin: 4px 0 8px;
}
.log-sweep-ai {
  margin-top: 8px;
}
.triage-fb {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 10px;
  padding-top: 8px;
  border-top: 1px solid var(--hair);
}
.attack-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
/* v6.3.1: Log-rules device/global tabs (was inline cssText w/ pre-v6 colours) */
.rules-tab {
  border-radius: 11px 11px 0 0;
  background: var(--surface);
  border-bottom: 1px solid var(--hair);
}
.rules-tab.active {
  background: var(--accent-soft, var(--surface-2, var(--surface)));
  border-bottom-color: var(--accent);
  color: var(--accent-ink, inherit);
}
.attack-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 12px;
  text-decoration: none;
  color: inherit;
  padding: 2px 6px;
  border: 1px solid var(--hair);
  border-radius: 8px;
}
.attack-chip:hover { border-color: var(--accent); }

/* ─── Index dashboard tiles ────────────────────────────────────────────
 * Big-number summary tiles for the home page. Hover lifts subtly. */
.tile-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 14px;
  margin-bottom: 24px;
}
.tile {
  background: var(--surface);
  border: 1px solid var(--hair);
  border-radius: 11px;
  padding: 18px 20px;
  position: relative;
  transition: transform 0.15s ease, border-color 0.15s ease;
  overflow: hidden;
}
.tile[data-action] { cursor: pointer; }
.tile:hover {
  transform: translateY(-1px);
  border-color: var(--accent-edge);
}
.tile[data-action]:hover { transform: translateY(-2px); box-shadow: var(--shadow-1); }
@media (prefers-reduced-motion: reduce) {
  .tile:hover, .tile[data-action]:hover { transform: none; }
}
.tile-label {
  font-size: 11px;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin-bottom: 8px;
  font-weight: 500;
}
.tile-value {
  font-size: 28px;
  font-weight: 600;
  line-height: 1;
  color: var(--text);
  font-feature-settings: 'tnum' 1;   /* tabular numerals: aligned digits */
}
.tile-subtle {
  font-size: 12px;
  color: var(--muted);
  margin-top: 6px;
}
.tile.alert {
  border-color: var(--red-edge);
  background: linear-gradient(135deg, var(--surface), var(--red-soft));
}
.tile.alert .tile-value { color: var(--red); }
.tile.warn {
  border-color: var(--amber-edge);
  background: linear-gradient(135deg, var(--surface), var(--amber-soft));
}
.tile.warn .tile-value { color: var(--amber); }
.tile.ok {
  border-color: var(--green-edge);
}
.tile.ok .tile-value { color: var(--green); }
/* v6.3.0: tone-coloured icon chip, top-right — the persistent visual the hero
   tiles lacked (matches the stat-card icon aesthetic). */
.tile-ic {
  position: absolute; top: 14px; right: 14px;
  width: 30px; height: 30px; border-radius: 8px;
  display: flex; align-items: center; justify-content: center;
  background: var(--accent-soft); color: var(--accent-ink);
}
.tile-ic svg { width: 17px; height: 17px; }
.tile.ok .tile-ic    { background: var(--green-soft); color: var(--green); }
.tile.warn .tile-ic  { background: var(--amber-soft); color: var(--amber); }
.tile.alert .tile-ic { background: var(--red-soft);   color: var(--red); }
/* keep the number clear of the corner chip on narrow tiles */
.tile-label { padding-right: 38px; }

/* Two-column dashboard layout for activity + attention */
.dash-cols {
  display: grid;
  /* minmax(0,…) lets columns shrink below their content so nowrap rows
     (e.g. open-ticket titles) clip with an ellipsis instead of overflowing. */
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 22px;
  margin-bottom: 22px;
}
@media (max-width: 900px) {
  .dash-cols { grid-template-columns: 1fr; }
}
.dash-card {
  background: var(--surface);
  border: 1px solid var(--hair);
  border-radius: 11px;
  padding: 16px 18px;
  margin-bottom: 16px;   /* v6: design .grp rhythm */
}

/* v4.7.0: inside the .dash-cols grid the `gap` (22px) already spaces the cards;
   the card's own 16px margin-bottom stacked ON TOP of it when the grid wraps to
   one column (22 + 16 = 38px ≈ "double space"). Let the grid gap own it. */
.dash-cols > .dash-card { margin-bottom: 0; }
.dash-card h3, .dash-card h4,
.settings-section h3, .settings-section h4 {
  /* v6 (chosen-design .gt): sentence-case ink heading, not an eyebrow. Raw
     <h4> sub-headers (integrations/SNMP/CMDB cards) render on the canonical
     .section-title idiom instead of UA-default. */
  font-size: 13px;
  font-weight: 620;
  color: var(--text);
  margin: 0 0 12px 0;
}
/* v5.8.0 (B4.1) → v6: card/section headers are the canonical `.section-title`;
   they render as the design's 13px/620 sentence-case ink heading (chosen-design
   .gt) — the chamfered industrial tab and the old uppercase eyebrow are gone. */
.dash-card > .section-title:first-child,
.dash-card > div:first-child > .section-title:first-child {
  font-size: 13px;
  font-weight: 620;
  color: var(--text);
  margin: 0 0 12px 0;
}
.settings-section > .section-title:first-child {
  font-size: 13px;
  font-weight: 620;
  margin-bottom: 6px;
}

/* v6.0.1: card header BAND (design study #09). The canonical card header — a
   .section-title (or a .section-header title+action row) that is the FIRST child
   of a card — becomes a --surface2 strip with a bottom hairline, bled to the card
   edges. MUST come after the .section-title:first-child font/margin rules above
   (same specificity) or their `margin: 0 0 12px 0` wins and the band won't bleed.
   Scoped to the first-child header so sub-section titles / nested-wrapper titles
   stay plain. Negative margins match each container's padding; inner radius is the
   card radius minus the 1px border (10px). */
/* v6.4.3: three more wrappers get the band. 15 cards put their title inside a
   .row-8-center / .ai-tool-head / .drift-prof-head row instead of a
   .section-header, so they rendered the 13px/620 title with NO --surface2 strip
   and no bottom hairline — a visually different card type sitting among 335
   identical neighbours.

   The band is ADDED to those wrappers rather than renaming them to
   .section-header, which was the obvious fix and is wrong: .section-header is
   `justify-content: space-between` while .row-8-center is `gap: 8px;
   flex-wrap: wrap`. Renaming would have quietly re-aligned every one of those
   cards as a side effect of a cosmetic fix. The missing band is the defect; the
   alignment is intentional. */
.dash-card > .section-title:first-child,
.dash-card > .section-header:first-child,
.dash-card > .row-8-center:first-child,
.dash-card > .ai-tool-head:first-child,
.dash-card > .drift-prof-head:first-child,
.settings-section > .section-title:first-child,
.settings-section > .section-header:first-child,
.settings-section > .row-8-center:first-child,
.settings-section > .ai-tool-head:first-child {
  background: var(--surface2);
  border-bottom: 1px solid var(--hair);
  border-radius: 10px 10px 0 0;
  align-items: center;   /* keeps the title+action row centred inside the band */
}
.dash-card > .section-title:first-child,
.dash-card > .section-header:first-child,
.dash-card > .row-8-center:first-child,
.dash-card > .ai-tool-head:first-child,
.dash-card > .drift-prof-head:first-child {
  margin: -16px -18px 14px;
  padding: 11px 18px;
}
.settings-section > .section-title:first-child,
.settings-section > .section-header:first-child {
  margin: -24px -24px 18px;
  padding: 12px 24px;
}
/* v7.0.0: the compact override listed TWO of the five wrappers above, so
   .row-8-center / .ai-tool-head / .drift-prof-head kept the -16px/-18px bleed
   while the card around them shrank to 11px/13px — the band overhung its own
   card by 5px on each side and 5px at the top. Reported from the Drift
   profiles page, which uses .drift-prof-head. The base rule and this one must
   name the SAME five wrappers; tests/test_v700_card_band_parity.py fails when
   they diverge, because that divergence is invisible at the default density. */
body.density-compact .dash-card > .section-title:first-child,
body.density-compact .dash-card > .section-header:first-child,
body.density-compact .dash-card > .row-8-center:first-child,
body.density-compact .dash-card > .ai-tool-head:first-child,
body.density-compact .dash-card > .drift-prof-head:first-child {
  margin: -11px -13px 12px;
  padding: 9px 13px;
}
/* v6.2.3: a muted-ink .btn-icon fails WCAG contrast on the band's --surface2
   strip (axe: color-contrast, serious) — inside a band header, buttons use
   full ink. */
.dash-card > .section-header:first-child .btn-icon,
.settings-section > .section-header:first-child .btn-icon {
  color: var(--text);
}
.dash-feed-item {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 8px 0;
  border-bottom: 1px solid var(--border);
  font-size: 13px;
}
.dash-feed-item:last-child { border-bottom: none; }
.isl-751 { font-size: 11px; padding: 2px 8px; opacity: 0.7; color: #abb3bb; }
.isl-751:hover { opacity: 1; }
/* v6.4.0: referenced by the host-config logrotate/cron textareas since their
   introduction but never defined — config-file content wants the mono font
   (found by the CSS class-parity gate). */
.isl-752, .isl-753 { font-family: var(--font-mono); min-height: 120px; }
.isl-754 { width: 64px !important; text-align: center; }
.isl-755 { width: 120px !important; }
.isl-756 { flex: 1; min-width: 0; }
.isl-757 { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.isl-758 { margin-bottom: 10px; }
.isl-759 { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--muted); margin-bottom: 16px; cursor: pointer; }
/* isl-760: danger-toned clear button */
.isl-760 { color: var(--red) !important; border-color: rgba(239,68,68,0.35) !important; }
.isl-760:hover { background: rgba(239,68,68,0.08) !important; }
/* isl-761: manage link in dash-card header */
.isl-761 { font-size: 12px; }
/* isl-762: links widget body */
.isl-762 { display: flex; flex-direction: column; gap: 12px; }
/* isl-763: link card anchor wrapper */
.isl-763 { text-decoration: none; color: inherit; }
/* isl-764: compact link card */
.isl-764 { border-radius: 6px; padding: 8px 10px; border-width: 1px; border-style: solid; border-color: var(--border); transition: background 0.15s; cursor: pointer; }
.isl-764:hover { background: var(--surface); }
/* isl-765: link card title */
.isl-765 { font-size: 13px; font-weight: 500; }
/* isl-766: link card hostname */
.isl-766 { font-size: 11px; color: var(--muted); margin-top: 2px; font-family: var(--font-mono); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* isl-767: category group */
.isl-767 { display: flex; flex-direction: column; gap: 6px; }
/* isl-768: category label */
.isl-768 { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); }
/* isl-769: card grid within category */
.isl-769 { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 6px; }
.isl-770 { width: 100%; }
/* v3.4.0 RAG settings panel — utility classes (CSP: no inline style=) */
.isl-771 { vertical-align: -3px; margin-right: 6px; }
.isl-772 { vertical-align: -2px; margin-right: 6px; }
.isl-773 { margin-top: 10px; }
.isl-774 { margin-top: 6px; }
.isl-775 { margin-top: 12px; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.isl-776 { margin-top: 16px; border-top: 1px solid var(--border); padding-top: 14px; }
.isl-777 { font-size: 13px; }
.isl-778 { margin-bottom: 6px; }
.isl-779 { align-items: flex-end; }
.err-text { color: var(--red); }
/* recurring event glyph on calendar chips */
.cal-recur-glyph { font-size: 10px; opacity: 0.75; margin-right: 2px; vertical-align: middle; }
.cmd-badge.script { background: rgba(124,58,237,0.1); color: #a78bfa; }
.dash-feed-item .ts {
  font-size: 11px;
  color: var(--muted);
  white-space: nowrap;
  font-family: var(--font-mono);
}

/* ─── Status stripe (7-day uptime visualisation per device) ────────────
 * Compact horizontal cells coloured by historical online state. Hover
 * shows the timestamp for that cell. */
.status-stripe {
  display: inline-flex;
  gap: 2px;
  vertical-align: middle;
  margin-left: 8px;
}
.status-stripe .cell {
  display: inline-block;
  width: 8px;
  height: 14px;
  border-radius: 2px;
  background: var(--border);
  cursor: help;
  transition: transform 0.1s ease;
}
.status-stripe .cell:hover { transform: scaleY(1.15); }
.status-stripe .cell.up      { background: var(--green); }
.status-stripe .cell.partial { background: var(--amber); }
.status-stripe .cell.down    { background: var(--red); }
.status-stripe .cell.unknown { background: var(--border); }

/* ─── Drift diff renderer ──────────────────────────────────────────────
 * Unified diff view with syntax-style colouring. Used in the drift
 * detail modal when the operator clicks "Show diff" on a drifted file. */
.diff-view {
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 1.5;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 0;
  overflow-x: auto;
  max-height: 60vh;
  overflow-y: auto;
}
.diff-view .diff-line {
  display: flex;
  padding: 0 12px;
  white-space: pre;
}
.diff-view .diff-line.add {
  background: var(--green-soft);
  color: var(--green);
}
.diff-view .diff-line.del {
  background: var(--red-soft);
  color: var(--red);
}
.diff-view .diff-line.hunk {
  background: var(--surface2);
  color: var(--muted);
  font-style: italic;
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  margin: 4px 0;
}
.diff-view .ln {
  display: inline-block;
  width: 40px;
  text-align: right;
  padding-right: 12px;
  color: var(--muted);
  user-select: none;
  flex-shrink: 0;
}
.diff-view .marker {
  display: inline-block;
  width: 14px;
  text-align: center;
  flex-shrink: 0;
}

/* Compact empty-state styling with a primary action */
.empty-state-icon {
  font-size: 28px;
  margin-bottom: 12px;
  opacity: 0.4;
}
.empty-state-title {
  font-size: 13px;
  color: var(--text);
  margin-bottom: 6px;
  font-weight: 620;
}
.empty-state-body {
  font-size: 13px;   /* descriptive prose = body (13) */
  margin-bottom: 16px;
  max-width: 480px;
  margin-left: auto;
  margin-right: auto;
  line-height: 1.55;
}

/* ═══════════════════════════════════════════════════════════════════════
 * v2.2.1 — Mobile / narrow-viewport polish
 *
 * Tightens the dashboard for phone-sized screens (carrying the fleet in
 * your pocket during on-call). The desktop layout uses a fixed sidebar +
 * data-dense tables — both fail badly on mobile. This block:
 *
 *   - Collapses the sidebar to a top bar with a hamburger toggle
 *   - Stacks dashboard tiles vertically
 *   - Hides density-y columns (Hostname, Version, Group) on phones
 *   - Bumps tap targets to ≥36px
 *   - Disables the hover-action strip (no hover on touch; uses tap-on-row
 *     instead)
 *   - Keeps the status pulse + sparklines + distro logos working
 *
 * Breakpoint: 720px. Below that we go full mobile; tablet sizes are
 * mostly fine on the desktop layout with the existing responsive bits.
 * ═══════════════════════════════════════════════════════════════════════ */

@media (max-width: 720px) {
  /* Hide sidebar by default; reveal via the burger button.
   *
   * v2.2.7: this block is the mobile DRAWER. The separate 768px block
   * above is the tablet ICON-RAIL (56px, labels hidden). Below 720px
   * BOTH used to apply — the drawer became 240px wide but the 768px
   * rules kept the nav labels hidden and the buttons centred, and the
   * 72px top padding shoved every icon halfway down an empty panel.
   * That was the "wide useless drawer of mystery icons" bug.
   *
   * Fix: this block now explicitly restores everything the 768px
   * rail block strips — labels visible, buttons left-aligned, sane
   * padding. These overrides come later in the file so they win. */
  .sidebar {
    position: fixed;
    top: 48px;   /* under the v6 topbar */
    left: 0;
    bottom: 0;
    width: 248px;
    z-index: 810;
    transform: translateX(-100%);
    transition: transform 0.2s ease;
    overflow-y: auto;
    background: var(--side);
    border-right: 1px solid var(--hair);
    /* Override the 768px rail's `72px 6px 16px` — the drawer already
     * starts below the header, so a big top padding just pushes the nav
     * items down into dead space. Bottom 0: the sticky .side-foot owns
     * the bottom edge (a padding there floats it above the edge). */
    padding: 12px 10px 0;
  }
  /* Restore the nav labels + section headers the icon-rail hid. */
  .sidebar .nav-btn span,
  .sidebar .sidebar-group-toggle span { display: inline; }
  .sidebar .sidebar-label { display: block; }
  /* Left-align nav buttons again (the rail centred them as icons). */
  .sidebar .nav-btn {
    justify-content: flex-start;
    padding: 10px 12px;
    gap: 10px;
  }
  body.mobile-nav-open .sidebar { transform: translateX(0); }
  body.mobile-nav-open::after {
    content: '';
    position: fixed;
    inset: 0;
    /* v3.0.3: explicit resets for the properties that bleed in from the
       ambient-glow body::after rule (line ~250). That rule sets a fixed
       800×400 box with translateX(-50%) and pointer-events:none. Without
       these resets the scrim inherits the box and shows as a partial
       rectangle near the top of the screen instead of a full-viewport
       overlay, and tap-outside-to-close stops working because the ghost
       overlay can't receive pointer events. */
    width: auto;
    height: auto;
    transform: none;
    pointer-events: auto;
    background: rgba(0, 0, 0, 0.4);
    z-index: 800;
  }

  /* Show the burger button — hidden on desktop */
  .mobile-burger {
    display: inline-flex !important;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    margin-right: 8px;
    background: transparent;
    /* v3.0.3: no border. The previous 1px var(--border) drew a small
     * square around the icon that looked like a separate clickable
     * element next to the logo on mobile. The hamburger glyph is
     * visually distinct enough without a frame. */
    border: none;
    border-radius: 6px;
    color: var(--text);
    cursor: pointer;
  }

  /* Main content takes the full width — the drawer is an overlay, so
   * content must NOT keep a 56px left margin (the icon-rail's margin).
   * Without this override the content sat 56px in from the edge with
   * nothing in that strip. */
  main { padding: 12px !important; }
  .app-content { margin-left: 0 !important; }
  .container { padding: 0 !important; }

  /* Header gets compact — and spans the full width again (the sidebar is an
     overlay drawer here, so the main column IS the viewport). */
  header { margin-left: 0 !important; }
  .header-inner { gap: 6px !important; padding: 0 10px; }
  .status-bar { display: none; } /* "Live" indicator hidden on mobile */

  /* Dashboard tile grid: stack to one column */
  .tile-grid { grid-template-columns: 1fr !important; gap: 10px; }
  .tile { padding: 14px 16px; }
  .tile-value { font-size: 28px; }

  /* Two-col dashboard layout → stack */
  .dash-cols { grid-template-columns: 1fr; gap: 12px; }

  /* Hide hover affordances entirely — no hover on touch */
  tr.has-hover-actions .row-actions { display: none; }

  /* Devices minimal table: hide low-priority columns */
  .devices-minimal-table .dev-host-cell,
  .devices-minimal-table .dev-group-cell,
  .devices-minimal-table .dev-version-cell {
    display: none;
  }
  .devices-minimal-table th[data-col="hostname"],
  .devices-minimal-table th[data-col="group"],
  .devices-minimal-table th[data-col="version"] {
    display: none;
  }
  .devices-minimal-table .dev-name-cell { width: auto !important; }

  /* Drift / TLS / CVE tables: reduce padding, allow horizontal scroll */
  .data-table { font-size: 12px; }
  .data-table th, .data-table td { padding: 6px 4px !important; }
  .table-card { overflow-x: auto; }

  /* Device cards in non-minimal density: 1 per row */
  .device-card { width: 100% !important; margin-bottom: 10px; }
  .device-meta { grid-template-columns: 1fr 1fr; }

  /* Modal: nearly full-screen on mobile */
  .modal { width: 96vw !important; max-width: none !important; }
  .modal-overlay { padding: 6px !important; }

  /* Bigger tap targets */
  .btn-icon, .nav-btn, button {
    min-height: 36px;
  }
  .row-actions button { min-height: 0; }  /* hidden anyway */

  /* Sidebar group items: more vertical space */
  .nav-btn { padding: 10px 12px; }
}

/* Hide burger on desktop */
.mobile-burger { display: none; }

/* Small additional polish for in-between sizes */
@media (min-width: 721px) and (max-width: 980px) {
  .tile-grid { grid-template-columns: repeat(2, 1fr); }
}

/* ─── v2.2.5: capped-height tables with internal scroll ─────────────────
 *
 * When a table has >20 rows, switch to a fixed-height container with
 * internal scrolling rather than letting the page grow to several
 * thousand pixels. The thead becomes sticky so column headers stay
 * pinned while you scroll. ~20 rows at the existing 32px row height
 * works out to roughly 720px including the header.
 *
 * Applied to the `.scrollable-table-wrap` class on the wrapper div.
 * JS adds this class only when the table row count exceeds the
 * threshold — so small tables still render full-height as before.
 */
.scrollable-table-wrap {
  max-height: 720px;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: 6px;
  contain: paint;
  /* The thead inside this wrap gets sticky top:0 — backgrounds need
   * to be opaque so rows scrolling underneath don't show through. */
}
.scrollable-table-wrap thead th {
  position: sticky;
  top: 0;
  background: var(--surface);
  z-index: 1;
  /* The default thead-row border-bottom now needs to also be a
   * box-shadow because position:sticky removes it from layout flow. */
  box-shadow: inset 0 -1px 0 var(--border);
}

/* Custom scrollbar tuned to the dark theme — chunkier than the OS
 * default so it's findable on a long table without being intrusive. */

/* ── Global page scrollbar (critical for PWA/installed app mode) ──────── */
:root {
  scrollbar-color: var(--border) transparent;  /* Firefox */
  scrollbar-width: thin;
}
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: var(--border2, rgba(255,255,255,0.12));
  border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
  background: var(--muted, #8892aa);
}
::-webkit-scrollbar-corner { background: transparent; }


.scrollable-table-wrap::-webkit-scrollbar { width: 10px; }
.scrollable-table-wrap::-webkit-scrollbar-track {
  background: var(--surface);
  border-radius: 0 6px 6px 0;
}
.scrollable-table-wrap::-webkit-scrollbar-thumb {
  background: var(--border);
  border-radius: 5px;
  border: 2px solid var(--surface);
}
.scrollable-table-wrap::-webkit-scrollbar-thumb:hover {
  background: var(--muted);
}

/* v4.9.0: DNS record content cell — TXT records (DKIM/SPF/DMARC keys) can run
 * to hundreds of chars and would otherwise blow out the table width/height.
 * Cap it to a scrollable box; full value is on the title= tooltip. */
.dns-content-cell {
  display: inline-block;
  max-width: 460px;
  max-height: 88px;
  overflow: auto;
  word-break: break-all;
  white-space: pre-wrap;
  vertical-align: top;
}

/* v4.9.0 ResolutionMatters: authoritative-vs-public resolve panel — two columns
 * that stack on narrow viewports. */
.dns-resolve-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 16px;
}

/* v4.9.0: DNS "import from agent" device typeahead (never a dropdown — device
 * pickers are always a search box at fleet scale). Results float under the input. */
.dns-agent-search-wrap { position: relative; display: inline-block; }
.dns-agent-results {
  position: absolute;
  z-index: 60;
  top: 100%;
  left: 0;
  margin-top: 4px;
  min-width: 280px;
  max-height: 300px;
  overflow: auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 8px;
  box-shadow: var(--shadow-2);
}

/* v2.2.5: card-grid scroll wrap (companion to .scrollable-table-wrap).
 * Same threshold logic in JS — only applied when device count > 20. */
.scrollable-grid-wrap {
  max-height: 76vh;
  overflow-y: auto;
  padding: 8px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg);
}
.scrollable-grid-wrap::-webkit-scrollbar { width: 10px; }
.scrollable-grid-wrap::-webkit-scrollbar-track {
  background: transparent;
}
.scrollable-grid-wrap::-webkit-scrollbar-thumb {
  background: var(--border);
  border-radius: 5px;
  border: 2px solid var(--bg);
}
.scrollable-grid-wrap::-webkit-scrollbar-thumb:hover {
  background: var(--muted);
}

/* ═══════════════════════════════════════════════════════════════════════
 * v2.2.6 — z-index normalisation + mobile modal fixes
 *
 * The stacking order had grown organically and collided on mobile —
 * "windows over each other". Notably the device dropdown menu sat at
 * z-index 10000, ABOVE modal overlays at 200, so an open dropdown bled
 * through any modal. And there was no body-scroll lock, so the page
 * scrolled behind open modals on touch.
 *
 * Clean tier scale (low → high):
 *   base content      1
 *   sidebar (desktop) 90
 *   header            100
 *   dropdowns         200   ← was 10000, now firmly BELOW modals
 *   mobile nav        800 / 810
 *   modal (base)      1000
 *   modal (nested)    1100  ← drift diff over drift detail
 *   toast             2000
 *
 * These rules come last in the file so they win over the earlier
 * declarations without needing !important everywhere.
 * ═══════════════════════════════════════════════════════════════════════ */

/* Row menus must sit below modals — an open row menu bleeding through a
 * modal was the most visible desktop+mobile stacking bug. */
#row-more-pop { z-index: 200; }

/* Modal overlays — base tier. */
.modal-overlay { z-index: 1000; }
/* The confirm/prompt dialog must sit ABOVE any modal it's launched from
   (delete-ticket, drift-accept, etc.) regardless of DOM order. */
#ui-prompt-modal { z-index: 1100; }

/* Mobile nav drawer + its dim overlay — above header, below modals. */
@media (max-width: 720px) {
  .sidebar                      { z-index: 810; }
  body.mobile-nav-open::after   { z-index: 800; }
}

/* Toasts always on top — they're transient and must never be hidden. */
.toast-container { z-index: 2000; }

/* v3.0.4: mobile-only ✕ close button inside the sidebar drawer.
 * Hidden by default; the @media (max-width: 720px) rule below shows it.
 * Has its own z-index above the sidebar so taps reliably land on it
 * even if a nav-btn renders close to the top edge. */
.sidebar-mobile-close {
  display: none;
}
@media (max-width: 720px) {
  /* v3.0.4 (iter 6): on mobile, the sidebar is an overlay drawer that
   * closes via tap-outside (handled by the document-level click handler
   * in app.js). Both the ✕ close button AND the Collapse button are
   * hidden — neither makes sense in the drawer UX. ✕ was redundant
   * with the scrim-tap, Collapse never worked here (the sidebar is
   * out of flow, not docked). */
  .sidebar-mobile-close { display: none !important; }
  .sidebar-collapse-btn { display: none !important; }
}

/* v3.0.4 (iter 5+6): PWA / installed-app standalone mode at >720px.
 *
 * When the dashboard is launched as an installed PWA at a width above
 * the mobile breakpoint, the sidebar should be docked (not an overlay
 * drawer) and the Collapse button should work at any of those widths —
 * including the 721–768px gap where the default desktop rules
 * (min-width: 769px) don't apply. The ✕ close button is suppressed:
 * collapse is the docked-sidebar's equivalent.
 *
 * At ≤720px (mobile-shaped PWA window, e.g. a phone home-screen app)
 * the mobile rules above still apply — drawer UX, no buttons,
 * scrim-tap to dismiss.
 */
@media (not (display-mode: browser)) and (min-width: 721px) {
  .sidebar-mobile-close { display: none !important; }
  .sidebar-collapse-btn { display: block; }
  /* v7.0.0: the collapse-rail rules that used to be duplicated here are gone.
     They lived in this standalone-only block because the desktop copy started
     at 769px and missed the 721–768px gap — patching the gap for the installed
     app while leaving the browser broken. The desktop block now starts at
     721px itself, so it covers every display mode and there is one copy. */
}

/* v3.3.x: docked-sidebar clipping fixes (browser + installed PWA).
 *
 * Whenever the sidebar is docked (≈220px, every width >720px), two
 * layouts don't subtract that 220px:
 *
 *  1. The device-table column-drop breakpoints below key off the
 *     *viewport* width, not the sidebar-reduced table area. In a narrow
 *     window too few columns are hidden, so table-layout: fixed shrinks
 *     every column proportionally and the 90px Status pill collapses to
 *     a lone "…". Fix: shed low-priority columns ~200px earlier while
 *     docked, so the essential columns never get squeezed. Applied in
 *     ALL contexts (browser too) — a narrow window is narrow regardless
 *     of how it got that way; the original display-mode: standalone
 *     scope missed minimal-ui installs and the plain browser entirely.
 *
 *  2. The "MCP Confirmations" nav row is tight inside the 220px sidebar;
 *     the scrollbar tips its count badge past the edge. Fix: pin the
 *     badge (flex-shrink:0) and let the label ellipsis instead. This one
 *     v4.7.0: applied in ALL display modes (dropped the display-mode gate) so
 *     the installed PWA and the plain browser clip nav labels IDENTICALLY — the
 *     PWA-only scope is exactly what made the PWA truncate labels the browser
 *     showed in full. With the 240px sidebar, truncation should rarely trigger
 *     at all; when it must, both modes now behave the same. */
@media (min-width: 721px) {
  body:not(.sidebar-collapsed) .nav-btn > span:not(.nav-badge) {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  body:not(.sidebar-collapsed) .nav-badge { flex-shrink: 0; }
}
@media (min-width: 721px) and (max-width: 1480px) {
  body:not(.sidebar-collapsed) .devices-minimal-table .dev-host-cell,
  body:not(.sidebar-collapsed) .devices-minimal-table th[data-col="hostname"] { display: none; }
}
@media (min-width: 721px) and (max-width: 1280px) {
  body:not(.sidebar-collapsed) .devices-minimal-table .dev-version-cell,
  body:not(.sidebar-collapsed) .devices-minimal-table th[data-col="version"] { display: none; }
}
@media (min-width: 721px) and (max-width: 1120px) {
  body:not(.sidebar-collapsed) .devices-minimal-table .dev-group-cell,
  body:not(.sidebar-collapsed) .devices-minimal-table th[data-col="group"] { display: none; }
}
@media (min-width: 721px) and (max-width: 960px) {
  body:not(.sidebar-collapsed) .devices-minimal-table .dev-ip-cell,
  body:not(.sidebar-collapsed) .devices-minimal-table th[data-col="ip"] { display: none; }
}
@media (min-width: 721px) and (max-width: 840px) {
  body:not(.sidebar-collapsed) .devices-minimal-table .dev-os-cell,
  body:not(.sidebar-collapsed) .devices-minimal-table th[data-col="os"] { display: none; }
}
/* Backstop: the Status cell holds a fixed-size pill, never arbitrary
   text, so it should never render an ellipsis. Even if a column is
   squeezed, let the pill keep its size rather than collapse to "…". */
.devices-minimal-table .dev-status-cell { overflow: visible; }

/* v3.3.x: "Did you know?" tip card on the About page. */
.dyk-card { display: flex; flex-direction: column; align-items: flex-start; gap: 10px; max-width: 600px; }
.dyk-header { display: flex; align-items: center; gap: 8px; color: var(--accent-ink); font-weight: 600; font-size: 13px; }
.dyk-header svg { width: 16px; height: 16px; }
.dyk-text { margin: 0; color: var(--text); font-size: 13px; line-height: 1.5; min-height: 2.6em; }

/* v2.2.6: body scroll lock while a modal is open. Without this the
 * page behind the modal scrolls on touch, which on mobile reads as
 * two windows moving independently — part of the reported bug. */
body.modal-open {
  overflow: hidden;
  /* iOS Safari also needs position fixing to truly stop rubber-band
   * scroll, but that loses scroll position; overflow:hidden is the
   * pragmatic 90% fix without the scroll-jump side effect. */
  touch-action: none;
}
/* The modal itself must still scroll internally. */
body.modal-open .modal-overlay.active { touch-action: auto; }
body.modal-open .modal { touch-action: auto; }

/* Mobile: make modals true full-bleed sheets so a shorter modal can't
 * leave an older modal peeking out behind it. Fully opaque background. */
@media (max-width: 720px) {
  .modal-overlay {
    padding: 0 !important;
    align-items: stretch;
  }
  .modal {
    width: 100vw !important;
    max-width: 100vw !important;
    min-height: 100vh;
    max-height: 100vh;
    border-radius: 0;
    margin: 0;
    /* Opaque — no bleed-through from whatever is stacked below. */
    background: var(--surface);
  }
  /* Nested modal (drift diff) sits above the base modal. */
  #drift-diff-modal { z-index: 1100; }
}

/* ── v2.6.0: Host Config modal tabs ─────────────────────────────────────── */
.hc-tab {
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--muted);
  cursor: pointer;
  font-family: var(--font);
  font-size: 12px;
  padding: 5px 12px;
  transition: background .12s, color .12s, border-color .12s;
}
.hc-tab:hover  { background: var(--surface2); color: var(--text); }
.hc-tab.active { background: var(--accent); border-color: var(--accent); color: var(--accent-contrast, #fff); }

/* ── v2.6.0: Mobile performance & UX optimisations ─────────────────────────
 *
 * 1. Prevent double-tap zoom on interactive elements — removes the 300ms
 *    tap delay on touch devices without disabling pinch-zoom on content.
 * 2. Momentum scrolling on overflowing containers.
 * 3. Reduce paint complexity by promoting scroll containers to their own
 *    compositor layer.
 * 4. Respect the OS-level "reduce motion" preference.
 * 5. Ensure all tap targets meet the 44×44 px minimum (WCAG 2.5.5).
 */

/* 1. Kill the 300ms tap delay */
button, a, [role="button"],
.nav-btn, .btn-icon, .btn-primary, .btn-secondary,
.hc-tab, .cs-device-cb + label {
  touch-action: manipulation;
}

/* 2+3. Smooth momentum scroll on overflow containers */
.table-card,
.modal,
.sidebar,
.cmd-output,
.log-output,
.hc-panel textarea {
  -webkit-overflow-scrolling: touch;
  scroll-behavior: smooth;
}

/* 4. Honour "prefers-reduced-motion" — turn off decorative animations */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  .ai-btn.available::after { animation: none; }
  .refresh-bar-fill        { transition: none; }
}

/* 5. Minimum touch target size — apply only on touch screens */
@media (hover: none) and (pointer: coarse) {
  .btn-icon, .btn-secondary, .nav-btn, .hc-tab {
    min-height: 44px;
  }
  /* Batch-bar buttons get extra breathing room */
  .batch-bar button {
    padding: 10px 14px;
  }
  /* Table action buttons: expand hit area without changing layout */
  td button.btn-icon {
    padding: 10px 12px;
  }
  /* Checkboxes: larger hit area */
  input[type="checkbox"] {
    width: 20px;
    height: 20px;
  }
}


/* v2.8.1: utility display values for elements that need a non-default
   display. Used to be paired with an anti-FOUC inline <style> in <head>
   that hid them initially — that <style> was removed during the CSP L1
   migration (v3.0.4), but these rules still serve as the canonical
   "this is how the element should render" declarations.
   #pwa-install-btn removed from this list: it MUST start hidden
   (.isl-6 carries display:none) and only be revealed by JS when
   beforeinstallprompt fires. An ID rule with display:flex here
   would override the class and the button would always show. */
.refresh-bar       { display: block; }
.toast-container   { display: flex; z-index: 9999; } /* must top all modals + drawer */


/* ══ v2.9.0: Device Drawer ═══════════════════════════════════════════════════
 * Full-screen overlay drawer — replaces detail-modal + ⋮ dropdown.
 * Two tabs: Actions/Settings and Audit.
 ═══════════════════════════════════════════════════════════════════════════════ */

.device-drawer {
  position: fixed; inset: 0; z-index: 500;
  display: none;
}
.device-drawer.open { display: flex; }

/* Backdrop */
.device-drawer .drawer-backdrop {
  position: absolute; inset: 0;
  background: rgba(0,0,0,0.55);
  /* No backdrop-filter — it creates a stacking context that can intercept
     clicks on the panel in Safari/WebKit mobile, making buttons unreachable */
  z-index: 1;
}

/* Panel — slides in from right, explicitly above backdrop */
.device-drawer .drawer-panel {
  position: absolute; top: 0; right: 0; bottom: 0;
  width: min(820px, 100vw);
  background: var(--surface);
  border-left: 1px solid var(--hair);
  display: flex; flex-direction: column;
  transform: translateX(100%);
  transition: transform 0.22s cubic-bezier(0.4,0,0.2,1);
  overflow: hidden;
  z-index: 2;
}
.device-drawer.open .drawer-panel { transform: translateX(0); }

/* Header */
.drawer-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: 14px 16px 12px;
  border-bottom: 1px solid var(--border);
  background: var(--surface);
  flex-shrink: 0;
}
.drawer-device-name { font-weight: 700; font-size: 16px; }
.drawer-device-sub  { font-size: 13px; color: var(--muted); margin-top: 2px; }

/* Tabs */
.drawer-tabs {
  display: flex; gap: 0;
  border-bottom: 1px solid var(--border);
  background: var(--surface);
  flex-shrink: 0;
}
.drawer-tab-btn {
  flex: 1; padding: 10px 12px;
  font-size: 13px; font-weight: 500;
  color: var(--muted); background: none;
  border: none; border-bottom: 2px solid transparent;
  cursor: pointer; transition: color .15s, border-color .15s;
  font-family: var(--font);
}
.drawer-tab-btn.active {
  color: var(--accent2-ink); border-bottom-color: var(--accent);
}

/* Content area */
.drawer-content {
  flex: 1; overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}
.drawer-tab-panel { padding: 0 0 24px; }

/* Section */
.drawer-section {
  padding: 14px 16px 0;
}
/* v6.4.1: .drawer-section-title (11px uppercase eyebrow) is GONE — it was the
   last live survivor of the retired industrial skin, on the device drawer of
   all places. Its two uses are now plain .section-title, the page rule. Do not
   reintroduce uppercase eyebrow headers. */

/* Actions grid */
.drawer-actions-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
  gap: 6px;
  margin-bottom: 4px;
}
.drawer-action-btn {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 10px;
  background: var(--surface2); border: 1px solid var(--border);
  border-radius: 8px; cursor: pointer; font-size: 12px;
  color: var(--text); font-family: var(--font);
  transition: background .12s, border-color .12s;
  text-align: left; white-space: nowrap;
}
.drawer-action-btn:hover { background: var(--surface2); border-color: var(--border); }
.drawer-action-btn.danger { color: var(--red); }
.drawer-action-btn.danger:hover { background: rgba(239,68,68,0.08); border-color: rgba(239,68,68,0.3); }
.drawer-action-btn svg { flex-shrink: 0; }

/* Settings form */
.drawer-setting-row {
  /* label + control(s) on the first line; the descriptive hint wraps to its
     own full-width line below (see .drawer-setting-row .hint) so a long
     description no longer crams onto the same line as the input. */
  display: flex; align-items: center; flex-wrap: wrap; gap: 8px 10px;
  padding: 11px 0;
  border-bottom: 1px solid var(--border);
}
.drawer-setting-row:last-child { border-bottom: none; }
.drawer-setting-label {
  font-size: 13px; color: var(--text); font-weight: 600; flex-shrink: 0;
  min-width: 132px;
}
/* Push the explanatory hint (and any block of help text) onto its own line,
   spanning the full row width, so it reads as a caption rather than crowding
   the input. */
.drawer-setting-row > .hint {
  flex-basis: 100%; margin: 0; font-size: 12px; line-height: 1.45;
}

/* Audit accordion */
.audit-section {
  border-bottom: 1px solid var(--border);
}
.audit-section summary {
  display: flex; align-items: center; justify-content: space-between;
  padding: 12px 16px;
  cursor: pointer; font-size: 13px; font-weight: 500;
  user-select: none; list-style: none;
}
.audit-section summary::-webkit-details-marker { display: none; }
.audit-section summary::after {
  content: '›'; font-size: 16px; color: var(--muted);
  transition: transform .15s;
}
.audit-section[open] summary::after { transform: rotate(90deg); }
.audit-section-body {
  padding: 0 16px 14px;
  font-size: 13px; line-height: 1.6;
  /* v3.13.0: cap each drawer card to ~15 rows then scroll internally, so a
   * host with many containers/disks/ports doesn't make one card grow
   * unboundedly. Inner tables wrapped in .audit-scroll cap first. */
  max-height: 460px; overflow-y: auto;
}
.audit-section-badge {
  font-size: 10px; color: var(--muted);
  background: var(--surface2); border: 1px solid var(--border);
  padding: 1px 6px; border-radius: 10px; margin-left: 6px;
}
/* v3.4.0: soft-danger small button (LXC delete on the card) */
.btn-danger-soft { color: var(--red); border-color: var(--red); }
.btn-danger-soft:hover { background: rgba(220,53,69,0.12); }

/* v3.4.0: "new" badge on freshly-added nav items (dismissed once visited) */
.nav-new {
  margin-left: 6px; font-size: 10px; text-transform: uppercase; letter-spacing: .04em;
  font-weight: 700; background: rgba(59,126,255,0.15); color: var(--accent-ink);
  border: 1px solid var(--accent); border-radius: 8px; padding: 0 5px; line-height: 14px;
}

/* v3.14.0: sidebar favorites — hover-reveal star + pinned clones under "Main" */
.nav-favorites { display: flex; flex-direction: column; }
.nav-fav-empty { font-size: 11px; color: var(--muted); padding: 2px 12px 6px; line-height: 1.35; }
/* v6.1.1 (#62): the star is a SIBLING of .nav-btn (both wrapped in
   .nav-item), not nested inside it — a role="button" span nested inside a
   real <button> is two independently-focusable/activatable controls, which
   axe-core's nested-interactive rule correctly flags as invalid. .nav-btn
   keeps its own padding/content layout unchanged; .nav-item just replaces
   the row .nav-btn used to lay out alone. */
.nav-item { display: flex; align-items: center; }
.nav-item .nav-btn { width: auto; flex: 1 1 auto; min-width: 0; }
/* A module- or role-gated entry hides its button, not the wrapper, and the wrapper
   still holds the favorites star: a hidden entry left an empty row in the menu. */
.nav-item:has(> .nav-btn.d-none) { display: none; }
.nav-star {
  flex-shrink: 0; display: inline-flex; align-items: center;
  justify-content: center; width: 22px; height: 22px; border-radius: 6px;
  margin-right: 3px;
  color: var(--muted); opacity: 0; cursor: pointer;
  transition: opacity .12s, color .12s, background .12s;
}
.nav-star svg { width: 14px; height: 14px; fill: none; }
.nav-item:hover .nav-star,
.nav-item:focus-within .nav-star { opacity: .85; }
.nav-star:hover { opacity: 1; color: var(--text); background: var(--surface2); }
.nav-star:focus-visible { opacity: 1; outline: 2px solid var(--accent); outline-offset: 1px; }
.nav-star.faved { opacity: 1; color: #f5c518; }
.nav-star.faved svg { fill: #f5c518; }
body.sidebar-collapsed .nav-star,
body.sidebar-collapsed .nav-fav-empty { display: none; }

/* v3.14.0: saved & shareable views dropdown (Devices toolbar) */
.views-menu { position: relative; display: inline-block; }
.views-dropdown {
  position: absolute; right: 0; top: calc(100% + 6px); min-width: 200px; z-index: 1000;
  background: var(--card, #1e1e24); border: 1px solid rgba(127,127,127,0.25);
  border-radius: 8px; box-shadow: var(--shadow-2); padding: 4px;
}
.views-row { display: flex; align-items: center; }
.views-item {
  display: flex; align-items: center; gap: 8px; flex: 1; width: 100%; text-align: left;
  background: none; border: 0; color: inherit; padding: 8px 10px; border-radius: 6px;
  cursor: pointer; font-size: 13px;
}
.views-item:hover { background: rgba(127,127,127,0.14); }
.views-del {
  background: none; border: 0; color: var(--muted); cursor: pointer; font-size: 16px;
  line-height: 1; padding: 4px 8px; border-radius: 6px;
}
.views-del:hover { color: var(--red); background: rgba(127,127,127,0.14); }
.views-empty { padding: 8px 10px; }
.views-save { border-top: 1px solid rgba(127,127,127,0.18); margin-top: 4px; color: var(--accent-ink); }

/* v3.14.0: power page cost/kWh input */
.input-cost-kwh { width: 90px; }

/* v3.14.0: custom report builder */
.rdef-sections { display: flex; flex-wrap: wrap; gap: 6px 16px; }
.rdef-sec { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; }
.report-builder { border: 1px solid var(--border); border-radius: 8px; padding: 12px; margin-top: 8px; }
.rdef-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 6px 0; border-bottom: 1px solid var(--border); }
.rdef-row:last-child { border-bottom: 0; }
.rdef-row-name { font-size: 13px; }

/* v3.14.0: on-demand container logs viewer */
.log-pre {
  max-height: 60vh; overflow: auto; background: var(--surface2);
  border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px;
  font-family: var(--font-mono, ui-monospace, monospace); font-size: 12px;
  line-height: 1.45; white-space: pre-wrap; word-break: break-word; margin: 0;
}

/* v3.14.0: CVE prioritization — KEV (known-exploited) badge + EPSS chip */
.kev-badge {
  display: inline-block; margin-left: 6px; font-size: 10px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .04em; vertical-align: middle;
  color: #fff; background: var(--red); border-radius: 8px; padding: 0 6px; line-height: 15px;
}
td .kev-badge { margin-left: 0; }
.epss-chip {
  display: inline-block; margin-left: 6px; font-size: 10px; font-weight: 600;
  vertical-align: middle; color: var(--muted); border: 1px solid var(--border);
  border-radius: 8px; padding: 0 5px; line-height: 14px;
}

/* v3.14.0: per-container "update available" badge (device-drawer Containers) */
.upd-badge {
  display: inline-block; margin-left: 6px; font-size: 10px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .04em; vertical-align: middle;
  color: var(--amber, #d98a00); background: rgba(217,138,0,0.14);
  border: 1px solid var(--amber, #d98a00); border-radius: 8px;
  padding: 0 5px; line-height: 14px;
}

/* v3.4.0: audit-section group labels (drawer) */
.audit-group-label {
  font-size: 10px; text-transform: uppercase; letter-spacing: .06em;
  /* v6.4.3: `opacity: .8` removed. --muted alone is 4.98:1; multiplied by .8 it
     renders #6a7580 = 3.66:1, under the 4.5:1 AA floor — axe measured exactly
     that. It is the FOURTH stray opacity on a --muted label, and the seeded
     a11y walk's own comment says this class is "ZERO, not a baseline… they are
     all gone, so there is nothing to grandfather". This one survived because it
     lives in the device drawer, which no axe walk had ever opened. */
  font-weight: 700; color: var(--muted);
  margin: 14px 0 4px; padding-left: 2px;
}
.audit-group-label:first-child { margin-top: 2px; }

/* v3.4.0: Health & Hardware drawer section */
.hw-block { margin-bottom: 16px; }
.hw-block:last-child { margin-bottom: 0; }
/* v6: was an uppercase eyebrow — now renders as the canonical .section-title
   (13px/620 ink, sentence-case), keeping its 6px header→content gap. */
.hw-h {
  font-size: 13px; font-weight: 620; color: var(--text); margin-bottom: 6px;
}
.hw-changes { margin: 0; padding-left: 18px; max-height: 200px; overflow-y: auto; }
.hw-changes li { margin-bottom: 2px; }
.hw-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.diag-pending { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--muted); animation: diagPulse 1.4s ease-in-out infinite; }
.diag-done { display: flex; align-items: center; gap: 6px; font-size: 12px; }
@keyframes diagPulse { 0%,100% { opacity: .55; } 50% { opacity: 1; } }

/* v3.4.0: AI tools on the AI page (anomaly scan + cron builder) */
.ai-tool-card { margin-bottom: 14px; }
.ai-tool-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.ai-tool-body { margin-top: 10px; }
.ai-cron-row { display: flex; gap: 8px; }
.ai-cron-row .form-input { flex: 1; }
.anomaly-row { display: flex; gap: 10px; align-items: flex-start; padding: 8px 0; border-top: 1px solid var(--border); }
.anomaly-row:first-child { border-top: none; }
.anomaly-sev { text-transform: uppercase; font-size: 10px; font-weight: 700; letter-spacing: .04em; min-width: 56px; padding-top: 2px; }
.anomaly-dev { font-weight: 500; }
.anomaly-main { flex: 1; min-width: 0; }
.anomaly-act { flex-shrink: 0; }
.anomaly-link { color: var(--accent-ink); cursor: pointer; text-decoration: none; }
.anomaly-link:hover { text-decoration: underline; }
.compliance-fix { color: var(--accent-ink); cursor: pointer; text-decoration: none; white-space: nowrap; margin-left: 6px; }
.compliance-fix:hover { text-decoration: underline; }
.cron-result-expr { display: flex; align-items: center; gap: 10px; font-size: 13px; }
.cron-runs { margin: 4px 0 0; padding-left: 18px; max-height: 132px; overflow-y: auto; }
.cron-runs li { font-size: 12px; color: var(--muted); }
.ai-insight-body { max-height: 60vh; overflow-y: auto; padding: 4px 2px; line-height: 1.55; }

/* v3.4.0: compliance report */
.compliance-summary { font-size: 13px; margin-bottom: 14px; font-weight: 500; }
.compliance-fw-card { margin-bottom: 14px; }
.compliance-fw-head { display: flex; align-items: center; justify-content: space-between; }
.compliance-score { font-size: 28px; font-weight: 700; }

/* v3.4.0: host-config enforce opt-in */
.hc-enforce-row { padding: 10px 12px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface2); }
.hc-enforce-row .hint { margin-top: 6px; }

/* v3.4.0: LXC create wizard form grids */
.hc-grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.hc-grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; }
.lxc-toggles { display: flex; flex-direction: column; gap: 6px; margin-top: 4px; padding: 10px 12px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface2); }
@media (max-width: 560px) { .hc-grid-2, .hc-grid-3 { grid-template-columns: 1fr; } }

/* v3.4.0 Tier-3 mobile pass: wide v3.4.0 tables/modals shouldn't overflow the
   viewport — let them scroll horizontally inside their container instead. */
@media (max-width: 640px) {
  .audit-section-body, .table-card, #compliance-body, #discovery-body { overflow-x: auto; -webkit-overflow-scrolling: touch; }
  .modal.modal-wide { width: 96vw; max-width: 96vw; }
  .compliance-fw-head { flex-wrap: wrap; gap: 4px; }
  .hw-actions { gap: 6px; }
  .ai-cron-row { flex-direction: column; }
}
.audit-filter {
  width: 100%; padding: 6px 10px; margin-bottom: 10px;
  background: var(--surface2); border: 1px solid var(--border);
  border-radius: 6px; font-size: 12px; color: var(--text);
  font-family: var(--font-mono);
}
.audit-filter:focus { outline: none; border-color: var(--accent); }

/* Command history in audit */
.audit-cmd-entry {
  border: 1px solid var(--border); border-radius: 6px;
  margin-bottom: 6px; overflow: hidden;
}
.audit-cmd-summary {
  display: flex; align-items: center; gap: 8px;
  padding: 7px 10px; cursor: pointer;
  background: var(--surface2);
}
.audit-cmd-output {
  padding: 8px 10px; display: none;
  background: var(--surface2); border-top: 1px solid var(--border);
  font-family: var(--font-mono); font-size: 12px;
  max-height: 200px; overflow-y: auto; white-space: pre-wrap;
  word-break: break-all;
}
.audit-cmd-entry.expanded .audit-cmd-output { display: block; }

/* Port table */
.ports-table th, .ports-table td {
  padding: 5px 8px; font-size: 12px;
}

/* Mobile: full screen */
@media (max-width: 720px) {
  .device-drawer .drawer-panel {
    width: 100vw; border-left: none;
  }
  .drawer-actions-grid {
    grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
  }
}


/* ══ v3.0.0: IaC Generator page ═══════════════════════════════════════════════ */

.iac-layout {
  display: grid;
  grid-template-columns: minmax(320px, 380px) 1fr;
  gap: 16px;
  align-items: start;
}
@media (max-width: 900px) {
  .iac-layout { grid-template-columns: 1fr; }
}

.iac-controls {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 16px;
}
.iac-section { margin-bottom: 16px; }
.iac-section:last-of-type { margin-bottom: 12px; }
.iac-label {
  display: block;
  font-size: 12px; font-weight: 600;
  color: var(--muted);
  text-transform: uppercase; letter-spacing: 0.05em;
  margin-bottom: 6px;
}

.iac-cat-list {
  max-height: 380px; overflow-y: auto;
  border: 1px solid var(--border); border-radius: 6px;
  padding: 8px 10px;
  background: var(--surface2);
}
.iac-cat-row {
  display: flex; align-items: center; gap: 8px;
  padding: 4px 0;
  font-size: 13px;
  cursor: pointer;
}
.iac-cat-row input[type="checkbox"] { cursor: pointer; }
.iac-cat-row:hover { color: var(--accent2-ink); }
.iac-cat-row .hint { color: var(--muted); font-size: 12px; margin-left: auto; }

.iac-status {
  padding: 8px 12px; border-radius: 6px;
  font-size: 12px;
  background: var(--surface2); border: 1px solid var(--border);
}
.iac-status.error  { color: var(--red);   border-color: rgba(239,68,68,0.3); }
.iac-status.done   { color: var(--green); border-color: rgba(34,197,94,0.3); }

.iac-output {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  display: flex; flex-direction: column;
  min-height: 480px;
}
.iac-output-header {
  display: flex; justify-content: space-between; align-items: center;
  padding: 10px 14px;
  border-bottom: 1px solid var(--border);
}
.iac-code {
  flex: 1;
  font-family: var(--font-mono);
  font-size: 12px; line-height: 1.55;
  padding: 14px 16px;
  margin: 0;
  overflow-x: auto;
  max-height: 55vh; overflow-y: auto;   /* v5.5.0: cap generated IaC; was unbounded */
  white-space: pre;
  color: var(--text);
  background: var(--surface2);
  border-bottom-left-radius: 10px;
  border-bottom-right-radius: 10px;
  max-height: 70vh;
}


/* v3.0.0: IaC tabs and conversation view */
.iac-tabs {
  display: flex; border-bottom: 1px solid var(--border);
  background: var(--surface);
  border-top-left-radius: 10px; border-top-right-radius: 10px;
}
.iac-tab {
  flex: 0 0 auto; padding: 10px 16px;
  font-size: 13px; font-weight: 500;
  color: var(--muted); background: none;
  border: none; border-bottom: 2px solid transparent;
  cursor: pointer; transition: color .15s, border-color .15s;
  font-family: var(--font);
}
.iac-tab.active {
  color: var(--accent2-ink); border-bottom-color: var(--accent);
}
.iac-convo {
  flex: 1; overflow-y: auto;
  padding: 14px 16px;
  font-size: 12px;
  background: var(--surface2);
  border-bottom-left-radius: 10px; border-bottom-right-radius: 10px;
  max-height: 70vh;
}
.iac-convo-block {
  margin-bottom: 14px;
  border: 1px solid var(--border); border-radius: 6px;
  overflow: hidden;
}
.iac-convo-role {
  padding: 6px 12px; font-size: 11px; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.08em;
  background: var(--surface2); color: var(--muted);
  border-bottom: 1px solid var(--border);
}
.iac-convo-role.system    { color: var(--accent2-ink); }
.iac-convo-role.user      { color: var(--green); }
.iac-convo-role.assistant { color: var(--amber); }
.iac-convo-content {
  padding: 10px 12px;
  font-family: var(--font-mono); font-size: 12px;
  white-space: pre-wrap; word-break: break-word;
  max-height: 400px; overflow-y: auto;
  color: var(--text);
}


/* v3.0.1: collapsible desktop sidebar */
/* v7.0.0: the floor here is 721px, matching the width at which the Collapse
   button becomes visible (it is hidden at ≤720px, where the sidebar is an
   overlay drawer). It used to be 769px — left behind when v2.2.7 moved the
   mobile drawer breakpoint 768→720 — so between 721 and 768 in a browser the
   button rendered and did nothing at all: no rail, no content margin, no
   label hiding. A copy of this block scoped to installed-PWA mode patched the
   gap there and nowhere else; that copy is gone now, because one rule at the
   right breakpoint is what stops the two drifting apart again. */
@media (min-width: 721px) {
  body.sidebar-collapsed .sidebar {
    width: 56px;
    padding-left: 6px;
    padding-right: 6px;
  }
  body.sidebar-collapsed .app-content { margin-left: 56px; }
  body.sidebar-collapsed header { margin-left: 56px; }
  body.sidebar-collapsed .sidebar-label,
  body.sidebar-collapsed .nav-btn span:not(.nav-icon),
  body.sidebar-collapsed .sidebar-group-toggle span:not(.nav-icon),
  body.sidebar-collapsed .sidebar-group-items,
  body.sidebar-collapsed .brand .logo-text,
  body.sidebar-collapsed .brand-v,
  body.sidebar-collapsed .acct-name {
    display: none;
  }
  body.sidebar-collapsed .sidebar .brand,
  body.sidebar-collapsed .side-foot .acct-btn { justify-content: center; }
  body.sidebar-collapsed .side-foot .acct-btn > svg { display: none; }
  body.sidebar-collapsed .nav-btn,
  body.sidebar-collapsed .sidebar-group-toggle {
    justify-content: center;
    padding-left: 6px;
    padding-right: 6px;
  }
  body.sidebar-collapsed .sidebar-collapse-btn-text { display: none; }
}
/* While the sidebar collapse/expand is committing, kill every transition so
   content cards don't animate their width and reflow text ("floating/blinking"
   bug). Added by toggleSidebarCollapse for ~2 frames, then removed. */
body.no-anim,
body.no-anim * { transition: none !important; }

.sidebar-collapse-btn {
  width: 100%;
  padding: 6px 10px;
  background: transparent;
  border: 1px solid var(--hair);
  border-radius: 6px;
  color: var(--muted);
  cursor: pointer;
  font-size: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  margin-bottom: 8px;
  transition: background .15s, color .15s;
}
.sidebar-collapse-btn:hover { background: var(--hair-2); color: var(--text); }
#sidebar-collapse-icon { display: inline-flex; }
#sidebar-collapse-icon svg { transition: transform .18s ease; }
body.sidebar-collapsed #sidebar-collapse-icon svg { transform: rotate(180deg); }

/* ═══ v6.0.0 — AUTO-HIDE CHROME (default ON; My Account → Navigation) ════════
   Sidebar: the 56px rail expands to full width on hover as an OVERLAY
   (.sidebar is fixed, so content never shifts). While alerts are open the rail
   is pinned open so nothing needing attention is concealed.
   In auto-hide mode the manual Collapse button is redundant → hidden.

   v7.0.0: the pin keys off `body.sidebar-alert-pinned`, which app.js sets only
   when auto-hide is ON and the operator has not opted out. It used to key off
   `.has-active-alert`, which is set whenever alerts are open — so a MANUALLY
   collapsed sidebar (auto-hide off) was forced back to 248px while the content
   kept the 56px rail margin, and the two overlapped. */
body.sidebar-collapsed .sidebar { transition: width 0.16s ease; }
body.autohide-sidebar .sidebar-collapse-btn { display: none !important; }
body.sidebar-collapsed .sidebar:hover,
body.sidebar-collapsed.sidebar-alert-pinned .sidebar {
  width: 248px; padding-left: 8px; padding-right: 8px;
  /* v6.4.2: the flyout sidebar lifts from a vertical EDGE, so it cannot reuse
     --shadow-3's downward offset — but it takes the same per-theme alpha via
     --shadow-edge, which body.light lowers with the rest. */
  box-shadow: var(--shadow-edge); z-index: 900;
}
body.sidebar-collapsed .sidebar:hover .sidebar-group-items,
body.sidebar-collapsed.sidebar-alert-pinned .sidebar .sidebar-group-items { display: flex; }
body.sidebar-collapsed .sidebar:hover .sidebar-label,
body.sidebar-collapsed.sidebar-alert-pinned .sidebar .sidebar-label { display: block; }
body.sidebar-collapsed .sidebar:hover .sidebar-search-wrap,
body.sidebar-collapsed.sidebar-alert-pinned .sidebar .sidebar-search-wrap { display: block; }
body.sidebar-collapsed .sidebar:hover .nav-btn span:not(.nav-icon),
body.sidebar-collapsed .sidebar:hover .sidebar-group-toggle span:not(.nav-icon),
body.sidebar-collapsed .sidebar:hover .brand .logo-text,
body.sidebar-collapsed .sidebar:hover .brand-v,
body.sidebar-collapsed .sidebar:hover .acct-name,
body.sidebar-collapsed.sidebar-alert-pinned .sidebar .nav-btn span:not(.nav-icon),
body.sidebar-collapsed.sidebar-alert-pinned .sidebar .sidebar-group-toggle span:not(.nav-icon),
body.sidebar-collapsed.sidebar-alert-pinned .sidebar .brand .logo-text,
body.sidebar-collapsed.sidebar-alert-pinned .sidebar .brand-v,
body.sidebar-collapsed.sidebar-alert-pinned .sidebar .acct-name { display: inline; }
body.sidebar-collapsed .sidebar:hover .nav-btn,
body.sidebar-collapsed .sidebar:hover .sidebar-group-toggle,
body.sidebar-collapsed .sidebar:hover .sidebar .brand,
body.sidebar-collapsed .sidebar:hover .side-foot .acct-btn,
body.sidebar-collapsed.sidebar-alert-pinned .sidebar .nav-btn,
body.sidebar-collapsed.sidebar-alert-pinned .sidebar .sidebar-group-toggle,
body.sidebar-collapsed.sidebar-alert-pinned .sidebar .side-foot .acct-btn {
  justify-content: flex-start; padding-left: 9px; padding-right: 9px;
}
body.sidebar-collapsed .sidebar:hover .side-foot .acct-btn > svg,
body.sidebar-collapsed.sidebar-alert-pinned .sidebar .side-foot .acct-btn > svg { display: inline; }
/* v6.1.1: the favorite star (+ its "star a page" empty hint) must reveal
   the same way every other row control does here -- the base
   `body.sidebar-collapsed .nav-star { display: none }` rule (above, for the
   narrow icon-only rail) otherwise stays in effect even while hover/alert
   has expanded the rail to full width, permanently hiding the star with no
   way to reach it in auto-hide sidebar mode. */
body.sidebar-collapsed .sidebar:hover .nav-star,
body.sidebar-collapsed.sidebar-alert-pinned .sidebar .nav-star { display: inline-flex; }
body.sidebar-collapsed .sidebar:hover .nav-fav-empty,
body.sidebar-collapsed.sidebar-alert-pinned .sidebar .nav-fav-empty { display: block; }

/* (v6: top-bar auto-hide was removed by request — only the sidebar auto-hides.) */


/* ── v3.0.1: ACME UI ──────────────────────────────────────────────────── */
.acme-step-pill {
  padding: 4px 10px;
  border: 1px solid var(--border);
  border-radius: 999px;
  font-weight: 500;
  color: var(--muted);
  background: transparent;
  letter-spacing: .5px;
}
.acme-step-pill.active {
  background: var(--accent);
  color: var(--accent-contrast, #fff);
  border-color: var(--accent);
}
.acme-step-pill.done {
  border-color: var(--green);
  color: var(--green);
}
.acme-pill {
  display: inline-block;
  padding: 2px 8px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 500;
  white-space: nowrap;
}
.acme-pill-ok   { background: rgba(0,200,120,0.12); color: var(--green); border: 1px solid rgba(0,200,120,0.30); }
.acme-pill-warn { background: rgba(255,170,40,0.12); color: var(--amber); border: 1px solid rgba(255,170,40,0.30); }
.acme-pill-crit { background: rgba(220,60,60,0.12);  color: var(--red);   border: 1px solid rgba(220,60,60,0.30); }
.acme-pill-info { background: var(--surface2); color: var(--muted); border: 1px solid var(--border); }
.acme-wildcard-glyph {
  display: inline-block;
  width: 18px; height: 18px;
  text-align: center;
  line-height: 18px;
  border-radius: 4px;
  background: rgba(120, 100, 220, 0.16);
  color: #9a8dff;
  font-size: 11px;
  font-weight: 600;
  margin-left: 6px;
  vertical-align: middle;
}
.acme-timeline-item {
  display: flex;
  gap: 12px;
  padding: 10px 0;
  border-bottom: 1px solid var(--border);
}
.acme-timeline-item:last-child { border-bottom: none; }
.acme-timeline-dot {
  flex: 0 0 10px;
  width: 10px; height: 10px;
  border-radius: 50%;
  margin-top: 6px;
}
.acme-timeline-dot.ok    { background: var(--green); }
.acme-timeline-dot.fail  { background: var(--red); }
.acme-timeline-dot.pending { background: var(--amber); }
.acme-row { transition: background .12s; }
.acme-row:hover { background: var(--surface2); cursor: pointer; }

/* v3.0.1: Mitigation runner modal */
.mitigate-pane { font-size: 13px; }
.mitigate-pane pre { font-family: var(--font-mono); }

/* CSP migration (L1): rules moved from inline <style> in index.html.
 *
 * The <style> this came from lived in <head> BEFORE the stylesheet <link>, so
 * it was an anti-FOUC hide the stylesheet then overrode on load. Moved into
 * this file it landed 700 lines AFTER the canonical declarations at the top of
 * this section, same specificity — so the hide won permanently and never
 * lifted.
 *
 * `.toast-container { display: none }` therefore made every toast in the
 * product invisible: they were built, appended and timed out inside a
 * zero-height container. `.refresh-bar { display: none }` did the same to the
 * auto-refresh progress strip, which app.js still drives with a scaleX
 * transform once a second.
 *
 * Both are gone; `.toast-container { display: flex }` and
 * `.refresh-bar { display: block }` above are the declarations that were always
 * meant to apply. The sidebar rule below has no counterpart and is kept. */
@media (max-width: 720px) {
  .sidebar { transform: translateX(-100%); }
}

/* CSP L1 fix: utility classes replacing inline style= attributes */
/* Plain (NOT !important) on purpose: #login-page and #app carry `d-none` as a
 * pre-JS hide and are revealed at boot via inline `style.display = 'flex'`.
 * An `!important` here would beat that inline style and leave the whole app
 * blank — so the global utility must stay overridable by inline styles. */
.d-none { display: none; }
/* The custom-script device-assignment rows (.cs-device-row / .isl-605) set
 * `display:flex` in a rule defined LATER in this sheet, which beat the plain
 * `.d-none` and left filtered rows visible (the count + Select-all disagreed
 * with the screen). Scope the !important to those rows only — never globally. */
.cs-device-row.d-none { display: none !important; }
.grid-2col { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.w-full { width: 100%; }
.flex-1 { flex: 1; }
.c-accent { color: var(--accent-ink); }
.c-muted { color: var(--muted); }
.c-red { color: var(--red); }
.c-green { color: var(--green); }
.c-amber { color: var(--amber); }
.c-danger-outline { color: var(--red); border-color: rgba(239,68,68,0.3); }
.c-warn-outline { color: var(--amber); border-color: rgba(245,158,11,0.3); }
.fs-11 { font-size: 11px; }
.fs-12 { font-size: 12px; }
.fs-13 { font-size: 13px; }
.bh-day { min-width: 96px; }
.fs-body { font-size: 13px; }  /* THE body anchor — reach for this one */
.fs-14 { font-size: 13px; }    /* deprecated alias of .fs-body (the 14px and 13.5px anchors were retired) */
.meta-sm { font-size: 11px; color: var(--muted); margin-top: 4px; }
.hint-mb { font-size: 12px; color: var(--muted); margin-bottom: 8px; }
.meta-sm-nm { font-size: 11px; color: var(--muted); }
.meta-amber { font-size: 11px; color: var(--amber); }
.hint { font-size: 12px; color: var(--muted); }
.decl-detail { font-size: 12px; color: var(--muted); margin-left: 16px; }
.decl-detail .decl-add { color: var(--ok, #2ecc71); font-weight: 600; }
.decl-detail .decl-rm { color: var(--danger, #e74c3c); font-weight: 600; }
.decl-detail .decl-chg { color: var(--warn, #e0a800); font-weight: 600; }
/* v6: was an uppercase eyebrow — now the canonical .section-title look. */
/* v6.4.1: .section-label is retired — it duplicated .section-title's
   typography exactly while matching no header-band selector, so every
   card using it silently lost the band. Its card headers are now
   .section-title; its four field labels are .stat-label. */
.empty-state-sm { color: var(--muted); text-align: center; padding: 24px; }
.ta-center { text-align: center; }
.ta-right { text-align: right; }
.ff-mono { font-family: var(--font-mono); }
/* v3.14.0 (#22): customizable dashboard */
.dash-off { display: none !important; }
.dash-toolbar { display: flex; justify-content: flex-end; margin-bottom: 8px; }
.dash-edit-panel { background: var(--surface2); border: 1px solid var(--border); border-radius: 8px; padding: 10px 14px; margin-bottom: 16px; }
.dash-edit-hint { margin-bottom: 8px; }
.dash-edit-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 5px 0; }
.dash-edit-row + .dash-edit-row { border-top: 1px solid var(--border); }
.dash-edit-name { display: flex; align-items: center; gap: 8px; margin: 0; cursor: pointer; }
.dash-edit-moves { display: flex; gap: 4px; flex-shrink: 0; }
.row-8 { display: flex; gap: 8px; }
.row-8-mb8 { display: flex; gap: 8px; margin-bottom: 8px; }
/* v6.4.1: no flex-wrap meant a variable-length chip row (Timeline kind
   filters, event-kind pickers) ran off the viewport with no way to reach
   the overflow — its sibling .toolbar has always wrapped. */
.row-8-center { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.flex-between { justify-content: space-between; }
.toolbar { display: flex; gap: 10px; margin-bottom: 16px; flex-wrap: wrap; align-items: center; }
.toolbar-mb24 { display: flex; gap: 10px; margin-bottom: 24px; flex-wrap: wrap; align-items: center; }
.click-row { display: flex; align-items: center; gap: 8px; cursor: pointer; }
.click-row-mb6 { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; cursor: pointer; }
/* Baseline-check catalog rows. A two-column grid (checkbox | body) so the name,
   the type badge and the description all align on the same left edge instead of
   becoming flex siblings on one wrapping line (which .click-row-mb6 would do). */
.bc-row { display: grid; grid-template-columns: auto 1fr; gap: 10px; align-items: start;
          padding: 9px 4px; border-top: 1px solid var(--hair); cursor: pointer; }
.bc-row:first-of-type { border-top: none; }
.bc-row:hover { background: var(--surface2); }
.bc-row > input { margin: 3px 0 0; }
.bc-row-body { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.bc-row-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.bc-row-name { font-size: 13px; font-weight: 620; }
.bc-row-scope { font-family: var(--font-mono); font-size: 11px; color: var(--muted); }
.bc-row-desc { font-size: 12px; color: var(--muted); line-height: 1.45; }
.bc-cat { margin-top: 14px; }
/* Unified with .click-row / .click-row-mb6 so every checkbox→label gap is 8px
   (checkmarks line up consistently across Settings). */
.click-row-6 { display: flex; align-items: center; gap: 8px; cursor: pointer; }
/* A hint/sub-text that is a SIBLING div below a .click-row checkbox (not a child
   of the label) starts at the container edge, ~24px left of the checkbox's label
   text (16px box + 8px gap) — so it reads misaligned. Hang it under the text. */
.click-row-hint { padding-left: 24px; }
/* Consistency sweep: a checkbox/radio in a BARE <label> (no click-row/form-label
   class — e.g. day-of-week toggles, "All day", "Show Homelab…") only had the ~4px
   HTML text-space between the box and its text, so it read tighter than every
   .click-row (8px). Give the box the same 8px trailing gap so EVERY checkbox→label
   spacing matches. Classed/flex-gap labels are unaffected (they own their gap). */
label:not([class]) > input[type="checkbox"],
label:not([class]) > input[type="radio"] { margin-right: 8px; }
/* `label.form-row` (3 DMARC IMAP toggles) had no rule at all → the checkbox and
   its text collapsed to the ~4px text-space. Make it the same 8px flex row as
   .click-row. Only labels — the `div.form-row` uses are untouched. */
label.form-row { display: flex; align-items: center; gap: 8px; cursor: pointer; }
/* A `.form-label` is a 12px uppercase eyebrow caption meant to sit ABOVE an
   input. Several Settings toggles reused it as the label NEXT TO a checkbox,
   which rendered them as tiny uppercase block text (and broke the inline
   checkbox alignment). When a .form-label directly wraps a checkbox/radio,
   render it as normal 14px sentence-case body text, inline with the box. */
label.form-label:has(> input[type="checkbox"]),
label.form-label:has(> input[type="radio"]) {
  /* A block with the checkbox absolutely positioned in the left gutter and ALL
     text (incl. inline <strong>/<code>) flowing with a hanging indent. The old
     grid (auto 1fr) treated each inline element + text node as a SEPARATE grid
     item, so a <strong>/<code> mid-label got dropped into column 1 under the box
     — the "weird indentation". Block + padding-left keeps it one flowing run, and
     any .hint stacks underneath at the same indent. */
  display: block; position: relative; padding-left: 26px;
  font-size: 13px; text-transform: none; letter-spacing: normal; line-height: 1.5;
  color: var(--text); margin-bottom: 14px; cursor: pointer;
}
label.form-label:has(> input[type="checkbox"]) > input[type="checkbox"],
label.form-label:has(> input[type="radio"]) > input[type="radio"] {
  position: absolute; left: 0; top: 3px; margin: 0;
}
label.form-label:has(> input[type="checkbox"]) > .hint,
label.form-label:has(> input[type="radio"]) > .hint { display: block; margin: 3px 0 0; }
.row-12-center { display: flex; align-items: center; gap: 12px; }
.row-6-wrap { display: flex; gap: 6px; flex-wrap: wrap; }
/* v7.0.0: the Alerts row carried up to eleven actions in a `nowrap` cell,
   so the whole table scrolled sideways to fit one long line. v7.1.0 keeps
   four on the row (Triage, Fix, Mute, Resolve) and moves the rest into the
   row's More menu. The cell still wraps and is bounded, so a translated
   label that runs long wraps inside the column instead of widening it. */
.alert-actions { display: flex; flex-wrap: wrap; gap: 6px; align-items: center;
                 min-width: 352px; max-width: 368px; }
/* v7.1.0: a row's More menu (rowMoreMenu in app.js). Body-level and fixed,
   so a scrolling table cannot clip it. */
#row-more-pop {
  position: fixed; min-width: 200px; max-width: 280px; padding: 4px;
  background: var(--surface); color: var(--text);
  border: 1px solid var(--hair); border-radius: 8px; box-shadow: var(--shadow-2);
}
#row-more-pop > button, #row-more-pop > a {
  display: flex; align-items: center; gap: 8px; width: 100%;
  padding: 8px 10px; border: 0; border-radius: 6px;
  background: none; color: inherit; font: inherit; font-size: 13px;
  text-align: left; white-space: nowrap; text-decoration: none; cursor: pointer;
}
#row-more-pop > button:hover, #row-more-pop > button:focus-visible,
#row-more-pop > a:hover, #row-more-pop > a:focus-visible {
  background: var(--surface2); color: var(--accent-ink); outline: none;
}
#row-more-pop svg { flex: none; color: var(--muted); }

.col-4 { display: flex; flex-direction: column; gap: 4px; }
.mb-6 { margin-bottom: 6px; }
.mb-8 { margin-bottom: 8px; }
.mb-12 { margin-bottom: 12px; }
.mb-16 { margin-bottom: 16px; }
.mb-24 { margin-bottom: 24px; }
.mt-2 { margin-top: 2px; }
.mt-4 { margin-top: 4px; }
.mt-8 { margin-top: 8px; }
.mt-16 { margin-top: 16px; }
.mt-32 { margin-top: 32px; }
.mw-160 { max-width: 160px; }
.mw-200 { max-width: 200px; }
.mw-380 { max-width: 380px; }
.input-wide { max-width: 400px; padding: 8px 12px; font-size: 13px; }
.input-wide-sm { max-width: 380px; padding: 8px 12px; font-size: 13px; }
.input-auto { width: auto; min-width: 160px; padding: 8px 12px; font-size: 13px; }
.input-search { padding: 8px 12px 8px 34px; font-size: 13px; }
.select-sm { padding: 8px; background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; color: var(--text); font-size: 13px; min-width: 160px; }
.icon-btn { width: 14px; height: 14px; margin-right: 4px; display: inline-block; vertical-align: -2px; }
/* v6.4.3 (SPACING): `.icon-btn` is set on the <svg> itself and predates flex
   `gap`. Inside a button that now provides its own gap the margin STACKS on
   top of it — measured 9px where every other button in the product renders
   5px, because .btn-icon's svg carries no such class. A sibling cleanup had
   already removed `.btn-primary svg { margin-right: 5px }` for exactly this
   reason and missed this one. Nulled only inside the flex buttons, so the
   inline uses that still rely on the margin are untouched. */
:is(.btn-primary, .btn-secondary, .btn-icon, .enroll-btn, .btn) > .icon-btn { margin-right: 0; }

/* v6.4.3: the same mechanism, found by measuring 422 icon-to-label pairs in a
   real browser rather than by reading. A margin and a flex `gap` do not
   override each other — they ADD — so every rule below rendered at twice its
   intended spacing, and every rule involved is individually correct.

   Scoped nulls, never blanket removals: each of these margins is the ONLY
   spacing its element has in its non-flex uses. `.distro-icon`'s 6px is what
   separates it from the device name in the Devices table (`a.isl-319`, not a
   flex row); deleting the declaration would fix the Home card and break the
   table.

   td > .btn-icon + .btn-icon (styles.css:1396) was added at v6.4.1 for BARE
   icon buttons in a cell, which is right — but `td.exposure-actions` and
   `td.row-6` are themselves gapped flex rows, so it double-spaced 160 rendered
   buttons to 12px against the 6px both of them declare. */
td.exposure-actions > .btn-icon + .btn-icon,
td.row-6 > .btn-icon + .btn-icon { margin-left: 0; }
.isl-558 > .distro-icon { margin-right: 0; }
.board-prob-chip > .sem { margin-right: 0; }
.gpu-trend-row > .sparkline { margin-left: 0; }
.isl-156 > .isl-158 { margin-left: 0; }
.icon-noshrink { width: 13px; height: 13px; flex-shrink: 0; }
.icon-search-abs { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); width: 16px; height: 16px; pointer-events: none; }
.icon-search-abs-sm { position: absolute; left: 8px; top: 50%; transform: translateY(-50%); width: 13px; height: 13px; pointer-events: none; }

/* CSP L1 fix: one-off inline styles (auto-generated) */
.isl-1 { text-align:center; font-size:16px; letter-spacing:6px; font-weight:700 }
.isl-2 { display:flex; align-items:center; gap:8px; cursor:pointer; margin:8px 0 16px; font-size:13px; color:var(--muted) }
.isl-3 { width:16px; height:16px; accent-color:var(--accent) }
.isl-4 { display:none; background:linear-gradient(90deg,#1e3a8a,#7c3aed); color:white; padding:8px 18px; font-size:13px; text-align:center; border-bottom:1px solid rgba(255,255,255,0.1) }
.isl-5 { color:#bfdbfe; margin-left:8px }
.isl-6 { display:none; padding:4px 10px; font-size:12px; gap:5px }
.isl-7 { display:flex; align-items:center; gap:5px; padding:8px 14px }   /* v6.4.3 (SPACING): was 6px; the product's icon-to-label gap is 5px on
      308 buttons. One button a pixel off is the kind of thing you cannot
      name but can see when it sits beside its neighbours. */
.isl-8 { font-size:13px; color:var(--accent-ink); font-weight:600 }
.isl-9 { padding:7px 14px }
.isl-10 { margin-bottom:16px; display:flex; gap:10px; align-items:center; flex-wrap:wrap }
.isl-11 { position:relative; flex:1; max-width:300px }
.isl-12 { margin-left:auto }
.isl-13 { color:var(--muted); font-size:13px; padding:16px 0 }
.isl-14 { position:relative }
.isl-15 { padding:6px 10px 6px 28px; font-size:12px; width:160px }
.isl-16 { width:auto; padding:6px 10px; font-size:12px }
.isl-17 { padding:6px 14px; font-size:12px }
.isl-18 { display:flex; gap:10px; margin-bottom:14px; flex-wrap:wrap }
.isl-19 { margin-bottom:14px }
.isl-20 { max-width:420px; padding:8px 12px; font-size:13px }
.isl-21 { flex:1; max-width:320px }
.isl-22 { font-size:12px; color:var(--muted); align-self:flex-end; padding-bottom:12px }
.isl-23 { max-width:180px }
.isl-24 { max-width:120px }
.isl-25 { display:flex; flex-direction:column; gap:10px }
.isl-26 { margin-top:10px }
.isl-27 { font-size:11px; margin-top:8px }
.isl-28 { flex:0 0 auto; align-self:flex-end }
.isl-29 { padding:9px 12px }
.isl-30 { font-size:12px; color:var(--green); margin-top:4px; display:none }
.isl-31 { font-size:12px; color:var(--muted); margin-top:6px; font-family: var(--font-mono); word-break:break-all; display:none }
.isl-33 { display:flex; align-items:center; gap:8px; margin-bottom:14px; cursor:pointer }
.isl-34 { width:18px; height:18px; accent-color:var(--accent) }
.isl-35 { flex:2 }
.isl-36 { font-size:11px; color:var(--green); display:none }
.isl-37 { font-size:11px; color:var(--green); margin-top:4px; display:none }
.isl-38 { display:flex; gap:8px; align-items:center; margin-top:8px; flex-wrap:wrap }
.isl-39 { flex:1; min-width:240px }
.isl-40 { padding:8px 14px }
.isl-41 { font-size:12px; margin-top:8px; display:none; padding:8px 12px; border-radius:6px }
.isl-42 { margin:0 }
.isl-43 { display:flex; gap:6px }
.isl-44 { padding:4px 8px }
.isl-45 { padding:4px 8px; color:var(--red); border-color:rgba(239,68,68,0.3) }
.isl-46 { max-height:300px; overflow-y:auto }
.isl-47 { display:flex; align-items:flex-start; gap:10px; cursor:pointer; margin-top:14px }
.isl-48 { font-size:13px; font-weight:500 }
.isl-49 { max-width:280px }
.isl-50 { margin-top:6px }
.isl-51 { display:flex; align-items:center; gap:6px; cursor:pointer; padding:9px 4px }
/* LDAP "Verify TLS cert + Timeout" row: top-align and let both fields size to
   content (the generic `.settings-row .form-group{flex:1}` would otherwise stretch
   each to a half-width column and `align-items:flex-end` would drop the shorter one
   to the bottom — the ragged look). */
/* v7.0.2: the autonomy allow-list. 25 machine names, each with pills and a
   sentence of description, rendered as one flat inline column — the name, the
   platforms and the prose all ran together and wrapped differently on every
   row. A three-column grid puts the checkbox, the name+badges and the
   description in their own lanes so the list scans vertically, and the group
   headings give it structure the filter can also fold away. */
.autonomy-act-group {
  font-size: 12px; font-weight: 620; color: var(--muted);
  margin: 12px 0 4px; padding-bottom: 3px; border-bottom: 1px solid var(--hair);
}
.autonomy-act-group:first-child { margin-top: 0; }
.autonomy-act-row {
  display: grid; grid-template-columns: 18px minmax(0, 260px) minmax(0, 1fr);
  align-items: baseline; gap: 8px; padding: 3px 0;
}
.autonomy-act-name { display: flex; align-items: baseline; gap: 5px; flex-wrap: wrap; }
/* v7.0.2: the list must FILL its row.
   #autonomy-actions is a block container wrapped in a .settings-row, which is
   display:flex — so with no flex property it took the default `0 1 auto` and
   sized to its content: 392px inside a 1096px row, about a third of the card.
   The 3-column grid above then had ~110px left for the description, so every
   row wrapped into a paragraph and the list read as a wall of text.
   Same shape as the #cmdb-asset-tickets sliver above: a .scroll-cap in a flex
   parent does not get min-height/min-width protection, because it is a scroll
   container. */
#autonomy-actions { flex: 1 1 100%; min-width: 0; }
@media (max-width: 720px) {
  .autonomy-act-row { grid-template-columns: 18px minmax(0, 1fr); }
  .autonomy-act-row > .hint { grid-column: 2; }
}
.settings-row.ldap-row { align-items: flex-start; }
.settings-row.ldap-row .form-group.isl-28 { flex: 0 0 auto; min-width: 0; }
.settings-row.ldap-row .form-group.ldap-narrow { flex: 0 0 140px; min-width: 0; }
.isl-53 { display:flex; gap:8px; margin-top:10px; flex-wrap:wrap }
.isl-54 { font-size:12px; margin-top:8px; display:none; padding:8px 12px; border-radius:6px; font-family: var(--font-mono) }
.isl-55 { margin-top:8px; font-size:12px }
.isl-56 { display:none; color:var(--green) }
/* v3.12.0: top-align so a field with a hint below (e.g. AI Model) doesn't push
   the sibling selects (Enabled / Provider) upward out of line. */
.isl-57 { flex-wrap:wrap; gap:12px; align-items:flex-start }
.isl-58 { min-width:120px }
.isl-59 { min-width:220px }
.isl-60 { min-width:140px }
.isl-61 { flex-wrap:wrap; gap:12px; align-items:flex-end; margin-top:12px }
.isl-62 { min-width:260px }
.isl-63 { min-width:160px }
.isl-64 { display:flex; gap:10px; margin-top:14px; align-items:center }
.isl-65 { max-width:340px }
.isl-66 { font-family:var(--font-mono); font-size:12px }
.isl-67 { display:flex; align-items:center; gap:10px; cursor:pointer; margin:10px 0 }
.isl-68 { max-width:240px }
.isl-69 { font-size:12px; color:var(--muted); margin-top:4px }
.isl-70 { margin-top:14px }
.isl-71 { flex:1; min-width:280px }
.isl-72 { flex-wrap:wrap; gap:12px }
.isl-73 { flex:1; min-width:260px }
.isl-74 { font-size:11px; color:var(--muted); font-weight:400 }
.isl-75 { display:flex; gap:10px; align-items:center; flex-wrap:wrap }
.isl-76 { font-size:13px; color:var(--muted) }
.isl-77 { display:flex; flex-direction:column; gap:14px; margin-top:12px }
.isl-78 { color:var(--muted); font-size:13px; text-align:center; padding:30px }
.isl-79 { width:auto; min-width:160px }
.isl-80 { width:150px }
.isl-81 { width:130px }
.isl-82 { flex-wrap:wrap; gap:8px; align-items:flex-end }
.isl-83 { flex:2; min-width:200px }
.isl-84 { flex:1; min-width:140px }
.isl-85 { margin-bottom:12px; font-family:var(--font-mono); font-size:12px }
/* v4.1.0 box-overflow sweep: cap the variable-length rule/pattern lists that
   render into bare divs (no .table-card cap) so they scroll past ~15 rows. */
#swpol-rules, #cc-list { max-height: 340px; overflow-y: auto; }
#log-ignore-list { max-height: 132px; overflow-y: auto; }
.isl-86 { flex:1; font-family:var(--font-mono); font-size:12px }
.isl-87 { display:grid; grid-template-columns:repeat(auto-fill,minmax(200px,1fr)); gap:0 }
.isl-88 { margin-top:12px }
.isl-89 { font-size:12px; margin-top:12px }
.isl-90 { display:flex; gap:10px; flex-wrap:wrap; margin-top:24px; padding-top:20px; border-top:1px solid var(--border) }
.isl-91 { font-size: 16px; padding:4px 8px }
.isl-93 { display:flex; justify-content:space-between; align-items:center; margin-bottom:6px }
.isl-94 { font-size:11px; padding:3px 8px }
.isl-95 { color:var(--muted); font-weight:400; text-transform:none; letter-spacing:0 }
.isl-96 { width:100%; min-height:70px; font-family:var(--font-mono); font-size:12px; resize:vertical }
.isl-97 { display:flex; gap:8px; flex-direction:column }
.isl-98 { width:100%; background:var(--accent2) }
.isl-99 { display:none; margin-top:10px }
.isl-100 { color:var(--muted); font-size:13px }
.isl-101 { font-size:12px; padding:4px 10px }
.isl-102 { display:flex; flex-direction:column; gap:12px; max-width:920px }
.isl-103 { margin:24px 0 8px; padding-top:16px; border-top:1px solid var(--border) }
.isl-104 { margin:8px 0 8px 18px; font-size:13px; line-height:1.8 }
.isl-105 { display:none; background:rgba(239,68,68,0.08); border:1px solid rgba(239,68,68,0.3); padding:16px; border-radius:8px; margin-bottom:16px }
.isl-106 { font-weight:600; color:var(--red); margin-bottom:6px }
.isl-107 { margin-bottom:16px; display:none }
.isl-108 { display:flex; flex-wrap:wrap; gap:24px; align-items:flex-start }
.isl-109 { font-weight:600; margin-top:2px }
.isl-110 { font-size:12px; color:var(--muted); font-family: var(--font-mono) }
.isl-111 { margin-top:2px }
.isl-112 { margin-top:2px; font-size:13px }
.isl-113 { display:flex; gap:8px; margin-bottom:12px; flex-wrap:wrap; align-items:center }
.isl-114 { max-width:340px; font-size:13px }
.isl-115 { font-size:12px; margin-left:auto }
.isl-116 { background:var(--surface2); border:1px solid var(--border); border-radius:8px; padding:14px; min-height:300px; max-height:55vh; overflow-y:auto; font-size: 13px; line-height:1.55 }
.isl-117 { color:var(--muted); text-align:center; padding:40px 12px }
.isl-118 { margin-top:12px; display:flex; gap:8px; align-items:flex-end }
.isl-119 { flex:1; resize:vertical; min-height:60px; font-family:inherit; font-size: 13px }
.isl-120 { height:60px; min-width:90px }
.isl-121 { margin-top:6px; font-size:11px; color:var(--muted) }
.isl-122 { display:flex; gap:8px; margin-bottom:16px; flex-wrap:wrap }
.isl-123 { display:flex; flex-direction:column; gap:14px }
.isl-124 { max-width:600px }
.isl-125 { display:flex; align-items:center; gap:16px; margin-bottom:24px }
.isl-126 { width:52px; height:52px; border-radius:12px; display:block; object-fit:contain }
.isl-127 { font-size:28px; font-weight:700 }
.isl-128 { margin-bottom:20px }
.isl-129 { color:var(--muted); padding:6px 12px 6px 0; width:160px }
.isl-130 { color:var(--muted); padding:6px 12px 6px 0 }
.isl-131 { font-size:13px; color:var(--muted); line-height:1.7 }
.isl-132 { position:relative; flex:1; max-width:280px }
.isl-133 { width:auto; padding:8px 12px; font-size:13px; max-width:220px }
.isl-134 { grid-template-columns:repeat(5,1fr) }
.isl-135 { flex:1; min-width:240px; max-width:380px; padding:8px 12px; font-size:13px }
.isl-136 { grid-template-columns:repeat(5,1fr); margin-bottom:24px }
.isl-137 { color:var(--orange) }
.isl-138 { display:none; font-size:12px; color:var(--amber); margin-left:12px }
.isl-139 { grid-template-columns:repeat(4,1fr); margin-bottom:24px }
/* v7.0.2: layout only. This carried a full field skin of its own, which made
   #logs-search-input the only visible text input in the product not on
   .form-input — so it missed the toolbar height contract and rendered 36px
   next to 34px buttons, in its own surface2/6px-radius box. The markup now
   says `form-input isl-140`, matching every other filter input. */
.isl-140 { flex:1; min-width:300px }
.isl-141 { max-width:100px }
.isl-142 { grid-template-columns:repeat(4,1fr); margin-bottom:16px }
/* v4.10.0: the patch/CVE/summary stat rows are defined after the .stats-row
   mobile collapse, so they need their own narrow-width collapse or they stay
   4-5 columns at ~360px and clip. */
@media (max-width: 600px) {
  .isl-134, .isl-136, .isl-139, .isl-142 { grid-template-columns: 1fr; }
}
.isl-143 { display:flex; justify-content:space-between; align-items:center; margin-bottom:8px }
.isl-144 { font-size:12px; color:var(--muted); display:flex; align-items:center; gap:8px }
.isl-145 { max-height:500px; min-height:300px; font-size:12px; overflow-y:auto; background:var(--bg); border:1px solid var(--border); border-radius:6px; padding:8px }
.isl-146 { padding:18px }
.isl-147 { margin:24px 0 12px; display:flex; justify-content:space-between; align-items:center }
.isl-149 { display:flex; gap:0; margin-bottom:-1px; position:relative; z-index:1 }
.isl-150 { border-radius:6px 6px 0 0; border-bottom:1px solid transparent }
.isl-151 { border-radius:6px 6px 0 0; border-bottom:1px solid var(--border); background:var(--surface2) }
.isl-152 { font-size:12px; color:var(--muted); padding:12px 16px 0 }
.isl-153 { margin-top:24px; display:none }
.isl-155 { display:flex; justify-content:space-between; align-items:center; margin-bottom:16px; gap:12px; flex-wrap:wrap }
.isl-156 { display:flex; align-items:center; gap:8px }
.isl-157 { font-size: 13px; font-weight:620; min-width:180px; text-align:center }
.isl-158 { margin-left:8px; font-size:12px }
.isl-159 { display:grid; grid-template-columns:repeat(7,1fr); gap:4px; font-size:11px; color:var(--muted); text-transform:uppercase; letter-spacing:0.5px; padding:0 4px 8px }
.isl-160 { display:grid; grid-template-columns:repeat(7,1fr); gap:4px; min-height:500px }
.isl-161 { display:flex; justify-content:space-between; align-items:center; margin-bottom:16px; flex-wrap:wrap; gap:10px }
.isl-162 { padding:8px; background:var(--surface2); border:1px solid var(--border); border-radius:6px; color:var(--text); font-size:13px; min-width:180px }
.isl-163 { display:grid; grid-template-columns:repeat(4,1fr); gap:12px; min-height:500px }
.isl-164 { color:#60a5fa }
.isl-165 { color:#a78bfa }
.isl-166 { margin-bottom:16px; padding:12px 16px; border:1px solid var(--border); border-radius:8px; background:var(--surface2); display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:10px }
.isl-167 { display:flex; align-items:center; gap:10px; font-size:13px }
.isl-168 { flex:1; min-width:240px; display:flex; gap:8px; align-items:center }
.isl-169 { flex:1; max-width:480px }
.isl-170 { flex:1; min-width:240px; max-width:380px }
.isl-171 { display:none; margin-top:24px }
/* v7.0.0: `flex-wrap: wrap`. Without it these toolbar rows SHRINK their
   controls to fit, and a flex item shrunk below its content width squashes
   the label inside it. Measured on the Network map page, whose toolbar is
   the busiest of the three: its `AI review` button rendered 90.1px against
   98.5px for the byte-identical button on Virtualization -- every computed
   style on the two was identical except width -- and the squeeze pushed the
   icon-to-label gap to 8.8px against the 5px standard.
   Wrapping to a second line is the tidy answer and the one the rest of this
   stylesheet already uses; squashing a control is not. */
.isl-173 { display:flex; gap:10px; margin-bottom:16px; align-items:center;
           flex-wrap: wrap }
.isl-174 { color:var(--muted); font-size:13px; margin-left:auto }
.isl-175 { padding:16px }
.isl-176 { background:var(--surface); border-radius:6px }
/* The network map is a 600px-tall <svg>, but it's wrapped in a .table-card
   (overflow:auto; max-height:480px) for its border/background — which clipped
   the map and added a redundant inner scrollbar. The map isn't a table: let it
   size to its content and scroll with the page instead. #page-netmap id-scope
   beats the bare .table-card rule. */
#page-netmap > .table-card { max-height:none; overflow:visible; contain:none; }
.isl-177 { flex:1; min-width:200px; max-width:340px; padding:8px 12px; font-size:13px }
.isl-178 { display:flex; align-items:flex-end; justify-content:space-between; flex-wrap:wrap; gap:12px; margin-bottom:8px }
.isl-180 { margin-bottom:0 }
.isl-181 { display:flex; gap:8px; flex-wrap:wrap; align-items:center }
.isl-182 { font-weight:500 }
.isl-183 { display:none; padding:24px; background:var(--surface); border:1px solid var(--border); border-radius:8px; text-align:center; color:var(--muted); font-size:13px }
.isl-184 { display:none; font-size:12px; color:var(--muted); padding:6px 12px; font-style:italic }
.isl-185 { display:flex; flex-direction:column; gap:24px }
.isl-186 { display:flex; gap:10px; margin-bottom:20px; flex-wrap:wrap; align-items:center }
.isl-187 { width:30% }
.isl-188 { width:90px }
.isl-189 { width:120px }
.isl-190 { width:220px }
.isl-191 { font-size:12px; display:none; padding:8px 12px; border-radius:6px; margin-bottom:8px }
.isl-192 { min-height:100px }
.isl-193 { max-width:640px; max-height:85vh; overflow-y:auto }
.isl-194 { background:var(--surface2); padding:12px; border-radius:6px; margin:12px 0; font-size:11px; color:var(--muted) }
.isl-195 { display:grid; grid-template-columns:1fr 1fr 1fr; gap:8px; margin-bottom:8px; font-size:11px; color:var(--muted); text-transform:uppercase }
.isl-196 { display:grid; grid-template-columns:1fr 1fr 1fr; gap:8px; margin-bottom:6px; align-items:center }
.isl-197 { display:grid; grid-template-columns:1fr 1fr 1fr; gap:8px; margin-bottom:14px; align-items:center }
.isl-198 { border-top:1px solid var(--border); padding-top:12px; margin-bottom:8px }
.isl-199 { font-size:13px; font-weight:600; margin-bottom:6px }
.isl-200 { font-size:11px; color:var(--muted); margin-bottom:8px }
.isl-201 { display:flex; flex-direction:column; gap:6px }
.isl-202 { color:var(--red); font-size:13px; margin:8px 0; display:none }
.isl-203 { max-width:500px }
.isl-204 { border-top:1px solid var(--border); padding-top:12px }
.isl-205 { background:rgba(0,0,0,0.92) }
.isl-206 { max-width:1100px; width:90vw; height:80vh; display:flex; flex-direction:column; padding:12px }
.isl-207 { display:flex; align-items:center; gap:12px; margin-bottom:8px }
.isl-208 { flex:1; font-weight:600; font-family: var(--font-mono); font-size:13px }
.isl-209 { flex:1; background:#000; border-radius:6px; overflow:hidden }
.isl-210 { min-height:80px }
.isl-211 { min-height:160px; font-family: var(--font-mono) }
.isl-212 { display:none; margin-bottom:16px }
.isl-213 { font-family: var(--font-mono); min-height:240px; resize:vertical; white-space:pre }
.isl-214 { display:none; font-size:12px; padding:10px 12px; border-radius:6px; margin-bottom:8px; font-family: var(--font-mono); white-space:pre-wrap; word-break:break-word }
.isl-215 { display:flex; gap:8px; flex-wrap:wrap; margin-bottom:10px; align-items:center; padding:10px; background:var(--surface2); border:1px solid var(--border); border-radius:6px }
.isl-216 { font-size:11px; color:var(--muted); margin-left:auto }
.isl-217 { margin-right:auto }
.isl-218 { display:none; font-size:12px; color:var(--amber); margin-bottom:8px; cursor:pointer }
.isl-219 { margin-right:6px }
.isl-220 { min-height:120px; max-height:60vh; overflow-y:auto }
.isl-221 { display:flex; gap:6px; margin-bottom:12px; flex-wrap:wrap }
.isl-222 { background:var(--surface2); border:1px solid var(--border); border-radius:6px; padding:10px 12px; font-family: var(--font-mono); font-size:12px; white-space:pre-wrap; word-break:break-word; max-height:280px; overflow-y:auto; color:var(--muted) }
.isl-223 { min-height:120px; max-height:70vh; overflow-y:auto }
.isl-224 { font-size:13px; color:var(--muted); margin-bottom:12px }
.isl-225 { display:flex; align-items:center; gap:8px; margin:6px 0; cursor:pointer; font-size:13px }
.isl-226 { width:100%; font-family: var(--font-mono); font-size:13px; background:var(--surface2); border:1px solid var(--border); color:var(--text); padding:10px; border-radius:6px; resize:vertical }
.isl-227 { display:flex; gap:6px; margin-bottom:14px; border-bottom:1px solid var(--border); padding-bottom:0 }
.isl-228 { display:flex; justify-content:space-between; align-items:center; margin-bottom:10px }
.isl-229 { font-weight:600; font-size: 13px }
.isl-230 { display:none; padding:24px; text-align:center; color:var(--muted); background:var(--surface2); border:1px dashed var(--border); border-radius:8px; font-size:13px }
.isl-231 { display:none; padding:12px; background:rgba(245,158,11,0.08); border:1px solid rgba(245,158,11,0.3); border-radius:6px; color:var(--amber); font-size:13px; margin-bottom:12px }
.isl-232 { color:var(--muted); text-align:center; padding:24px; font-size:13px }
.isl-233 { display:flex; gap:6px; margin-bottom:6px }
.isl-234 { min-height:300px; font-family: var(--font-mono); font-size:13px }
.isl-235 { display:none; min-height:300px; padding:12px; background:var(--surface2); border:1px solid var(--border); border-radius:6px; font-size:13px; line-height:1.55; overflow-y:auto; max-height:50vh }
.isl-236 { min-height:120px; max-height:65vh; overflow-y:auto }
.isl-237 { padding:10px; background:var(--surface2); border:1px solid var(--border); border-radius:6px; font-family: var(--font-mono) }
.isl-238 { font-size:11px; padding:2px 8px }
.isl-239 { padding:10px; background:var(--surface2); border:1px solid var(--border); border-radius:6px; font-family: var(--font-mono); word-break:break-all }
.isl-240 { padding:10px; background:var(--surface2); border:1px solid var(--border); border-radius:6px; font-size:13px; color:var(--muted) }
.isl-241 { display:grid; grid-template-columns:1fr 1fr 1fr; gap:10px }
.isl-242 { font-size:11px; color:var(--muted); margin-top:4px; padding-left:24px }
.isl-243 { display:flex; gap:6px; margin:8px 0 18px 0; font-size:11px; color:var(--muted); text-transform:uppercase; letter-spacing:.5px }
.isl-244 { color:var(--muted); font-weight:400; font-size:11px }
.isl-245 { display:flex; align-items:center; gap:8px; cursor:pointer; font-size:13px }
.isl-246 { font-size:12px; background:var(--surface2); border:1px solid var(--border); border-radius:6px; padding:10px; color:var(--muted) }
.isl-247 { font-size:13px; margin-bottom:12px }
.isl-248 { background:var(--bg); border:1px solid var(--border); border-radius:6px; padding:12px; font-size:12px; overflow-x:auto; white-space:pre-wrap; word-break:break-all }
.isl-249 { font-size:11px; color:var(--muted); margin-top:8px }
.isl-250 { display:flex; gap:4px; border-bottom:1px solid var(--border); margin-bottom:14px; margin-top:8px }
.isl-251 { font-size:13px; min-height:240px }
/* v6.4.0 box-overflow: the ACME timeline + logs panes grow one row per cert
   event / acme.sh run and sat inside an overflow:hidden modal — cap + scroll
   internally instead of clipping (box-overflow rule). */
.isl-252 { font-size:13px; min-height:240px; max-height:52vh; overflow-y:auto; display:none }
.isl-253 { min-height:280px }
/* v6.4.0 box-overflow: outer stacks that grow one card/table PER DEVICE (command
   queue) or PER INSTANCE (PBS datastores) — the inner tables are each capped,
   but the outer stack was unbounded on a busy fleet. Scroll the stack. */
#cmdqueue-body, #pbs-body { max-height:65vh; overflow-y:auto }
.isl-254 { background:var(--bg); border:1px solid var(--border); border-radius:6px; padding:12px; font-size:11px; max-height:420px; overflow:auto; white-space:pre-wrap; word-break:break-word; margin:0 }
.isl-255 { display:none; min-height:280px }
.isl-256 { background:var(--surface); border:1px solid var(--border); border-radius:6px; padding:14px; font-size:13px; white-space:pre-wrap; min-height:120px }
.isl-257 { margin-top:14px; display:none }
.isl-258 { font-size:11px; color:var(--muted); text-transform:uppercase; letter-spacing:.5px; margin-bottom:6px }
.isl-259 { background:var(--bg); border:1px solid var(--border); border-radius:6px; padding:10px; font-size:12px; margin:0; white-space:pre-wrap }
.isl-260 { margin-top:8px; font-size:12px; display:none }
.isl-261 { margin-top:14px; display:flex; gap:8px; flex-wrap:wrap }
.isl-262 { display:flex; flex-direction:column; gap:10px; margin-bottom:14px }
.isl-263 { font-family:var(--font-mono); font-size:12px; width:100% }
.isl-264 { font-family:var(--font-mono); max-width:160px }
.isl-265 { display:flex; gap:8px; padding-top:14px; border-top:1px solid var(--border) }
.isl-266 { max-height:60vh; overflow-y:auto }
.isl-267 { display:flex; gap:14px; font-size:13px }
.isl-268 { display:flex; gap:10px; margin-bottom:12px; align-items:flex-end; flex-wrap:wrap }
.isl-269 { flex:1; min-width:160px }
.isl-270 { max-height:50vh; overflow-y:auto }
.isl-271 { width:100%; background:var(--surface2); border:1px solid var(--border); color:var(--text); padding:8px; border-radius:6px; font-size:13px; resize:vertical }
.isl-272 { width:28px; height:28px; border-radius:4px; background:#3b82f6; cursor:pointer; border:2px solid transparent }
.isl-273 { width:28px; height:28px; border-radius:4px; background:#22c55e; cursor:pointer; border:2px solid transparent }
.isl-274 { width:28px; height:28px; border-radius:4px; background:#f59e0b; cursor:pointer; border:2px solid transparent }
.isl-275 { width:28px; height:28px; border-radius:4px; background:#ef4444; cursor:pointer; border:2px solid transparent }
.isl-276 { width:28px; height:28px; border-radius:4px; background:#a855f7; cursor:pointer; border:2px solid transparent }
.isl-277 { width:28px; height:28px; border-radius:4px; background:#14b8a6; cursor:pointer; border:2px solid transparent }
.isl-278 { width:28px; height:28px; border-radius:4px; background:#64748b; cursor:pointer; border:2px solid transparent }
.isl-279 { justify-content:space-between }
.isl-280 { display:none; color:var(--red); border-color:rgba(239,68,68,0.3) }
.isl-281 { font-size:11px; color:var(--muted); margin:4px 0 8px; display:none }
.isl-282 { display:none; font-size:12px; color:var(--muted); background:var(--surface2); border:1px solid var(--border); padding:10px; border-radius:6px }
.isl-283 { display:none; font-size:11px; color:var(--muted) }
.isl-284 { display:grid; grid-template-columns:1fr 1fr; gap:4px; font-size:13px }
.isl-285 { display:flex; flex-wrap:wrap; gap:6px; margin:16px 0; max-height:200px; overflow-y:auto }
.isl-286 { font-size:16px; text-align:center; max-width:80px }
.isl-287 { white-space:nowrap; padding:0 14px }
.isl-288 { font-size:12px; color:var(--muted); margin-top:3px }
.isl-289 { color:var(--muted); font-size:11px; margin-left:6px }
.isl-290 { font-family:var(--font-mono); font-size:12px; resize:vertical }
.isl-291 { display:flex; flex-wrap:wrap; gap:8px; max-height:200px; overflow-y:auto; padding:8px; background:var(--surface2); border:1px solid var(--border); border-radius:8px }
.isl-292 { color:var(--muted); font-size:12px }
.cs-device-tools { display:flex; align-items:center; gap:6px; margin-bottom:6px }
.cs-device-tools-spacer { flex:1 1 auto }
.isl-293 { font-family:var(--font-mono); font-size:12px; background:var(--surface2); border:1px solid var(--border); border-radius:8px; padding:14px; max-height:60vh; overflow-y:auto; white-space:pre-wrap; word-break:break-word }
.isl-294 { max-width:860px; width:95vw }
.isl-295 { display:none; background:rgba(59,130,246,0.08); border:1px solid rgba(59,130,246,0.2); border-radius:8px; padding:10px 14px; margin-bottom:12px; font-size:13px; color:var(--accent2-ink) }
.isl-296 { display:none; background:rgba(245,158,11,0.1); border:1px solid rgba(245,158,11,0.3); border-radius:8px; padding:10px 14px; margin-bottom:16px; font-size:13px; color:var(--amber) }
.isl-297 { display:flex; gap:4px; flex-wrap:wrap; margin-bottom:16px; border-bottom:1px solid var(--border); padding-bottom:10px }
.isl-298 { font-family:var(--font-mono); font-size:12px; min-height:280px; resize:vertical }
.isl-299 { font-family:var(--font-mono); font-size:12px; min-height:180px; resize:vertical }
.isl-300 { font-family:var(--font-mono); font-size:12px; min-height:200px; resize:vertical }
.isl-301 { font-family:var(--font-mono); font-size:12px; min-height:140px; resize:vertical }
.isl-302 { display:flex; flex-direction:column; gap:12px }
.isl-303 { display:flex; flex-direction:column; gap:8px }

/* CSP L1 fix: additional utility classes for app.js inline styles */
.c-muted-padded { color: var(--muted); padding: 4px 12px 4px 0; }
.fw-600 { font-weight: 600; }
.fw-500 { font-weight: 500; }
.badge-sm { font-size: 11px; padding: 3px 10px; }

/* v7.2.0: Threat intel — what the logs showed about an address. 11px and 12px are
   the design's own stops; colours are the theme tokens, so every theme and the
   light set get a legible chip without a rule of their own. */
.ipi-chip { display: inline-block; font-size: 11px; padding: 2px 8px; margin: 0 4px 4px 0;
  border: 1px solid var(--hair); border-radius: 999px; background: var(--surface2); color: var(--text); }
.ipi-detail > td { background: var(--hair-2); padding: 10px 14px; }
.ipi-facts { display: grid; grid-template-columns: max-content 1fr; gap: 4px 14px; margin: 0; font-size: 12px; }
.ipi-facts dt { color: var(--muted); }
.ipi-facts dd { margin: 0; overflow-wrap: anywhere; }
.ipi-comment { display: block; margin-top: 2px; font-family: var(--font-mono); font-size: 11px; color: var(--muted); }
.badge-xs { font-size: 11px; padding: 3px 8px; }
.cell-pad { padding: 6px 4px; }
.cell-padl { padding: 6px 16px 6px 0; }
.fs-10 { font-size: 10px; }
.code-pill { background: var(--bg); padding: 2px 8px; border-radius: 4px; border: 1px solid var(--border); font-family: var(--font-mono); }
.mono-12 { font-family: var(--font-mono); font-size: 12px; }
.fw-600-mb10 { font-weight: 600; margin-bottom: 10px; }
.nowrap { white-space: nowrap; }
.hint-nowrap { font-size: 12px; color: var(--muted); white-space: nowrap; }
.cell-sm { padding: 4px 8px; font-size: 11px; }
.hint-mb6 { font-size: 12px; color: var(--muted); margin-bottom: 6px; }
.cell-m { padding: 6px 8px; }
.c-red-p20 { color: var(--red); padding: 20px; }
.row-between { display: flex; justify-content: space-between; align-items: center; }
.empty-p20 { color: var(--muted); padding: 20px; text-align: center; }
.c-muted-13 { font-size: 13px; color: var(--muted); }
.fw-600-13 { font-size: 13px; font-weight: 600; }
.p-16 { padding: 16px; }
.p-8 { padding: 8px; }
.mb-14 { margin-bottom: 14px; }
.fs-12-p4-10 { font-size: 12px; padding: 4px 10px; }
.c-green-bold { color: var(--green); font-weight: 600; }
.c-red-bold { color: var(--red); font-weight: 600; }
.c-amber-bold { color: var(--amber); font-weight: 600; }
.pointer { cursor: pointer; }
.cursor-default { cursor: default; }
.p-2-8 { padding: 2px 8px; }
.fs-11-bold { font-size: 11px; font-weight: 600; }
.fs-12-bold { font-size: 12px; font-weight: 600; }
.fw-600-14 { font-size: 13px; font-weight: 600; }
.row-space-between { display: flex; justify-content: space-between; }
.row-between-center { display: flex; align-items: center; justify-content: space-between; }
.row-4-center { display: flex; align-items: center; gap: 4px; }
.row-4 { display: flex; gap: 4px; }
.row-6 { display: flex; gap: 6px; }
.row-6-center { display: flex; gap: 6px; align-items: center; }
.mb-4 { margin-bottom: 4px; }
.mt-6 { margin-top: 6px; }
.mt-12 { margin-top: 12px; }
.fs-12-mb6 { font-size: 12px; margin-bottom: 6px; }
.opacity-60 { opacity: 0.6; }
.opacity-50 { opacity: 0.5; }
.va-middle { vertical-align: middle; }
.meta-red { font-size: 11px; color: var(--red); }
.meta-green { font-size: 11px; color: var(--green); }
.meta-accent { font-size: 11px; color: var(--accent-ink); }
.text-truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cell-trunc { max-width: 240px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.p-4-0 { padding: 4px 0; }
.p-0 { padding: 0; }
.p-8-12 { padding: 8px 12px; }
.p-12 { padding: 12px; }
.p-16-0 { padding: 16px 0; }
.gap-8 { gap: 8px; }
.c-muted-fs13 { color: var(--muted); font-size: 13px; }
.fs-12-center { font-size: 12px; text-align: center; }
.border-top { border-top: 1px solid var(--border); }
.border-bottom { border-bottom: 1px solid var(--border); }
.c-accent-12 { color: var(--accent-ink); font-size: 12px; }
.c-accent-bold { color: var(--accent-ink); font-weight: 600; }
.min-w-0 { min-width: 0; }
.max-w-full { max-width: 100%; }
.fs-13-accent { font-size: 13px; color: var(--accent-ink); }
.meta-right { font-size: 11px; color: var(--muted); text-align: right; }

/* CSP L1 fix: one-off auto-generated classes from app.js */
.isl-304 { color: var(--muted); opacity: .3; font-size: 10px }
.isl-305 { font-size: 10px; opacity: .7 }
.isl-306 { color: var(--accent-ink); font-size: 10px }
.isl-307 { padding: 6px 10px; font-size: 11px; font-weight: 500; background: var(--surface); color: var(--muted); border: 1px solid var(--border); cursor: pointer }
.isl-308 { display: inline-flex; border-radius: 6px; overflow: hidden; border: 1px solid var(--border) }
.isl-309 { font-size: 12px; opacity: 0.85; margin: 6px 0 4px }
.isl-310 { font-size: 12px; opacity: 0.7; margin-top: 6px }
.isl-311 { display: inline-flex; align-items: center; gap: 5px; padding: 3px 10px; border-radius: 20px; font-size: 11px; font-weight: 500; cursor: pointer; border: 1px solid var(--border); background: transparent; color: var(--muted); font-family: var(--font) }
.isl-312 { display: inline-flex; align-items: center; gap: 5px; padding: 3px 10px; border-radius: 20px; font-size: 11px; cursor: pointer; border: 1px solid var(--border); color: var(--muted); background: transparent; font-family: var(--font) }
.isl-313 { font-size: 22px; line-height: 1 }
/* .isl-314 — dynamic colour, applied via data-color attribute */
.isl-315 { font-size: 10px; color: var(--muted); background: var(--surface2); padding: 1px 5px; border-radius: 4px }
.isl-316 { font-size: 10px; padding: 1px 5px }
.isl-317 { text-align: center; padding: 0 6px }
.isl-319 { color: var(--text); text-decoration: none; font-weight: 500 }
.isl-320 { font-size: 10px; color: var(--muted); background: var(--surface2); padding: 1px 4px; border-radius: 3px }
.isl-321 { width: 36px; text-align: center }
.isl-322 { width: 190px }
.isl-323 { width: 160px }
.isl-324 { width: 130px }
.isl-325 { width: 50px }
/* v3.0.5: hover effect was inline `onmouseover`/`onmouseout` pre-CSP; now CSS. */
.isl-327 { font-family: var(--font-mono); font-size: 11px; background: var(--surface2); padding: 2px 6px; border-radius: 4px }
.isl-328 { font-family: var(--font-mono); font-size: 12px; color: var(--muted) }
.isl-329 { display: inline-block; width: 12px; height: 12px; border-radius: 50%; margin: 2px; background: var(--muted) }
/* .isl-330 — dynamic colour, applied via data-color attribute */
/* .isl-331 — dynamic colour, applied via data-color attribute */
.isl-332 { padding: 8px 0 }
.isl-333 { margin-top: 16px; max-height: 240px; overflow-y: auto }
.isl-334 { text-align: left; font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; padding: 8px; font-weight: 500 }
.isl-335 { text-align: center; font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; padding: 8px; font-weight: 500; width: 70px }
.isl-337 { font-size: 12px; color: var(--muted); margin-bottom: 4px }
.isl-338 { display: flex; gap: 10px; flex-wrap: wrap }
.isl-339 { display: flex; align-items: center; gap: 5px; cursor: pointer; font-size: 12px }
.isl-340 { accent-color: var(--accent) }
.isl-341 { font-family: var(--font-mono); font-size: 11px }
.isl-342 { background: rgba(59,126,255,0.1); color: var(--accent-ink) }
.isl-343 { font-weight: 600 }
.isl-344 { font-size: 12px; color: var(--muted); max-width: 280px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
.isl-345 { color: var(--muted); font-size: 12px; white-space: nowrap }
.isl-346 { color: var(--red); border-color: rgba(239,68,68,0.3); font-size: 13px; padding: 4px 8px }
.isl-347 { background: rgba(239,68,68,0.18); color: var(--red-on-tint); font-size: 10px }
.isl-348 { font-family: var(--font-mono) }
.isl-349 { font-family: var(--font-mono); font-size: 11px; margin-right: 10px }
.isl-350 { color: var(--text); text-decoration: none }
.isl-351 { font-size: 11px; padding: 4px 8px; margin-right: 4px }
.isl-352 { font-size: 11px; padding: 4px 8px }
.isl-354 { display: grid; gap: 16px }
.isl-355 { background: var(--surface2); border: 1px solid var(--border); border-radius: 10px; padding: 16px }
.isl-356 { font-size: 12px; color: var(--muted); margin-bottom: 8px; text-transform: uppercase; letter-spacing: .5px }
.isl-357 { font-size: 12px; color: var(--muted); margin-top: 12px }
/* v3.14.0: per-device metric time-series charts */
.metric-svg { width: 100%; height: 150px; display: block; }
.metric-chart-head { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; flex-wrap: wrap; margin-bottom: 4px; }
.metric-stats { font-size: 11px; color: var(--muted); }
.metric-range-picker { display: flex; gap: 6px; margin-bottom: 12px; }
.metric-range-btn { padding: 4px 12px; border-radius: 6px; border: 1px solid var(--border); background: var(--surface2); color: var(--muted); font-size: 13px; cursor: pointer; }
.metric-range-btn.sel { background: var(--accent); border-color: var(--accent); color: var(--accent-contrast, #fff); }
.metric-note { margin-bottom: 12px; }
/* v3.14.0 (#41): chargeback cost-allocation tables */
.chargeback-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 16px; }
.chargeback-col .isl-356 { margin-bottom: 6px; }
.isl-358 { font-family: var(--font-mono); font-size: 12px; color: var(--accent-ink) }
.isl-359 { padding: 16px; color: var(--muted); font-size: 12px; text-align: center }
.isl-360 { background: var(--bg); border: 1px solid var(--border); border-radius: 10px; padding: 20px; margin-bottom: 16px }
.isl-361 { display: flex; gap: 20px; align-items: flex-start; flex-wrap: wrap }
.isl-362 { background: #fff; padding: 8px; border-radius: 8px; flex-shrink: 0; min-width: 168px; min-height: 168px; display: flex; align-items: center; justify-content: center }
.isl-363 { color: var(--muted); font-size: 12px }
.isl-364 { flex: 1; min-width: 200px }
.isl-365 { font-size: 13px; font-weight: 620; margin-bottom: 8px }
.isl-366 { font-size: 12px; color: var(--muted); margin-bottom: 12px }
.isl-367 { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: .5px; margin-bottom: 4px }
.isl-368 { font-family: var(--font-mono); font-size: 13px; letter-spacing: 2px; color: var(--accent-ink); word-break: break-all; background: var(--surface2); padding: 10px; border-radius: 6px; cursor: pointer }
.isl-369 { max-width: 200px; text-align: center; font-size: 16px; letter-spacing: 4px }
.isl-370 { font-size: 11px; font-family: var(--font-mono); color: var(--muted); margin-top: 2px }
.isl-371 { padding: 4px 6px; font-size: 11px }
.isl-372 { display: inline-flex; align-items: center; margin-left: 6px; padding: 1px 5px; border-radius: 4px; font-size: 10px; font-weight: 600; color: var(--amber); background: var(--amber-soft); border: 1px solid var(--amber-edge); white-space: nowrap; vertical-align: middle }
.isl-373 { width: 10px; height: 10px; margin-right: 2px; flex-shrink: 0 }
.isl-374 { font-weight: 600 }
.isl-375 { display: flex; gap: 4px; align-items: center }
/* .isl-376 — dynamic colour, applied via data-color attribute */
/* .isl-377 — dynamic colour, applied via data-color attribute */
.isl-378 { font-size: 13px; font-weight: 620; margin: 16px 0 8px; color: var(--muted) }
.isl-379 { background: var(--surface2); border: 1px solid var(--border); border-radius: 8px; padding: 12px; margin-bottom: 8px }
.isl-380 { display: flex; justify-content: space-between; margin-bottom: 6px }
.isl-381 { font-size: 12px; color: var(--accent-ink) }
.isl-382 { max-height: 120px; overflow-y: auto }
.isl-383 { color: var(--muted); font-size: 13px; padding: 12px }
.isl-384 { text-align: center; font-weight: 600 }
.isl-385 { text-align: center; color: var(--muted) }
.isl-387 { font-size: 12px; padding: 6px 12px }
.isl-388 { background: rgba(239,68,68,0.1); border: 1px solid rgba(239,68,68,0.3); padding: 12px; border-radius: 8px; margin-bottom: 16px; font-size: 13px; color: var(--red) }
.isl-389 { background: var(--surface2); padding: 2px 6px; border-radius: 4px }
.isl-390 { background: var(--surface2); border: 1px solid var(--border); border-radius: 8px; padding: 12px; margin-bottom: 8px }
.isl-391 { display: flex; justify-content: space-between; gap: 12px; margin-bottom: 8px }
.isl-392 { color: white; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; text-transform: uppercase }
.isl-393 { margin-left: 8px; font-size: 13px; color: var(--accent-ink) }
.isl-394 { margin-left: 8px; font-size: 11px; color: var(--muted) }
.isl-395 { font-size: 11px; color: var(--muted); white-space: nowrap }
.isl-396 { font-size: 13px; margin-bottom: 6px }
.isl-397 { display: flex; gap: 8px; font-size: 11px; flex-wrap: wrap; align-items: center }
.isl-398 { padding: 2px 6px; font-size: 11px; margin-left: auto }
.isl-399 { padding: 2px 6px; font-size: 11px }
.isl-400 { display: inline-block; background: var(--surface2); border: 1px solid var(--border); border-radius: 12px; padding: 2px 8px; font-size: 11px; margin: 1px; font-family: var(--font-mono) }
/* .isl-401 — dynamic colour, applied via data-color attribute */
.isl-402 { text-align: center; color: var(--green); font-weight: 600 }
.isl-403 { text-align: center; color: var(--red); font-weight: 600 }
.isl-404 { font-size: 11px; color: var(--muted); font-family: var(--font-mono) }
.isl-405 { font-size: 11px; color: var(--muted); font-style: italic; padding: 4px 0 }
.isl-406 { font-size: 11px; padding: 2px 8px; margin-left: 8px }
.isl-407 { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; flex-wrap: wrap; gap: 8px }
.isl-408 { display: flex; align-items: center; flex-wrap: wrap }
.isl-409 { font-weight: 600 }
.isl-410 { font-size: 13px; color: var(--accent-ink); margin-left: 6px }
.isl-411 { margin-left: 8px; font-size: 12px; color: var(--muted) }
.isl-412 { font-size: 12px; color: var(--muted); cursor: pointer }
.isl-413 { padding-top: 6px }
.isl-414 { max-height: 200px; margin-top: 6px }
.isl-415 { font-size: 12px; font-family: var(--font-mono); color: var(--muted) }
/* v6.0.1: sizing-only. The red used to bleed onto the neutral Edit button that
   shares this class; delete buttons carry .c-danger-outline for the red. */
.isl-416 { padding: 4px 8px; font-size: 11px }
.isl-417 { font-family: var(--font-mono); font-size: 11px; color: var(--muted) }
.isl-418 { text-align: center; color: var(--muted); padding: 20px }
.isl-419 { font-family: var(--font-mono); font-size: 12px; line-height: 1.5; padding: 1px 4px }
.isl-420 { text-align: center; color: var(--red); padding: 40px }
.isl-421 { font-weight: 600; margin-bottom: 6px }
.isl-422 { font-size: 11px; color: var(--muted); margin-top: 10px }
.isl-423 { font-size: 13px; color: var(--accent-ink); cursor: pointer; padding: 4px 0 }
.isl-424 { font-family: var(--font-mono); font-size: 12px; padding: 1px 4px 1px 20px }
.isl-425 { font-size: 12px; background: var(--surface2); padding: 2px 6px; border-radius: 4px }
.isl-426 { font-size: 12px; color: var(--amber) }
.isl-427 { font-size: 12px; color: var(--muted); max-width: 300px; overflow: hidden; text-overflow: ellipsis }
.isl-428 { font-size: 10px; color: var(--muted); padding: 0 4px }
.isl-429 { font-size: 13px; color: var(--muted); padding: 12px 4px; text-align: center; font-style: italic }
.isl-430 { text-align: center; font-size: 13px; color: var(--muted) }
.isl-431 { background: var(--surface2); border: 1px solid var(--border); border-radius: 8px }
.isl-432 { padding: 10px 14px; cursor: pointer; list-style: none; display: flex; align-items: center; gap: 10px; user-select: none }
.isl-433 { font-weight: 620; font-size: 13px }
.isl-434 { padding: 3px 8px; font-size: 11px }
/* v6.4.3: the doc-card Edit/Delete pair, moved OUT of <summary> so the
   buttons are siblings of the disclosure control rather than nested
   inside it (axe nested-interactive). Right-aligned in the open body,
   which is where they now live. */
.cmdb-doc-actions { display: flex; gap: 6px; justify-content: flex-end;
  padding: 0 14px 10px; }
.isl-435 { padding: 3px 8px; font-size: 11px; color: var(--red); border-color: rgba(239,68,68,0.3) }
.isl-436 { padding: 0 14px 14px; border-top: 1px solid var(--border); font-size: 13px; line-height: 1.55 }
.isl-437 { background: var(--surface); padding: 10px; border-radius: 6px; overflow-x: auto }
.isl-438 { margin: 6px 0 6px 22px }
.isl-439 { margin: 12px 0 6px }
.isl-440 { margin: 14px 0 6px }
.isl-441 { margin: 16px 0 8px }
.isl-442 { background: var(--surface); padding: 1px 5px; border-radius: 3px; font-size: 12px }
.isl-443 { text-decoration: none }
.isl-444 { border: 1px solid var(--border); border-radius: 6px; padding: 10px 12px; margin-bottom: 8px; background: var(--surface2); display: flex; justify-content: space-between; align-items: flex-start; gap: 10px }
.isl-445 { flex: 1; min-width: 0 }
.isl-446 { display: flex; gap: 6px; flex-shrink: 0; flex-wrap: wrap; justify-content: flex-end }
.isl-447 { border: 1px solid var(--border); border-radius: 6px; margin-bottom: 8px; background: var(--surface2) }
.isl-448 { padding: 10px 12px; cursor: pointer; display: flex; gap: 12px; align-items: center; flex-wrap: wrap }
.isl-449 { margin: 0; padding: 12px; background: var(--surface); border-top: 1px solid var(--border); font-family: var(--font-mono); font-size: 12px; line-height: 1.5; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word; max-height: 400px; overflow-y: auto }
.isl-450 { display: inline-block; margin-left: 6px; padding: 1px 6px; border-radius: 3px; font-size: 10px; background: var(--amber); color: #000; font-weight: 600 }
/* v6.4.3: opacity REMOVED. This is on every row of the Containers table
   unconditionally — not a de-emphasis state, just a blanket 25% fade — and
   it dragged the already-muted cells inside each row from 4.98:1 to 3.38:1.
   33 of the 48 contrast violations on a populated instance were this one
   declaration. Nothing replaces it: the rows were never meant to be dim. */
.isl-451 { }
.isl-452 { background: var(--amber); color: #000; padding: 8px 12px; border-radius: 6px; margin-bottom: 12px; font-size: 12px; font-weight: 500 }
.isl-453 { background: var(--surface); padding: 1px 5px; border-radius: 3px; font-size: 11px }
/* .isl-454 — dynamic colour, applied via data-color attribute */
.isl-455 { font-size: 10px; padding: 1px 7px }
.isl-456 { font-size: 11px; color: var(--muted); margin-top: 4px; font-family: var(--font-mono); display: flex; gap: 12px }
/* .isl-457 — dynamic colour, applied via data-color attribute */
.isl-458 { display: flex; gap: 4px; margin-top: 8px; flex-wrap: wrap }
.isl-459 { font-size: 11px; padding: 3px 8px; color: var(--red); border-color: rgba(239,68,68,0.3) }
.isl-460 { border: 1px solid var(--border); border-radius: 6px; padding: 10px 12px; margin-bottom: 8px; background: var(--surface2) }
.isl-461 { display: flex; justify-content: space-between; align-items: center; gap: 10px; flex-wrap: wrap }
.isl-462 { font-weight: 600; display: flex; align-items: center; gap: 6px }
.isl-463 { display: flex; gap: 8px; align-items: center; font-size: 12px }
.isl-464 { font-size: 10px; padding: 2px 6px }
.isl-465 { font-size: 12px; color: var(--muted); margin-top: 4px; font-family: var(--font-mono) }
.isl-466 { margin-top: 6px; display: flex; gap: 4px; flex-wrap: wrap }
.isl-467 { cursor: grab }
.isl-468 { text-align: left; border-bottom: 1px solid var(--border) }
.isl-469 { padding: 6px 8px; font-weight: 500 }
.isl-470 { padding: 6px 8px; color: var(--muted); font-size: 12px }
.isl-471 { border: 1px solid var(--border); border-radius: 6px; padding: 10px 12px; margin-bottom: 8px; background: var(--surface2); display: flex; justify-content: space-between; align-items: center; gap: 10px }
.isl-472 { font-size: 11px; margin-left: 8px; background: rgba(255,255,255,0.04); padding: 1px 5px; border-radius: 3px; color: var(--muted) }
.isl-473 { color: var(--muted); font-size: 11px; margin-top: 2px }
.isl-474 { color: var(--accent-ink); font-size: 10px; margin-left: 6px; background: rgba(59,126,255,0.12); padding: 1px 5px; border-radius: 3px; text-transform: uppercase }
.isl-475 { padding: 2px 6px; font-size: 11px; margin-right: 6px }
.isl-476 { font-family: var(--font-mono); color: var(--muted) }
.isl-477 { background: var(--surface); padding: 1px 5px; border-radius: 3px; font-size: 11px; margin-right: 4px }
.isl-478 { color: var(--red); margin-top: 4px }
.isl-479 { color: var(--amber); margin-top: 4px }
.isl-480 { width: 100%; margin-top: 8px; font-family: var(--font-mono); font-size: 11px; border-collapse: collapse }
.isl-481 { border-bottom: 1px solid var(--border); text-align: left }
.isl-482 { padding: 4px 8px; word-break: break-all }
.isl-483 { margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--border) }
.isl-484 { font-size: 13px }
.isl-485 { color: var(--red); font-size: 12px; margin-top: 4px }
.isl-486 { display: grid; grid-template-columns: 140px 1fr; gap: 8px 14px }
.isl-487 { color: var(--muted); text-align: center; padding: 60px }
.isl-488 { display: flex; align-items: center; gap: 10px; margin-bottom: 10px }
.isl-490 { font-size: 11px; color: var(--muted); opacity: 0.7 }
.isl-491 { flex: 1; height: 1px; background: var(--border) }
.isl-492 { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 12px }
.isl-493 { font-size: 10px; color: var(--amber); background: rgba(245,158,11,0.12); padding: 2px 6px; border-radius: 3px; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600 }
.isl-494 { font-size: 10px; color: var(--accent-ink); background: rgba(59,126,255,0.12); padding: 2px 6px; border-radius: 3px; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600 }
.isl-495 { display: flex; gap: 6px; margin-top: 8px; border-top: 1px solid var(--border); padding-top: 8px }
.isl-496 { border-radius: 8px; padding: 12px 14px; background: var(--surface2); transition: background 0.1s; height: 100%; display: flex; flex-direction: column; justify-content: space-between; cursor: pointer; border: 1px solid var(--border) }
.isl-497 { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 6px }
.isl-498 { font-weight: 620; font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
.isl-499 { font-family: var(--font-mono); font-size: 11px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
.isl-500 { font-size: 12px; color: var(--muted); margin-top: 6px; line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden }
.isl-501 { text-decoration: none; color: inherit; display: block }
.isl-502 { font-size: 11px; padding: 4px 8px; color: var(--red); border-color: rgba(239,68,68,0.3) }
.isl-503 { margin-top: 6px; background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; padding: 10px 12px; font-size: 12px; max-height: 200px; overflow-y: auto; white-space: pre-wrap; word-break: break-word }
.isl-504 { font-size: 11px; color: var(--muted); margin-top: 2px }
.isl-505 { border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px; margin-bottom: 10px; background: var(--surface) }
.isl-506 { display: flex; justify-content: space-between; align-items: center; gap: 8px }
.isl-507 { color: var(--muted); padding: 20px }
.isl-508 { max-width: 720px; width: 92vw; max-height: 88vh; display: flex; flex-direction: column }
.isl-509 { padding: 8px 16px; font-size: 11px; color: var(--muted); border-bottom: 1px solid var(--border) }
.isl-510 { flex: 1; overflow: auto; padding: 14px 16px; font-size: 13px; line-height: 1.5; white-space: pre-wrap; font-family: var(--font) }  /* v6.4.1: body anchor — AI answers are prose */
.isl-511 { padding: 10px 16px; border-top: 1px solid var(--border); display: flex; gap: 8px; flex-wrap: wrap }
.isl-512 { display: flex; align-items: center; gap: 10px; color: var(--muted) }
.isl-513 { color: var(--red); white-space: pre-wrap }
.isl-514 { background: var(--surface2); border: 1px solid var(--border); padding: 10px 12px; border-radius: 6px; overflow-x: auto; max-height: 360px; overflow-y: auto; font-size: 12px; margin: 8px 0; line-height: 1.5; font-family: var(--font-mono) }
.isl-515 { background: var(--surface2); padding: 1px 5px; border-radius: 3px; font-size: 12px; font-family: var(--font-mono) }
.isl-516 { font-size: 13px; font-weight: 620; margin: 10px 0 4px }
.isl-517 { font-size: 13px; font-weight: 620; margin: 12px 0 6px; border-bottom: 1px solid var(--border); padding-bottom: 4px }
.isl-518 { font-size: 16px; font-weight: 700; margin: 14px 0 8px }
.isl-519 { margin: 6px 0; padding-left: 22px }
.isl-520 { border-left: 3px solid var(--border); padding: 2px 10px; color: var(--muted); margin: 6px 0 }
.isl-521 { margin: 6px 0; line-height: 1.55 }
/* v7.0.0: markdown tables. renderMarkdown had no table support, so every table
   in the documentation viewer rendered as literal `|` text — 80 of the 138 doc
   pages contain one and docs/features.md is tables-only by policy. The same
   renderer serves the KB and the AI chat, so a table in a model's answer was
   equally unreadable.

   Sized on the app's own scale (13px cells, 11px headers) so a doc table looks
   like the product's tables rather than a browser default, and the wrapper
   scrolls rather than widening the modal — the reference tables are wide. */
.md-table-wrap { overflow-x: auto; margin: 10px 0; }
/* v7.0.0: the product map. Cards rather than a list because the point is to
   SCAN 89 items and recognise one, and a description under each name is what
   makes recognition possible — a bare list of names is what the sidebar
   already is. Auto-fill so the count of columns follows the dialog width. */
.sitemap-group { margin-bottom: 14px; }
.sitemap-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 8px;
  margin-top: 8px;
}
.sitemap-card {
  display: flex;
  flex-direction: column;
  gap: 3px;
  text-align: left;
  padding: 9px 11px;
  border: 1px solid var(--hair);
  border-radius: 8px;
  background: var(--surface);
  cursor: pointer;
  font-family: var(--font);
}
.sitemap-card:hover { background: var(--hair-2); border-color: var(--accent-edge); }
.sitemap-card-name { font-size: 13px; font-weight: 620; color: var(--text); }
.sitemap-card-desc {
  font-size: 12px;
  color: var(--muted);
  line-height: 1.4;
  /* Two lines, then ellipsis — a long title must not make one card twice the
     height of its neighbours and break the grid's rhythm. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.md-table {
  border-collapse: collapse;
  width: 100%;
  font-size: 13px;
}
.md-table th, .md-table td {
  border: 1px solid var(--hair);
  padding: 6px 10px;
  text-align: left;
  vertical-align: top;
}
.md-table th {
  background: var(--surface2);
  font-size: 11px;
  font-weight: 620;
  white-space: nowrap;
}
.md-table tbody tr:nth-child(even) { background: var(--hair-2); }
.isl-522 { font-size: 10px; color: var(--muted); margin-top: 6px }
.isl-523 { white-space: pre-wrap }
.isl-524 { font-size: 11px; color: var(--muted); margin-bottom: 4px; text-transform: uppercase; letter-spacing: 0.5px }
.isl-525 { border-radius: 8px; padding: 10px 14px; word-wrap: break-word; background: var(--surface2); border: 1px solid var(--border) }
.isl-526 { font-size: 16px; padding: 2px 8px; line-height: 1 }
.isl-527 { max-width: 820px; width: 94vw; max-height: 88vh; display: flex; flex-direction: column }
.isl-528 { flex: 1; overflow: auto; padding: 16px 20px; font-size: 13px; line-height: 1.55 }
.isl-529 { padding: 10px 16px; border-top: 1px solid var(--border); display: flex; gap: 8px; flex-wrap: wrap; align-items: center }
/* v6.4.3: one named class for 'this table failed to load'. It replaces
   .isl-533 / .isl-597 / .isl-670, three anonymous classes with identical
   intent and three different paddings (30 / 20 / 24px), so the same message
   sat at a different height depending on which page you were on. 24px is
   the middle value and matches .empty-state-sm, its neutral counterpart. */
.empty-state-error { text-align: center; color: var(--red); padding: 24px; }
.isl-534 { text-align: center; color: var(--muted); padding: 30px }
/* .isl-535 — dynamic colour, applied via data-color attribute */
/* .isl-536 — dynamic colour, applied via data-color attribute */
.isl-537 { max-width: 900px; width: 96vw; max-height: 88vh; display: flex; flex-direction: column }
.isl-538 { flex: 1; overflow: auto; padding: 14px 18px; font-size: 13px }
.isl-539 { font-size: 11px; color: var(--muted); margin-bottom: 12px }
.isl-540 { width: 100%; font-size: 12px }
.isl-541 { font-size: 10px; padding: 2px 6px; margin-right: 4px }
.isl-542 { font-size: 10px; color: var(--muted); margin-right: 4px }
/* .isl-543 — dynamic colour, applied via data-color attribute */
.isl-544 { padding: 6px 4px; font-family: var(--font-mono); font-size: 11px }
.isl-545 { padding: 6px 4px; color: var(--muted) }
.isl-546 { padding: 6px 4px; text-align: right }
.isl-547 { opacity: 0.55 }
.isl-548 { padding: 0 4px 6px 16px; font-size: 10px; color: var(--muted) }
.isl-549 { padding: 0 4px 8px 16px }
.isl-550 { font-size: 11px; color: var(--muted); cursor: pointer }
.isl-551 { margin-top: 4px; font-family: var(--font-mono); font-size: 10px; color: var(--muted) }
.isl-552 { color: var(--muted); font-size: 16px }
.isl-553 { padding: 14px 8px }
.isl-554 { font-size: 12px; padding: 2px 8px }
.isl-555 { flex: 1; cursor: pointer }
.isl-556 { font-size: 13px; padding: 2px 8px; color: var(--muted) }
.isl-557 { display: flex; align-items: center; gap: 10px; padding: 5px 4px; border-bottom: 1px solid var(--border) }
.isl-558 { flex: 1; font-size: 13px; display: flex; align-items: center; gap: 6px }
.isl-559 { color: var(--text); text-decoration: none; font-weight: 600 }
.isl-560 { width: 80px; text-align: right; font-size: 11px; color: var(--muted) }
.isl-561 { max-width: 1100px; width: 96vw; max-height: 88vh; display: flex; flex-direction: column }
.isl-562 { font-family: var(--font-mono); font-size: 11px; color: var(--muted) }
.isl-563 { font-size: 12px; color: var(--muted); margin-bottom: 10px }
.isl-564 { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; font-size: 11px; color: var(--muted); margin-bottom: 10px }
.isl-572 { font-size: 11px; color: var(--muted); margin-top: 4px; font-family: var(--font-mono) }
.isl-573 { font-size: 11px; padding: 3px 10px; color: var(--amber); border-color: rgba(245,158,11,0.3) }
.isl-574 { color: var(--muted); font-family: var(--font-mono); font-size: 12px }
.isl-575 { font-size: 10px; color: var(--muted); background: var(--surface); padding: 1px 5px; border-radius: 3px; margin-left: 4px }
/* v5.1.1: Proxmox node badge — distinct from the muted tags badge (.isl-575): accent text + border. */
.isl-780 { font-size: 10px; color: var(--accent-ink); background: var(--surface); padding: 1px 5px; border-radius: 3px; margin-left: 4px; border: 1px solid var(--border) }
/* v5.1.1: Proxmox cluster-node summary line (chips use .isl-780). */
.isl-781 { font-size: 12px; color: var(--muted); margin-bottom: 8px; display: flex; flex-wrap: wrap; align-items: center; gap: 2px }
.isl-576 { font-size: 12px; font-weight: 600 }
.isl-577 { padding: 20px; color: var(--red) }
.isl-578 { padding: 20px; color: var(--muted) }
.isl-579 { padding: 14px }
.isl-580 { padding: 16px; color: var(--muted) }
.isl-581 { max-width: 620px }
.isl-582 { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px }
.isl-583 { display: flex; gap: 8px; margin-bottom: 14px; flex-wrap: wrap; align-items: flex-end }
.isl-584 { flex: 2; min-width: 160px; margin: 0 }
.isl-585 { color: var(--muted); padding: 12px }
.isl-586 { color: var(--red); padding: 12px }
.isl-587 { padding: 6px 4px; font-family: var(--font-mono) }
.isl-588 { font-size: 10px; color: var(--muted) }
.isl-589 { padding: 6px 4px; text-align: right; white-space: nowrap }
.isl-590 { font-size: 10px; padding: 2px 6px; margin-right: 4px; color: var(--amber); border-color: rgba(245,158,11,0.3) }
.isl-591 { font-size: 10px; padding: 2px 6px; color: var(--red); border-color: rgba(239,68,68,0.3) }
.isl-592 { text-decoration: none; margin-left: 4px; color: var(--text) }
.isl-593 { vertical-align: middle; opacity: 0.7 }
.isl-594 { font-size: 12px; font-weight: 600; color: var(--muted); margin-bottom: 6px }
.isl-595 { display: flex; justify-content: space-between; font-size: 12px; padding: 3px 0; border-bottom: 1px solid var(--border) }
.isl-596 { margin-top: 8px; display: flex; gap: 8px }
/* v6.4.3: opacity removed — double-dimming. These rows mark 'no results
   yet', and the cell inside them (.isl-599) is already var(--muted). The
   0.65 took it to 2.86:1. The muted cell colour IS the de-emphasis. */
.isl-598 { }
.isl-599 { color: var(--muted); font-style: italic }
.isl-600 { font-size: 11px; padding: 2px 8px; margin-right: 4px }
.isl-601 { font-size: 11px; padding: 2px 8px; color: var(--red); border-color: rgba(239,68,68,0.3) }
.isl-602 { font-size: 10px; color: var(--muted) }
.isl-603 { font-family: var(--font-mono); font-size: 11px; max-width: 260px; overflow: hidden; cursor: pointer }
.isl-604 { text-align: right; font-size: 11px; color: var(--muted) }
.isl-605 { display: flex; align-items: center; gap: 6px; padding: 4px 8px; border-radius: 6px; cursor: pointer; font-size: 12px; background: var(--surface); border: 1px solid var(--border) }
.isl-606 { width: 14px; height: 14px }
.isl-607 { position: absolute; top: 8px; right: 10px; background: none; border: none; color: var(--muted); cursor: pointer; font-size: 16px }
.isl-608 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 10px }
.isl-609 { font-size: 11px; color: var(--muted); display: block; margin-bottom: 4px }
.isl-610 { margin-bottom: 10px }
.isl-611 { font-family: var(--font-mono); font-size: 11px; min-height: 80px; resize: vertical }
.isl-612 { font-size: 12px; flex: 1 }
.isl-613 { font-size: 12px; width: 90px }
.isl-614 { background: none; border: none; color: var(--muted); cursor: pointer; font-size: 16px }
.isl-616 { color: var(--muted); font-size: 13px; padding: 8px 0 }
.isl-617 { display: flex; gap: 8px; align-items: center; padding: 6px 0; border-bottom: 1px solid var(--border) }
.isl-618 { flex: 1; font-size: 13px }
.isl-619 { flex: 1; padding: 5px 8px; font-size: 12px }
.isl-620 { width: 70px; padding: 5px 8px; font-size: 16px; text-align: center }
.isl-621 { width: 80px; padding: 5px 8px; font-size: 12px }
.isl-622 { align-items: flex-start }
.isl-623 { padding-top: 4px }
.isl-624 { flex: 1; padding: 5px 8px; font-size: 12px; font-family: var(--font-mono); resize: vertical }
.isl-625 { padding: 12px 0 4px; display: flex; gap: 8px }
.isl-626 { font-size: 12px; padding: 6px 16px }
.isl-627 { font-size: 12px; width: 100%; border-collapse: collapse }
.isl-628 { text-align: left; padding: 3px 6px }
.isl-629 { padding: 3px 6px }
.isl-630 { text-align: center }
.isl-631 { font-size: 11px; color: var(--muted); margin: 10px 0 4px }
.isl-632 { max-height: 200px }
.isl-633 { width: 100%; border-collapse: collapse }
.isl-634 { color: var(--muted); text-align: left }
/* .isl-635 — dynamic colour, applied via data-color attribute */
.isl-636 { max-height: 300px; overflow-y: auto }
.isl-637 { max-height: 320px; overflow-y: auto }
.isl-638 { flex: 1; font-size: 12px; color: var(--accent-ink); overflow: hidden; text-overflow: ellipsis }
.isl-639 { font-size: 10px; color: var(--muted); white-space: nowrap; margin-left: 8px }
.isl-640 { font-size: 11px; color: var(--muted); margin-left: 6px }
.isl-641 { font-size: 11px; margin-top: 6px }
.isl-642 { display: flex; align-items: flex-start; gap: 8px; padding: 4px 0; border-bottom: 1px solid var(--border) }
.isl-643 { font-size: 12px; padding: 4px 0; border-bottom: 1px solid var(--border) }
/* .isl-644 — dynamic colour, applied via data-color attribute */
/* .isl-645 — dynamic colour, applied via data-color attribute */
.isl-646 { font-size: 11px; font-weight: 600; margin-bottom: 3px; text-transform: uppercase }
.isl-647 { font-size: 11px; padding: 2px 0; border-bottom: 1px solid var(--border) }
/* .isl-648 — dynamic colour, applied via data-color attribute */
.isl-649 { font-size: 12px; width: 100%; border-collapse: collapse }
.isl-650 { text-align: left; padding: 4px 6px }
.isl-651 { padding: 4px 6px }
.isl-652 { text-align: center }
.isl-653 { text-align: center; color: var(--muted); font-size: 13px }
.isl-654 { font-size: 11px; overflow: auto; white-space: pre-wrap; max-height: 300px }
/* v3.13.0: drawer/page table cap — ~15 rows then scroll internally. Pairs
 * with .scrollable-table-wrap (sticky header + styled scrollbar); declared
 * after it so this tighter max-height wins for in-drawer tables. */
/* ── v5.6.x responsiveness/smoothness sweep ─────────────────────────────────
   (a) Page-entrance micro-transition: a 140ms compositor-only fade+rise on
       the page becoming .active. Opacity/transform only (no layout work),
       and overlays/modals live at body level so the transient transform's
       containing-block effect can't touch them. Motion-sensitive users get
       an instant swap.
   (b) Overscroll containment on every internal scroller: reaching the end of
       a capped table/list/drawer/modal no longer chain-scrolls the page
       behind it — the single biggest "scrolling feels janky" cause here.
   (c) Stable scrollbar gutter on the document: switching between a short
       page and a long one no longer shifts the whole layout by the
       scrollbar's width (classic-scrollbar platforms). */
@media (prefers-reduced-motion: no-preference) {
  .page.active { animation: rp-page-in 140ms ease-out; }
  @keyframes rp-page-in {
    from { opacity: 0; transform: translateY(4px); }
    to   { opacity: 1; transform: none; }
  }
}
.scrollable-table-wrap, .audit-scroll, .scroll-cap, .scroll-cap-sm,
.scroll-cap-lg, .device-drawer .drawer-panel, .modal-overlay, .sidebar {
  overscroll-behavior: contain;
}
html { scrollbar-gutter: stable; }

.audit-scroll { max-height: 360px; overflow: auto }
/* v6.4.3: the readiness table's per-row trailing actions ("Manage inbound
   tokens", "Mute"). Sits inside the .hint detail cell, so it needs its own
   gap rather than inheriting the sentence's word spacing. */
.self-row-acts { display: inline-flex; gap: 6px; margin-left: 8px; white-space: nowrap }
/* v3.13.0: shared content cap — any non-table list/box that renders a
 * variable number of rows caps at ~15 lines and scrolls internally
 * instead of growing unbounded. `-sm` for chip rows / short lists. */
.scroll-cap    { max-height: 340px; overflow-y: auto; }
.scroll-cap-sm { max-height: 132px; overflow-y: auto; }
/* Taller cap for prose reading panels (KB article view, long doc bodies) — still
   bounded so a huge article never grows the page unbounded, but readable. */
.scroll-cap-lg { max-height: 62vh; overflow-y: auto; }

/* v7.0.2: ACCUMULATOR panels — a container that appends one section, card or
 * editor row per item. Each of these reads as capped because the thing INSIDE
 * every section is capped (a 340px rule list, a 360px table wrap, a
 * scroll-cap-sm evidence list); it is the outer stack that had nothing, so the
 * height is (number of items) x (capped section), which grows without limit.
 *
 * Measured worst cases from the producers, not guessed:
 *   #fail2ban-detail   the agent sends up to 50 jails x 200 banned IPs; an
 *                      ordinary internet-facing box with 10 jails is ~3,700px
 *   #firewall-detail   one section per present backend (nftables + iptables +
 *                      ufw is ~1,200px)
 *   #adv-findings      groups by finding id, and application findings use the
 *                      SCANNER's rule id — a nuclei/nikto-scanned fleet yields
 *                      50-200 distinct ids, 7,500-30,000px of stacked cards
 *   #sla-targets-editor  one row per group + tag + DEVICE target; 100 per-device
 *                      targets is ~4,000px of form controls with Save at the end
 *   #wp-login-panels   one card per configured instance; 20 sites is ~9,000px
 *   #dns-block-panels  same shape
 *
 * All six are behind a click or start hidden, which is why the rendered
 * box-overflow gate never saw them: it walks pages and never presses a button. */
#fail2ban-detail,
#firewall-detail,
#adv-findings,
#sla-targets-editor,
#wp-login-panels,
#dns-block-panels { max-height: 70vh; overflow-y: auto; }

/* Don't nest a 340px scroller inside the 70vh cap above — two scrollbars on one
 * panel and the inner one swallows the wheel. Same reasoning as the
 * #page-thermal .thermal-detail-row rule. */
#fail2ban-detail .scroll-cap,
#firewall-detail .scroll-cap { max-height: 132px; }

/* A chip row grows one chip per distinct value. #timeline-kinds is one per
 * event kind present in the window, so it is bounded by the registry rather
 * than by fleet size — but it wraps, and on a busy fleet it wrapped to several
 * rows and pushed the timeline itself below the fold. */
#timeline-kinds { max-height: 132px; overflow-y: auto; }

/* v5.6.x: inline status icon for the Server-status "Serving & runtime" table. */
.rt-ico { display: inline-flex; vertical-align: -2px; margin-right: 2px; }

/* v5.6.x perf: a capped scroll container still LAYS OUT every child even though
   only ~one viewport is visible. A host with thousands of firewall rules, or the
   full App Catalog, ballooned the render tree to 17–20k layout objects and cost
   ~180 ms to relay out on EVERY navigation to the page (measured in a DevTools
   trace: LocalFrameView::performLayout + UpdateLayoutTree dominated the two
   worst long tasks, one per page). content-visibility:auto lets the browser skip
   layout + paint of the off-screen children; contain-intrinsic-size gives a
   per-child size estimate so the scrollbar stays stable, and the `auto` keyword
   makes Chrome remember each child's real size once it has actually rendered.
   Scoped to these two heavy DIV-based lists — NOT applied to
   <table> rows, where skipping off-screen rows would jitter the auto-sized
   column widths as you scroll. Children are block-level (a `.fw-rule-row` is a
   flex container but sits in block flow), so containment is side-effect-free. */
.fw-rules > *     { content-visibility: auto; contain-intrinsic-size: auto 30px; }
#catalog-grid > * { content-visibility: auto; contain-intrinsic-size: auto 150px; }

/* v5.6.0: Provisioning blueprints folder tree */
.bp-folder { display: flex; align-items: center; gap: 6px; font-size: 11px;
  text-transform: uppercase; letter-spacing: .04em; color: var(--muted);
  margin: 12px 0 4px; }
.bp-folder:first-child { margin-top: 0; }
.bp-item { display: flex; align-items: center; gap: 10px; padding: 6px 8px 6px 22px;
  border-bottom: 1px solid var(--border); }
.bp-item:last-child { border-bottom: 0; }
.bp-item-name { display: flex; align-items: center; gap: 6px; font-size: 13px;
  font-weight: 500; flex: 1; min-width: 0; }
.bp-item-name svg { flex: 0 0 auto; }
.bp-kind { font-size: 11px; color: var(--muted); font-family: var(--font-mono);
  padding: 1px 6px; border: 1px solid var(--border); border-radius: 4px; }
.bp-item-actions { display: flex; gap: 4px; flex: 0 0 auto; }

/* v5.6.0: timesheet "Watch for" + watch-grant typeahead results */
.ts-watch-results { border: 1px solid var(--border); border-radius: 6px; margin-top: 4px; }
.ts-watch-results > div:hover { background: var(--hover, rgba(127,127,127,.12)); }

/* v4.10.0: Firewall / fail2ban detail panels — rules grouped by chain, banned
   IPs with an inline action. */
.fw-rules { padding: 0 2px; }
.fw-chain-hdr { margin: 10px 0 2px; font-weight: 600; font-size: 12px; color: var(--muted); }
.fw-rule-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 3px 0; border-bottom: 1px solid var(--border); }
.fw-ban-row  { display: flex; align-items: center; gap: 12px; padding: 3px 0; border-bottom: 1px solid var(--border); }
.fw-rule-row code, .fw-ban-row code { font-size: 12px; word-break: break-all; }
.fw-jail-hdr { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
.fw-rule-row > button, .fw-ban-row > button { flex-shrink: 0; }
.fw-detail-hdr { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.fw-detail-actions { display: flex; gap: 6px; flex-shrink: 0; }
.fw-pending { opacity: .55; }
.fw-pending code { text-decoration: line-through; }
.fw-queued-note { font-size: 12px; color: var(--green); margin: 6px 0 2px; }
/* selected-row highlight (firewall/fail2ban detail) */
tr.row-selected td { background: rgba(127, 127, 127, .10); }
tr.row-selected td:first-child { box-shadow: inset 3px 0 0 var(--accent); }

/* v4.10.0: AI Insights hub — one-click report/advisor cards. */
.ai-insight-cat { margin: 14px 0 6px; }
.ai-insights-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 10px; margin: 0 0 10px; }
.ai-insight-card { text-align: left; padding: 10px 12px; border: 1px solid var(--border); border-radius: 6px; background: transparent; cursor: pointer; color: inherit; width: 100%; }
.ai-insight-card:hover { border-color: var(--accent); }
/* v5.6.0: each card + its small "debug" companion button (top-right corner). */
.ai-insight-wrap { position: relative; display: flex; }
.ai-insight-dbg { position: absolute; top: 5px; right: 5px; font-size: 10px; line-height: 1;
  padding: 2px 6px; border: 1px solid var(--border); border-radius: 4px; background: var(--bg);
  color: var(--muted); cursor: pointer; opacity: .5; }
.ai-insight-wrap:hover .ai-insight-dbg { opacity: 1; }
.ai-insight-dbg:hover { border-color: var(--accent); color: var(--accent-ink); }
/* Debug panel (what-was-sent) */
.ai-dbg-note { padding: 8px 10px; border-radius: 6px; margin-bottom: 10px; font-size: 13px; }
.ai-dbg-ok { background: rgba(60,180,90,.10); border: 1px solid rgba(60,180,90,.35); }
.ai-dbg-warn { background: rgba(210,150,40,.10); border: 1px solid rgba(210,150,40,.40); }
.ai-dbg-tbl { width: 100%; border-collapse: collapse; font-size: 13px; margin-bottom: 10px; }
.ai-dbg-tbl td { padding: 4px 8px; border-bottom: 1px solid var(--border); }
.ai-dbg-tbl td:first-child { color: var(--muted); width: 55%; }
.ai-dbg-h { font-size: 12px; color: var(--muted); margin: 12px 0 4px; }
.ai-dbg-pre { white-space: pre-wrap; word-break: break-word; font-size: 12px; background: var(--bg);
  border: 1px solid var(--border); border-radius: 6px; padding: 8px 10px; }
.ai-insight-top { display: flex; align-items: center; gap: 7px; margin-bottom: 3px; }
.ai-insight-icon { color: var(--accent-ink); flex-shrink: 0; }
.ai-insight-label { font-size: 13px; font-weight: 600; }
.ai-insight-desc { font-size: 12px; color: var(--muted); }
/* v3.13.0: drift profiles panel */
.drift-prof-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
.drift-prof-item { padding: 10px 0; border-bottom: 1px solid var(--border); }
.drift-prof-item:last-child { border-bottom: none; }
.drift-prof-row { display: flex; justify-content: space-between; align-items: center; gap: 8px; }
.drift-prof-actions { display: flex; gap: 6px; flex-shrink: 0; }
.drift-prof-files { margin-top: 4px; font-size: 11px; word-break: break-all; }
.drift-prof-assigns { margin-top: 6px; display: flex; flex-wrap: wrap; gap: 6px; max-height: 132px; overflow-y: auto; }
.tag-x { background: none; border: none; color: inherit; cursor: pointer; margin-left: 4px; padding: 0 2px; font-size: 13px; opacity: 0.7; }
.tag-x:hover { opacity: 1; }
.isl-655 { display: flex; align-items: center; gap: 8px; padding: 4px 0; border-bottom: 1px solid var(--border) }
.isl-656 { flex: 1; font-size: 12px }
.isl-657 { color: var(--red); font-size: 13px }
.isl-658 { font-size: 11px; color: var(--muted); margin-bottom: 10px }
.isl-659 { border: 1px solid var(--border); border-radius: 8px; padding: 12px; background: var(--surface2) }
.isl-660 { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; gap: 10px; flex-wrap: wrap }
.isl-661 { font-size: 11px; color: var(--amber); margin-left: 8px }
.isl-662 { font-size: 11px; color: var(--accent2-ink); margin-left: 8px }
.isl-663 { width: 100%; min-height: 90px; font-family: var(--font-mono); font-size: 12px; resize: vertical }
.isl-664 { cursor: pointer; font-size: 12px; color: var(--muted); user-select: none }
.isl-665 { display: grid; grid-template-columns: repeat(auto-fit,minmax(140px,1fr)); gap: 10px; margin-top: 8px; padding: 10px; background: var(--surface); border-radius: 6px }
.isl-666 { font-size: 11px; color: var(--muted); display: block; margin-bottom: 2px }
.isl-667 { display: flex; gap: 6px; margin-top: 6px; justify-content: flex-end }
.isl-668 { margin: 0 0 8px 0 }
.isl-669 { display: flex; align-items: center; gap: 10px; padding: 6px 10px; border: 1px solid var(--border); border-radius: 6px; margin-bottom: 4px; background: var(--surface2) }
.isl-671 { opacity: .7 }
.isl-672 { font-size: 12px; color: var(--muted); font-style: italic }
.isl-673 { opacity: .85 }
.isl-674 { font-size: 12px; font-weight: 500 }
.isl-676 { color: var(--red); padding: 20px; text-align: center }
.isl-677 { display: grid; grid-template-columns: repeat(auto-fit,minmax(180px,1fr)); gap: 12px; margin-bottom: 18px }
.isl-678 { color: #9a8dff }
.isl-679 { font-weight: 500; font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: .5px; margin-bottom: 6px }
.isl-680 { display: flex; flex-wrap: wrap; gap: 6px }
.isl-681 { background: var(--surface2); padding: 3px 8px; border-radius: 4px; font-size: 12px }
.isl-682 { margin-bottom: 14px; font-size: 12px; color: var(--muted); font-style: italic }
.isl-683 { margin-bottom: 14px; font-size: 12px; color: var(--muted) }
.isl-684 { font-size: 11px; padding: 2px 8px; margin-top: 4px }
.isl-685 { flex: 1; text-align: left; padding: 8px 12px; display: flex; justify-content: space-between; align-items: center }
.isl-686 { color: var(--muted); font-size: 11px; margin-left: 8px }
.isl-687 { font-size: 11px; padding: 6px 10px; color: var(--red) }
.isl-688 { font-size: 11px; padding: 6px 10px; color: var(--muted) }
.isl-689 { background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; padding: 10px }
.isl-691 { color: var(--red); padding: 14px }
.isl-692 { font-size: 11px; color: var(--muted); margin-bottom: 4px }
.isl-693 { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 12px; font-size: 11px; max-height: 400px; overflow: auto; white-space: pre-wrap; word-break: break-all }
.isl-694 { background: rgba(220,60,60,0.12); color: var(--red); border: 1px solid rgba(220,60,60,0.30); border-radius: 6px; padding: 10px }
.isl-695 { background: rgba(255,170,40,0.10); color: var(--amber); border: 1px solid rgba(255,170,40,0.30); border-radius: 6px; padding: 10px }
.isl-696 { margin-top: 4px; font-size: 12px }
.isl-697 { display: flex; align-items: flex-start; gap: 10px; background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; padding: 10px; cursor: pointer }
.isl-698 { margin-top: 3px }
.isl-699 { font-weight: 500; font-size: 12px }
.isl-700 { color: var(--amber); font-size: 11px; margin-left: 6px }
.isl-701 { font-size: 11px; color: var(--muted); display: block; margin-top: 4px; word-break: break-all }
.isl-702 { color: var(--muted); font-size: 13px; text-align: center; padding: 20px }
.isl-703 { background: rgba(220,60,60,0.12); color: var(--red); border: 1px solid rgba(220,60,60,0.30); border-radius: 6px; padding: 10px; font-size: 12px }
.isl-704 { background: rgba(255,170,40,0.10); color: var(--amber); border: 1px solid rgba(255,170,40,0.30); border-radius: 6px; padding: 10px; font-size: 12px }
.isl-705 { background: var(--surface2); color: var(--muted); border: 1px solid var(--border); border-radius: 6px; padding: 8px; font-size: 12px }
.isl-706 { color: var(--red); font-size: 12px }
.isl-707 { margin-top: 4px; font-size: 11px; color: var(--muted) }
.isl-708 { background: rgba(0,200,120,0.10); color: var(--green); border: 1px solid rgba(0,200,120,0.30); border-radius: 6px; padding: 10px; font-size: 12px }
.isl-709 { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 10px; font-size: 11px; margin-top: 8px; max-height: 280px; overflow: auto; white-space: pre-wrap }
.isl-710 { margin-top: 8px; font-size: 11px; color: var(--muted) }
.isl-711 { margin-top: 8px; font-size: 11px; color: var(--amber) }
.isl-712 { display: flex; justify-content: space-between; font-size: 12px; padding: 3px 0 }
.isl-713 { font-weight: 600 }
.isl-714 { font-size: 13px; margin-bottom: 10px }
/* v6.0.1 (item 2): Server-status key/value cards. Each card is a separate
   full-width table, so auto-layout gave every card a different label-column
   width and the values never lined up down the page. Pin the label column to a
   fixed width (values start at the same x in every card) and give both cells the
   same compact rhythm. Scoped to the 2-column info tables (.fs-13 / .isl-714) —
   the 3-column "Slow requests" table has no such class, so it's untouched. */
#self-status-body table.fs-13,
#self-status-body table.isl-714 { table-layout: fixed; }
#self-status-body table.fs-13 td,
#self-status-body table.isl-714 td {
  padding: 4px 12px 4px 0;
  border-bottom: none;
  vertical-align: top;
  overflow-wrap: anywhere;
}
#self-status-body table.fs-13 td:first-child,
#self-status-body table.isl-714 td:first-child {
  width: 210px;
  color: var(--muted);
}
.isl-715 { cursor: pointer; color: var(--muted); font-size: 12px }
.isl-716 { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; width: min(580px,90vw); max-height: 60vh; display: flex; flex-direction: column; box-shadow: var(--shadow-3) }
.isl-717 { width: 100%; padding: 14px 18px; background: transparent; border: none; border-bottom: 1px solid var(--border); color: var(--text); font-size: 13px; outline: none }
.isl-718 { overflow-y: auto; padding: 6px; flex: 1 }
.isl-719 { font-size: 11px; color: var(--muted); padding: 8px 12px; border-top: 1px solid var(--border); display: flex; gap: 14px }
.isl-720 { background: var(--bg); padding: 1px 5px; border-radius: 3px; border: 1px solid var(--border) }
.isl-721 { padding: 10px 12px; border-radius: 6px; cursor: pointer; display: flex; justify-content: space-between; align-items: center; }
/* v3.0.5+: highlight the row at the keyboard cursor in the command palette.
   The JS template already adds `.cmd-palette-active` for that row; the
   auto-class generator left a `${idx === _palCursor ? …}` interpolation
   in the .isl-721 rule that the browser silently dropped. */
.isl-721.cmd-palette-active { background: var(--surface2); }
.isl-722 { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: .5px }
.isl-723 { padding: 20px; text-align: center; color: var(--muted); font-size: 13px }
.isl-724 { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 24px 28px; max-width: 460px; width: 90vw; box-shadow: var(--shadow-3) }
.isl-725 { display: flex; justify-content: space-between; align-items: center; margin-bottom: 18px }
.isl-726 { margin: 0; font-size: 16px }
.isl-727 { font-size: 16px; padding: 4px 10px }
.isl-728 { width: 100%; font-size: 13px }
.isl-729 { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 24px 28px; max-width: 640px; width: 92vw; max-height: 80vh; overflow-y: auto; box-shadow: var(--shadow-3) }
.isl-730 { font-size: 13px; color: var(--muted); margin-bottom: 16px }
.isl-731 { display: flex; align-items: center; gap: 6px; cursor: pointer; font-size: 13px }
.isl-732 { width: 100%; padding: 8px }
.isl-733 { font-size: 12px; color: var(--muted); padding: 10px; background: var(--bg); border-radius: 6px; margin-bottom: 14px }
.isl-734 { margin-top: 6px; font-size: 11px; line-height: 1.7 }
.isl-735 { font-size: 13px; color: var(--muted); padding: 14px; background: var(--surface); border-radius: 8px; border: 1px dashed var(--border) }
.isl-736 { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 14px }
.isl-737 { display: flex; gap: 10px; margin-bottom: 10px; align-items: center }
.isl-738 { flex: 1; padding: 6px 10px; font-size: 13px }
.isl-739 { padding: 4px 10px; font-size: 11px }
.isl-740 { padding: 4px 10px; color: var(--red); font-size: 13px }
.isl-741 { display: flex; gap: 10px; margin-bottom: 10px }
.isl-742 { flex: 0 0 180px; padding: 6px 10px; font-size: 13px }
.isl-743 { flex: 1; padding: 6px 10px; font-size: 13px; font-family: var(--font-mono) }
.isl-744 { cursor: pointer; color: var(--muted) }
.isl-745 { margin-top: 8px; display: flex; gap: 10px; align-items: center; flex-wrap: wrap }
.isl-746 { display: flex; align-items: center; gap: 6px }
.isl-747 { padding: 4px 8px; font-size: 12px }
.isl-748 { width: 100%; padding: 6px 10px; font-size: 12px; font-family: var(--font-mono); resize: vertical }
.icon-inline-flex { display: inline-flex; vertical-align: middle; line-height: 0; }
.input-url-dest { flex: 1; padding: 6px 10px; font-size: 13px; }

/* CSP L1 — variant classes for the rules that used JS expressions */
.isl-307.sel { background: var(--accent); color: var(--accent-contrast, #fff) }
.isl-311.active { background: rgba(59,126,255,0.2); color: var(--accent-ink); border-color: var(--accent) }
.isl-314.is-selected { border-color: var(--accent); box-shadow: 0 0 0 2px rgba(59,126,255,0.2) }
/* v3.4.0 Forecast page */
.forecast-svg { width: 100%; height: auto; display: block; background: var(--surface); border: 1px solid var(--border); border-radius: 8px; }
tr.is-selected > td { background: rgba(59,126,255,0.10); }
.isl-329.ok { background: var(--green) }
.isl-329.bad { background: var(--red) }
.isl-390.is-ignored { opacity: 0.5 }
.isl-496.edit-mode { cursor: default }
/* v3.0.5: hover effect was inline `onmouseover`/`onmouseout` pre-CSP. */
.isl-496:hover { background: var(--surface); }
.isl-496.edit-mode:hover { background: var(--surface2); }
.isl-699.is-pre { color: var(--green) }
/* Anything with data-color is coloured at runtime by JS that walks the
   tree and applies element.style.color via the CSP-safe IDL setter
   (see _applyDataColors in app.js). Same for data-bg / data-bd. */

/* v3.2.0 (B2): Inbound webhook show-once modal — code blocks */
.inbound-wh-url-pre {
  white-space: pre-wrap;
  word-break: break-all;
  padding: 12px;
  margin: 12px 0;
}
.inbound-wh-example-pre {
  padding: 12px;
  margin: 12px 0;
  white-space: pre;
}

/* v3.2.0 (B3): OIDC sign-in button on the login page */
.login-btn-oidc {
  display: block;
  background: transparent;
  border: 1px solid var(--accent);
  color: var(--accent-ink);
  text-decoration: none;
  text-align: center;
  margin-top: 8px;
}

/* v3.2.0 (B1) → v6: sidebar item count (chosen-design .pg .c) — bare mono
   digits, coloured by severity; no pill chrome. */
.nav-badge {
  display: inline-block;
  margin-left: auto;
  background: none;
  color: var(--muted);
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 10px;
  font-weight: 400;
  padding: 0;
  text-align: right;
}
.nav-badge.nav-badge-ok    { color: var(--muted); }
.nav-badge.nav-badge-alert { color: var(--red); }
.nav-badge.nav-badge-info  { color: var(--accent-ink); }
/* v5.0.0 fix: `.nav-badge` sets display:inline-block and is defined AFTER the
   global `.d-none` (equal specificity → later rule wins), so a hidden zero-count
   badge still painted as an empty red pill (a stray "red dash" next to e.g.
   Status Board / Command Queue). This more-specific rule restores hiding. */
.nav-badge.d-none { display: none; }

/* v3.2.0 (B5): SNMP status pill on device cards */
.snmp-pill {
  display: inline-block;
  margin-left: 6px;
  padding: 1px 6px;
  font-size: 11px;
  border-radius: 4px;
  vertical-align: middle;
}
.snmp-pill.snmp-ok   { background: rgba(40,167,69,0.15); color: #28a745; border: 1px solid #28a745; }
.snmp-pill.snmp-fail { background: rgba(220,53,69,0.15); color: var(--red); border: 1px solid var(--red); }
/* v3.4.0: hardware-health pills on device cards (mirror snmp-pill) */
.hw-pill {
  display: inline-block; margin-left: 6px; padding: 1px 6px;
  font-size: 11px; border-radius: 4px; vertical-align: middle; cursor: pointer;
}
.hw-pill.hw-fail { background: rgba(220,53,69,0.15); color: var(--red); border: 1px solid var(--red); }
.hw-pill.hw-warn { background: rgba(245,158,11,0.15); color: var(--amber); border: 1px solid var(--amber); }

/* v3.2.0: Server Status performance bar.
   Width is set by JS via element.style.width = data-pct + '%' (the CSP-safe
   IDL setter — same pattern as data-color in app.js). */
.perf-bar {
  display: inline-block;
  width: 120px;
  height: 8px;
  background: var(--surface2);
  border-radius: 4px;
  overflow: hidden;
  vertical-align: middle;
  margin-left: 6px;
}
.perf-bar-fill {
  height: 100%;
  background: var(--accent);
  transition: width 0.3s ease;
}
.sev-pill {
  display: inline-block;
  padding: 2px 8px;
  border-radius: 10px;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}
/* v6.4.3 (a11y): var(--red) as TEXT on its own tint composites to 4.32:1 —
   under AA. Same shape as .patch-badge.crit above, same remedy: a dedicated
   ink colour for the label while the BORDER keeps the fill colour. The
   light-theme override follows, because its --red fails on a light tint too
   (3.74:1) — a latent failure the a11y suite cannot see, since it audits the
   default theme only. */
.sev-pill.sev-critical { background: rgba(220,53,69,0.15); color: #ec7a70; border: 1px solid var(--red); }
body.light .sev-pill.sev-critical { color: #a4232b; }
.sev-pill.sev-high     { background: rgba(255,140,0,0.15); color: #ff8c00; border: 1px solid #ff8c00; }
.sev-pill.sev-medium   { background: rgba(255,193,7,0.15); color: #d4a017; border: 1px solid #d4a017; }
.sev-pill.sev-low      { background: rgba(108,117,125,0.15); color: #8a95a1; border: 1px solid var(--muted); }
/* v6.3.0 (a11y): #28a745 sat at 4.48:1 on its own tinted background — 0.02
   under the WCAG AA bar (axe serious on compliance/reports/self). Same
   dedicated-color treatment sev-low got in the #62 pass. */
.sev-pill.sev-success  { background: rgba(40,167,69,0.15); color: #32c455; border: 1px solid #32c455; }
.sev-pill.sev-info     { background: rgba(59,130,246,0.15); color: var(--accent-ink); border: 1px solid var(--accent); }
.tl-dot.sev-info       { background: var(--accent); }
.alerts-row.resolved   { opacity: 0.55; }
/* v7.1.0: alert evidence key: value — the pair stays together, a long value
   (a path, a URL) breaks inside itself rather than widening the table. */
.alert-kv { white-space: nowrap; }
.alert-kv strong { white-space: normal; overflow-wrap: anywhere; }
/* v6.2.2: keyboard-inbox selection (j/k in app-alerts.js) */
.alerts-row.kb-selected td { background: var(--accent-soft); }
.alerts-row.kb-selected td:first-child { box-shadow: inset 2px 0 0 var(--accent); }
/* v6.2.2: device hover card (body-level, above the drawer/sidebar) */
#dev-hovercard {
  position: fixed; z-index: 220; max-width: 280px; padding: 10px 12px;
  background: var(--surface); border: 1px solid var(--hair);
  border-radius: 11px; box-shadow: var(--shadow-2);
  font-size: 13px; pointer-events: none;
}
#dev-hovercard .dev-hovercard-title { font-weight: 620; font-size: 13px; margin-bottom: 6px; display: flex; align-items: center; gap: 6px; }
#dev-hovercard .dev-hovercard-dot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; }
#dev-hovercard .dev-hovercard-dot.online  { background: var(--green); }
#dev-hovercard .dev-hovercard-dot.offline { background: var(--red); }
#dev-hovercard .dev-hovercard-row { display: flex; gap: 8px; margin: 2px 0; }
#dev-hovercard .dev-hovercard-label { color: var(--muted); min-width: 46px; font-size: 12px; }
#dev-hovercard .dev-hovercard-hint { color: var(--muted); font-size: 12px; margin-top: 6px; }
/* v4.1.0: per-host alert grouping + root-cause folding */
.alerts-group-row { background: var(--panel-2, rgba(127,127,127,0.06)); }
.alerts-group-row td { border-top: 2px solid var(--border); }
.alerts-group-toggle { background: none; border: none; color: var(--text); cursor: pointer;
                       font-size: 13px; padding: 2px 4px;
                       /* v6.4.3 (SPACING): was neither flex nor gapped, so the
                          chevron sat 3.6px from the hostname — whatever the
                          markup whitespace happened to collapse to — while every
                          other icon+label control in the product is a flex row
                          at 5px. */
                       display: inline-flex; align-items: center; gap: 5px; }
.alerts-group-toggle:hover { color: var(--accent, #4a9eff); }
.alert-rc-badge { display: inline-block; margin-left: 6px; padding: 1px 6px; border-radius: 4px;
                  font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.4px; }
.alert-rc-badge.rc-root { background: rgba(220,53,69,0.15); color: var(--red); border: 1px solid var(--red); }
/* v6.4.3: --muted-on-tint, not --muted. The badge paints its OWN 15% grey
   wash and then chose a colour picked against the un-tinted surface, so it
   lightened its backdrop out from under itself: 4.86:1 on --surface became
   4.2:1 on the wash, under the 4.5 AA floor at this 10px size. The token
   already existed for exactly this shape and had one user. Surfaced only when
   the seeded a11y pass finally had symptom-tagged alerts to render — an
   empty-database sweep can never see a badge that needs correlated rows.
   (Border keeps --muted: a 1px edge is a non-text element, floor 3:1.) */
.alert-rc-badge.rc-symptom { background: rgba(108,117,125,0.15); color: var(--muted-on-tint); border: 1px solid var(--muted); }
/* v6.4.2: this alert is downstream of a declared upstream that is offline.
   Amber, not red: the host really is down, but fixing IT is not the job — the
   upstream is. Distinct from rc-root (fix this) and rc-symptom (ignore this). */
.alert-rc-badge.rc-collateral { background: rgba(255,193,7,0.15); color: var(--amber); border: 1px solid var(--amber); }
.alerts-row.alert-symptom .alert-symptom-cell { padding-left: 22px; border-left: 2px solid var(--border); }
/* v4.1.0: resizable dashboard grid (size = column span on a 6-col grid) */
.dash-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 22px;
             align-items: start; }
/* grid gap owns the spacing — reset BOTH the .dash-card widgets and the one
   .dash-cols widget (home-overview); its margin-bottom otherwise stacked on top
   of the grid gap, leaving an oversized space under "Needs attention / Recent
   activity" vs every other widget. */
.dash-grid > .dash-card,
.dash-grid > .dash-cols { margin: 0; }
.dash-w-sm { grid-column: span 2; }
.dash-w-md { grid-column: span 3; }
.dash-w-lg { grid-column: span 6; }
@media (max-width: 1100px) {
  .dash-w-sm { grid-column: span 3; }
  .dash-w-md { grid-column: span 6; }
}
@media (max-width: 700px) {
  .dash-grid { grid-template-columns: 1fr; }
  .dash-w-sm, .dash-w-md, .dash-w-lg { grid-column: span 1; }
}
.dash-mini { font-size: 13px; }
.dash-big { font-size: 28px; font-weight: 700; line-height: 1.1; font-variant-numeric: tabular-nums; }
.dash-mini-head { font-size: 12px; color: var(--muted); margin-bottom: 6px; }
.dash-mini-row { display: flex; justify-content: space-between; gap: 8px; padding: 3px 0;
                 border-bottom: 1px solid var(--border); }
.dash-mini-row:last-child { border-bottom: none; }
.dash-mini-row .dm-l { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dash-mini-row .dm-r { flex: none; font-variant-numeric: tabular-nums; }
/* customize panel: size buttons + add-widget catalog */
.dash-edit-actions { display: flex; gap: 6px; margin: 8px 0 12px; flex-wrap: wrap; }
.dash-edit-row { display: flex; align-items: center; gap: 8px; }
.dash-edit-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis;
                  white-space: nowrap; }
.dash-edit-sizes { display: inline-flex; gap: 2px; }
.btn-icon.dash-size { padding: 1px 6px; font-size: 10px; font-weight: 700; opacity: 0.55; }
.btn-icon.dash-size.on { opacity: 1; border-color: var(--accent, #4a9eff);
                         color: var(--accent, #4a9eff); }
/* v4.1.0: Upcoming + Tickets dashboard cards */
.up-row, .tk-row { display: flex; align-items: center; gap: 8px; padding: 6px 0;
                   border-bottom: 1px solid var(--border); }
.up-row:last-child, .tk-row:last-child { border-bottom: none; }
.up-title, .tk-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis;
                       white-space: nowrap; font-size: 13px; }
.up-kind { display: inline-block; padding: 1px 7px; border-radius: 4px; font-size: 10px;
           font-weight: 700; text-transform: uppercase; letter-spacing: 0.4px;
           border: 1px solid var(--border); color: var(--muted); }
.up-kind.up-maintenance { color: #d4a017; border-color: #d4a017; }
.up-kind.up-job { color: #4a9eff; border-color: #4a9eff; }
/* v4.1.0: CheckMK-style check status pills */
.chk-pill { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px;
            font-weight: 600; text-transform: uppercase; letter-spacing: 0.4px; }
.chk-pill.chk-ok       { background: rgba(34,197,94,0.15);  color: var(--green); border: 1px solid var(--green); }
.chk-pill.chk-warning  { background: rgba(245,158,11,0.15); color: var(--amber); border: 1px solid var(--amber); }
.chk-pill.chk-critical { background: rgba(239,68,68,0.15);  color: var(--red-on-tint);   border: 1px solid var(--red); }
.chk-pill.chk-unknown  { background: rgba(100,116,139,0.15); color: var(--muted-on-tint); border: 1px solid var(--muted); }
.chk-row.disabled td   { opacity: 0.5; }
.alerts-summary-pill   { display: inline-block; padding: 4px 12px; background: var(--surface); border: 1px solid var(--border); border-radius: 6px; margin-right: 8px; font-size: 13px; }

/* ════════════════════════════════════════════════════════════════════
 * v3.3.0 mobile polish
 *
 * Touch targets, full-screen modals, larger drawer actions, and
 * better scrollable areas for phones. Apple HIG recommends 44pt
 * minimum; Material recommends 48dp. We aim for ~44px on all
 * interactive elements at ≤720px viewport.
 * ──────────────────────────────────────────────────────────────────── */
@media (max-width: 720px) {
  /* Bigger tap targets for icon buttons (table actions, drawer
   * chips, settings buttons). Desktop padding 8/12 → mobile 10/14
   * lands ~40px tall + ~46px wide. */
  .btn-icon {
    padding: 10px 14px;
    font-size: 13px;
    min-height: 40px;
  }
  /* Inside table cells the .btn-icon override at line 2374 shrinks
   * back to compact — explicitly preserve mobile size. */
  td .btn-icon, td button.btn-icon {
    padding: 10px 12px;
    min-height: 40px;
    font-size: 13px;
  }
  .btn-icon svg { width: 16px; height: 16px; }

  /* Primary/secondary buttons get a mobile-friendly tap height. */
  .btn-primary, .btn-secondary {
    padding: 12px 18px;
    min-height: 44px;
    font-size: 13px;
  }

  /* Form inputs at iOS 16+ default font-size avoids the page
   * zooming on focus. */
  .form-input, input[type="text"], input[type="url"], input[type="number"],
  input[type="password"], input[type="email"], select, textarea {
    font-size: 16px;
    min-height: 44px;
  }
  textarea { min-height: 88px; }

  /* Drawer action grid: cards become wider + taller so each
   * action is unambiguously tappable. */
  .drawer-actions-grid {
    grid-template-columns: repeat(2, 1fr);
    gap: 8px;
  }
  .drawer-action-btn {
    padding: 14px 12px;
    font-size: 13px;
    min-height: 56px;
    white-space: normal;
  }

  /* Modals: occupy the full viewport so operators don't lose
   * context to the dimmed backdrop. Stays scrollable inside. */
  .modal {
    width: 100vw !important;
    max-width: none !important;
    min-height: 100vh;
    margin: 0;
    border-radius: 0;
    padding: 16px;
  }
  .modal-overlay { padding: 0; }
  .modal-actions {
    position: sticky;
    bottom: 0;
    background: var(--surface);
    padding-top: 12px;
    margin-top: 16px;
    border-top: 1px solid var(--border);
  }
  .modal-actions button { flex: 1; }

  /* Device drawer: slide in from the right at full width. */
  #device-drawer.open { width: 100vw; }

  /* Settings tabs stack vertically — horizontal scroll feels weird
   * on phones with a long tab list. */
  .settings-tabs {
    flex-wrap: wrap;
    gap: 4px;
  }
  .settings-tab {
    flex: 1 1 30%;
    min-width: 100px;
    padding: 10px 12px;
    text-align: center;
  }

  /* Wide tables: ensure they scroll horizontally rather than
   * overflowing the viewport. Apply broadly — every .table-card
   * gets the scrollable behaviour on mobile. */
  .table-card { overflow-x: auto; -webkit-overflow-scrolling: touch; }
  .table-card table { min-width: 600px; }

  /* The dashboard tile group is a tight horizontal row on desktop;
   * stack as 2-up on phones so each tile stays readable. */
  #home-tiles {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 8px;
  }

  /* Device drawer settings rows stack the label above the input. */
  .drawer-setting-row {
    flex-direction: column;
    align-items: stretch;
    gap: 4px;
    padding: 10px 0;
  }
  .drawer-setting-label {
    min-width: 0;
    font-size: 13px;
  }
}

/* ── Narrower phones: portrait <= 480px ──────────────────────────── */
@media (max-width: 480px) {
  /* Drop the drawer grid to single-column on very small screens. */
  .drawer-actions-grid { grid-template-columns: 1fr; }
  /* Tighten modal padding so more vertical content shows. */
  .modal { padding: 12px; }
  .modal-title { font-size: 16px; }
  /* Tiles stack vertically. */
  #home-tiles { grid-template-columns: 1fr; }
}

/* ════════════════════════════════════════════════════════════════════
 * v3.4.1 — Timeline page (per-device chronological history)
 * ════════════════════════════════════════════════════════════════════ */
/* The kind-filter chip row. .row-8-center is a non-wrapping flex utility shared
   by many toolbars, so we can't add flex-wrap there globally — scope the wrap to
   the timeline chip container, otherwise ~17 chips run off the card edge. */
#timeline-kinds { flex-wrap: wrap; row-gap: 8px; }
.tl-chip {
  background: var(--surface); color: var(--muted);
  border: 1px solid var(--border); border-radius: 14px;
  padding: 3px 12px; font-size: 12px; cursor: pointer;
  text-transform: capitalize;
}
.tl-chip:hover { border-color: var(--accent); color: var(--accent-ink); }
.tl-chip.active { background: var(--accent); color: var(--accent-contrast, #fff); border-color: var(--accent); }
.tl-list { position: relative; }
.tl-row {
  display: grid; grid-template-columns: 14px 1fr auto; gap: 12px;
  align-items: start; padding: 10px 0;
  border-bottom: 1px solid var(--border);
}
.tl-row:last-child { border-bottom: none; }
.tl-dot {
  width: 10px; height: 10px; border-radius: 50%; margin-top: 4px;
  background: var(--muted);
}
.tl-dot.sev-critical { background: var(--red); }
.tl-dot.sev-high     { background: #ff8c00; }
.tl-dot.sev-medium   { background: #d4a017; }
.tl-dot.sev-low      { background: var(--muted); }
/* The middle (1fr) grid column. Without min-width:0 a grid/flex item defaults
   to min-width:auto and refuses to shrink below its content's intrinsic width,
   so a long unbreakable detail (an snmp-recover string, a command line) pushes
   the column wider than the card and the row overflows "out of the box". */
.tl-main { min-width: 0; }
.tl-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0; }
.tl-title { font-weight: 600; color: var(--text); overflow-wrap: anywhere; font-size: 13px; }
.tl-kind { font-size: 11px; color: var(--muted); text-transform: capitalize; }
.tl-devchip {
  font-size: 11px; font-weight: 600; color: var(--accent-ink);
  background: transparent; border: 1px solid var(--border); border-radius: 10px;
  padding: 1px 8px; cursor: pointer;
}
.tl-devchip:hover { border-color: var(--accent); }

/* v3.4.1: CVE↔patch cross-link badge on the Patches table */
.patch-cve-badge {
  margin-left: 8px; font-size: 11px; font-weight: 600;
  color: var(--red); background: rgba(239,68,68,0.12);
  border: 1px solid var(--red); border-radius: 10px;
  padding: 1px 8px; cursor: pointer;
}
.patch-cve-badge:hover { background: rgba(239,68,68,0.22); }

/* v3.4.1: read-only public status page (status.html) */
.status-page { background: var(--bg); color: var(--text); min-height: 100vh; margin: 0; }
.status-wrap { max-width: 640px; margin: 0 auto; padding: 48px 20px; }
.status-wrap h1 { font-size: 28px; margin: 0 0 24px; }
.status-score { text-align: center; margin-bottom: 24px; }
.status-num { font-size: 64px; font-weight: 800; line-height: 1; }
.status-grade { font-size: 13px; font-weight: 600; text-transform: uppercase; letter-spacing: 1px; margin-top: 6px; }
.status-cards { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 28px; }
.status-card { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 16px; text-align: center; }
.status-k { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; }
.status-v { font-size: 28px; font-weight: 700; margin-top: 4px; }
.status-mon { display: flex; align-items: center; gap: 10px; padding: 8px 0; border-bottom: 1px solid var(--border); font-size: 13px; }
.status-pill { display: inline-block; min-width: 48px; text-align: center; padding: 2px 8px; border-radius: 10px; font-size: 11px; font-weight: 700; }
.status-pill.ok { background: rgba(34,197,94,0.15); color: #22c55e; border: 1px solid #22c55e; }
.status-pill.down { background: rgba(239,68,68,0.15); color: var(--red-on-tint); border: 1px solid var(--red); }
.status-err { color: var(--red); padding: 20px 0; }
.status-foot { color: var(--muted); font-size: 12px; margin-top: 24px; text-align: center; }
@media (max-width: 480px) { .status-cards { grid-template-columns: 1fr; } }
.tl-detail { font-size: 13px; color: var(--muted); margin-top: 2px; word-break: break-word; }
.tl-time { font-size: 12px; color: var(--muted); white-space: nowrap; }
.tl-more { display: flex; align-items: center; gap: 12px; justify-content: center; padding: 14px 0 2px; }
@media (max-width: 480px) {
  .tl-row { grid-template-columns: 14px 1fr; }
  .tl-time { grid-column: 2; }
}

/* Fleet health panel (Home) */
.hh-wrap { display: grid; grid-template-columns: 140px 1fr; gap: 24px; align-items: center; }
.hh-score { text-align: center; }
.hh-num { font-size: 48px; font-weight: 700; line-height: 1; }
.hh-grade { font-size: 13px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; margin-top: 4px; }
.hh-sub { font-size: 13px; margin-top: 2px; }   /* widget subtitle = body text */
.hh-bar-track { width: 100%; height: 8px; background: var(--bg); border-radius: 4px; overflow: hidden; }
.hh-bar-fill { height: 100%; border-radius: 4px; transition: width 0.4s ease; }
.hh-counts { display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
.hh-worst { display: flex; flex-direction: column; gap: 4px; margin-top: 12px; }
.hh-dev {
  display: grid; grid-template-columns: 1fr auto auto; gap: 10px; align-items: center;
  background: var(--surface); border: 1px solid var(--border); border-radius: 6px;
  padding: 6px 10px; cursor: pointer; text-align: left; width: 100%;
}
.hh-dev:hover { border-color: var(--accent); }
.hh-dev-name { color: var(--text); font-size: 13px; font-weight: 500; }
.hh-dev-meta { display: flex; gap: 4px; }
.hh-dev-score { font-weight: 700; font-size: 13px; min-width: 28px; text-align: right; }
/* "Why is this host unhealthy?" expander — the worst-device row is now a
   <details>; its <summary> reuses .hh-dev, the body lists the NA reasons. */
.hh-why { width: 100%; }
.hh-why > summary { list-style: none; }
.hh-why > summary::-webkit-details-marker { display: none; }
.hh-why[open] > summary { border-color: var(--accent); border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
.hh-why-body {
  border: 1px solid var(--accent); border-top: none;
  border-radius: 0 0 6px 6px; padding: 8px 10px; display: flex; flex-direction: column; gap: 5px;
}
.hh-why-item { display: flex; gap: 6px; align-items: baseline; font-size: 12px; color: var(--text); }
.hh-trend { margin-top: 12px; }
/* v3.4.2: fleet heat map */
.heatmap-grid { display: flex; flex-wrap: wrap; gap: 4px; }
.heatmap-cell { width: 22px; height: 22px; border-radius: 4px; border: none; cursor: pointer; opacity: 0.85; padding: 0; }
.heatmap-cell:hover { opacity: 1; outline: 2px solid var(--text); }
.hh-spark { display: block; width: 120px; height: 32px; opacity: 0.9; }
@media (max-width: 600px) {
  .hh-wrap { grid-template-columns: 1fr; gap: 12px; }
}

/* Reports page — posture summary preview */
.rep-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
.rep-cell { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 12px 14px; }
.rep-k { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; }
.rep-v { font-size: 28px; font-weight: 700; color: var(--text); line-height: 1.1; margin-top: 4px; }
.rep-unit { font-size: 13px; font-weight: 500; color: var(--muted); margin-left: 3px; }
.rep-sub { font-size: 13px; margin-top: 2px; }   /* widget subtitle = body text */
.rep-compliance { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 12px; }
@media (max-width: 700px) { .rep-grid { grid-template-columns: repeat(2, 1fr); } }

/* v6.4.0 polish — narrow-width fallbacks for fixed-column grids that had no
   single-column collapse and clipped their content on phones. Additive: these
   only NARROW at small widths, never touch the desktop layout or the
   load-bearing sidebar/drawer breakpoints. */
@media (max-width: 900px) { .isl-163 { grid-template-columns: repeat(2, 1fr); } }   /* kanban board */
@media (max-width: 560px) { .isl-163 { grid-template-columns: 1fr; min-height: 0; } }
@media (max-width: 480px) { .rep-grid { grid-template-columns: 1fr; } }             /* reports posture */
@media (max-width: 560px) { .isl-241 { grid-template-columns: 1fr; } }              /* 3-col form grid */
.m-0 { margin: 0; }
.ml-8 { margin-left: 8px; }
/* v3.4.2: agent signed/integrity badge next to the version */
.agent-sig { display: inline-flex; vertical-align: middle; cursor: help; }
.agent-sig.agent-verified { color: var(--green); }
.agent-sig.agent-mismatch { color: var(--red); }
/* v3.4.2: automation rules list */
.auto-rule { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 14px 16px; margin-bottom: 10px; }
.auto-rule.auto-off { opacity: 0.6; }
.auto-rule-name { font-weight: 600; color: var(--text); margin-bottom: 4px; }
.auto-rule-sum { font-size: 13px; color: var(--muted); margin-bottom: 4px; }
.auto-rule-sum b { color: var(--text); font-weight: 600; }
.auto-rule-acts { display: flex; gap: 6px; flex-shrink: 0; }
/* Capacity top-consumers (Reports page) */
.cap-tops { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-top: 16px; }
.cap-top-row { display: flex; justify-content: space-between; gap: 10px; font-size: 13px; padding: 3px 0; border-bottom: 1px solid var(--border); }
.cap-top .rep-k { margin-bottom: 6px; }
@media (max-width: 700px) { .cap-tops { grid-template-columns: 1fr; } }

/* v3.4.2: rollouts, compliance baseline trend, utilities */
.o-50 { opacity: 0.5; }
.ro-badge { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 11px;
  font-weight: 600; text-transform: uppercase; letter-spacing: .03em; border: 1px solid var(--border); }
.ro-badge.rs-running { background: rgba(59,130,246,.15); color: var(--accent-ink); border-color: var(--accent); }
.ro-badge.rs-done    { background: rgba(34,197,94,.15);  color: var(--green-on-tint); border-color: var(--green); }
.ro-badge.rs-paused  { background: rgba(245,158,11,.15); color: var(--amber); border-color: var(--amber); }
.ro-badge.rs-failed,
.ro-badge.rs-cancelled { background: rgba(239,68,68,.12); color: var(--red-on-tint); border-color: var(--red); }
.ro-rings { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }
.ro-arrow { color: var(--muted); }
.ro-ring-pill { display: inline-flex; flex-direction: column; gap: 1px; padding: 4px 10px;
  border-radius: 8px; border: 1px solid var(--border); font-size: 12px; background: var(--surface2); }
.ro-ring-pill.rs-running { border-color: var(--accent); }
.ro-ring-pill.rs-done    { border-color: var(--green); }
.ro-ring-pill.rs-failed  { border-color: var(--red); }
.ro-ring-prog { font-size: 10px; color: var(--muted); }
.cis-spark { display: inline-flex; align-items: flex-end; gap: 2px; height: 28px; }
.cis-spark-bar { width: 4px; border-radius: 1px 1px 0 0; background: var(--muted); }

/* v3.4.2: print-friendly fleet report (Reports → Print / Save as PDF) */
.print-only { display: none; }
@media print {
  /* Print only the posture report, on a forced-light page. We do
     NOT use print-color-adjust:exact — that prints the dark theme's element
     backgrounds and hides the dark text. Instead: white page, dark text, and
     every descendant forced to a transparent (→ white) background so nothing
     from the dark theme survives. Layout is given a frame to settle before
     window.print() (see printFleetReport) so the injected DOM is painted. */
  /* Force a LIGHT colour scheme for the print. Without this, a browser/OS in
     dark mode remaps our explicit colours when printing — our #111 text gets
     lightened to near-white and prints invisible (while the browser's own
     header/footer chrome, unaffected, still shows). color-scheme:light opts the
     printed document out of that substitution. */
  html, body, #print-report, #print-report * { color-scheme: light !important; }
  html, body { background: #fff !important; }
  /* v6.4.2: this used to be an unconditional `body > *:not(#print-report)`,
     which blanked the sheet on all 79 other pages, on every modal/drawer, and
     on the public status board (which has no #print-report at all) — only the
     Reports page ever fills that element. Scope the takeover to "the report is
     actually populated"; everything else falls through to the print-the-active-
     page rules below. `:has()` argument specificity makes this (2,1,1), so it
     beats those rules; on an engine without :has() the whole selector is
     dropped and the report prints WITH the app chrome — degraded, not blank. */
  body:has(#print-report:not(:empty)) > *:not(#print-report) { display: none !important; }
  #print-report:not(:empty) {
    display: block !important;
    color: #111 !important;
    background: #fff !important;
    padding: 0;
    font-size: 13px;
  }
  #print-report * { color: #111 !important; background: transparent !important;
    border-color: #ccc !important; }
  #print-report .pr-head { display: flex; align-items: center; gap: 14px; margin-bottom: 18px; }
  #print-report .pr-logo { height: 40px; width: auto; }
  #print-report h1 { font-size: 28px; margin: 0 0 2px; }
  #print-report h2 { font-size: 16px; margin: 24px 0 8px; border-bottom: 1px solid #bbb; padding-bottom: 4px; }
  #print-report .pr-meta { color: #555 !important; font-size: 12px; }
  #print-report .pr-cards { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 8px; }
  #print-report .pr-card { border: 1px solid #bbb; border-radius: 8px; padding: 12px 16px; min-width: 120px; }
  #print-report .pr-k { font-size: 11px; text-transform: uppercase; color: #555 !important; letter-spacing: .04em; }
  #print-report .pr-v { font-size: 28px; font-weight: 600; }
  #print-report .pr-sub { font-size: 11px; color: #555 !important; }
  #print-report table { border-collapse: collapse; width: 100%; font-size: 13px; margin-top: 4px; }
  #print-report th, #print-report td { text-align: left; padding: 6px 10px; border-bottom: 1px solid #ddd; }
  #print-report th { color: #444 !important; font-size: 11px; text-transform: uppercase; }
  #print-report .pr-ok  { color: #137333 !important; font-weight: 600; }
  #print-report .pr-bad { color: #b00020 !important; font-weight: 600; }
  #print-report .pr-foot { margin-top: 24px; color: #777 !important; font-size: 11px;
    border-top: 1px solid #ddd; padding-top: 8px; }

  /* ── v6.4.2: Ctrl+P on any OTHER surface prints what you are looking at ──
     The rules below are the default path (the report block above overrides
     them when it is populated). Three shapes are printable: the active page,
     an open modal, and the open device drawer — whichever owns the screen. */

  /* 1. Ink on white. We still do NOT set print-color-adjust:exact — the dark
     themes' panel fills would print as dark boxes with dark text on top. Every
     descendant of the printed subtree is flattened to transparent instead, so
     a dark theme prints identically to a light one. */
  body * { color-scheme: light !important; }
  #main-content, #main-content *,
  .modal-overlay.active, .modal-overlay.active *,
  #device-drawer.open, #device-drawer.open * {
    color: #111 !important;
    background: transparent !important;
    background-image: none !important;
    border-color: #ccc !important;
    box-shadow: none !important;
    text-shadow: none !important;
  }
  /* Keep the semantic status colours legible rather than flattening them to
     #111 — a printed posture table is unreadable without ok/warn/bad. */
  #main-content .c-green, .modal-overlay.active .c-green,
  #device-drawer.open .c-green { color: #137333 !important; }
  #main-content .c-red, #main-content .c-crit,
  .modal-overlay.active .c-red, #device-drawer.open .c-red { color: #b00020 !important; }
  #main-content .c-amber, .modal-overlay.active .c-amber { color: #8a5a00 !important; }
  #main-content .c-muted, #main-content .hint,
  .modal-overlay.active .c-muted, #device-drawer.open .c-muted { color: #555 !important; }

  /* 2. Drop the chrome. `#app > header` (not a bare `header`) — status.html
     reuses this stylesheet and its board title is a <header class="status-head">
     that must still print. Only ACTION affordances are hidden by class; a
     blanket `button { display:none }` would blank surfaces whose content IS
     buttons (the theme picker's .theme-card, the doc cards). */
  #rp-topbar,
  #page-watermark,
  #app > header,
  .sidebar,
  .skip-link,
  .kiosk-exit,
  .toast-container,
  .update-banner,
  #demo-banner,
  .table-pager,
  .modal-actions,
  .drawer-actions-grid,
  .drawer-tabs,
  .btn-primary,
  .btn-secondary,
  .btn-icon,
  .btn-chip,
  .toast-action { display: none !important; }

  /* 3. Reclaim the sidebar gutter and the sticky/viewport-locked layout. */
  .app-content, header { margin-left: 0 !important; }
  .container { padding: 12px 0 !important; max-width: none !important; }
  header { position: static !important; }
  .app-layout, #main-content { min-height: 0 !important; }

  /* 4. Let every capped panel print in full instead of clipping at its
     scroll cap (the ~15-row box-overflow rule is a SCREEN affordance; on
     paper a clipped table silently loses rows). */
  #main-content .scrollable-table-wrap,
  #main-content .audit-scroll,
  #main-content .scroll-cap,
  #main-content .scroll-cap-sm,
  #main-content .scroll-cap-lg,
  #main-content .table-card,
  #main-content .cmd-output,
  .modal-overlay.active .modal,
  .modal-overlay.active .scroll-cap,
  .modal-overlay.active .scroll-cap-sm,
  .modal-overlay.active .scroll-cap-lg,
  .modal-overlay.active .audit-scroll,
  .modal-overlay.active .scrollable-table-wrap,
  #device-drawer.open .drawer-panel,
  #device-drawer.open .drawer-content,
  #device-drawer.open .audit-scroll,
  #device-drawer.open .scrollable-table-wrap {
    max-height: none !important;
    height: auto !important;
    overflow: visible !important;
    contain: none !important;
  }
  /* A sticky header inside a now-unclipped table would repeat mid-page. */
  #main-content .scrollable-table-wrap thead th { position: static !important; }

  /* 5. A modal or the drawer owns the screen — print it, not the page behind
     it. Both are lifted out of their fixed/translated positioning into normal
     flow so the whole thing paginates instead of clipping at one viewport. */
  .modal-overlay:not(.active) { display: none !important; }
  .modal-overlay.active {
    position: static !important;
    display: block !important;
    padding: 0 !important;
    animation: none !important;
  }
  .modal-overlay.active .modal {
    max-width: none !important;
    width: auto !important;
    transform: none !important;
    border: none !important;
  }
  #device-drawer.open { position: static !important; display: block !important; }
  #device-drawer .drawer-backdrop { display: none !important; }
  #device-drawer.open .drawer-panel {
    position: static !important;
    transform: none !important;
    width: auto !important;
    max-width: none !important;
    border-left: none !important;
  }
  body:has(.modal-overlay.active) #main-content,
  body:has(#device-drawer.open) #main-content { display: none !important; }

  /* 6. Pagination hygiene: never split a row, a card or a heading across the
     page break, and keep a section title with the rows it introduces. */
  tr, .table-card, .stat-card, .dash-card { break-inside: avoid; }
  .section-title, .page-title, h1, h2, h3 { break-after: avoid; }

  @page { margin: 16mm; }
}

/* v3.4.2 */
.mt-24 { margin-top: 24px; }

/* v3.4.2: Trends charts */
.trend-chart { width: 100%; overflow-x: auto; }
/* v6.4.2: metric explorer pickers (app-trends.js). Two flat panels beside each
   other above the chart — hairline, 11px radius, no font-size of their own so
   they stay on the 13px body anchor. The host box also carries .scroll-cap,
   which owns its height cap; do not add a second max-height here. */
.mx-hostbox {
  flex: 1 1 260px;
  min-width: 200px;
  align-self: stretch;
  padding: 8px 10px;
  border: 1px solid var(--hair);
  border-radius: 11px;
  background: var(--surface);
}
.mx-metricbox {
  flex: 0 1 auto;
  min-width: 150px;
  align-self: stretch;
  padding: 8px 10px;
  border: 1px solid var(--hair);
  border-radius: 11px;
  background: var(--surface);
}
.ts-chart { width: 100%; height: auto; display: block; }
.ts-legend { display: flex; gap: 16px; flex-wrap: wrap; margin-top: 8px; font-size: 12px; color: var(--muted); }
.ts-leg { display: inline-flex; align-items: center; gap: 6px; }
.ts-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
/* v6.3.0 (wave 10 deferral): shared-chart crosshair + hover tooltip. */
.ts-wrap { position: relative; }
.ts-xhair { stroke: var(--muted); stroke-width: 1; stroke-dasharray: 3 3; opacity: 0.65; pointer-events: none; }
.ts-tip {
  position: absolute; z-index: 5; pointer-events: none;
  background: var(--surface2); border: 1px solid var(--hair);
  border-radius: 8px; padding: 6px 9px; font-size: 11px; line-height: 1.5;
  box-shadow: var(--shadow-1); max-width: 220px; white-space: nowrap;
}
.ts-tip-t { color: var(--muted); font-size: 10px; margin-bottom: 2px; }
.ts-tip-dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; margin-right: 4px; }
/* v6.4.2: shared-chart time pinpointing (renderTimeSeries opts.zoom) — the
   drag surface, the selection band, and the exact from/to row under it. Only
   applied to charts that opt in; the classes are absent otherwise. */
.ts-zoomable { cursor: crosshair; touch-action: pan-y; user-select: none; -webkit-user-select: none; }
.ts-sel { fill: var(--accent); fill-opacity: 0.14; stroke: var(--accent); stroke-width: 1; pointer-events: none; }
.ts-zoom { display: flex; align-items: flex-end; gap: 8px; flex-wrap: wrap; margin-top: 10px; }
.ts-zoom-f { display: inline-flex; flex-direction: column; gap: 4px; margin: 0; }
.ts-zoom-lab { font-size: 11px; color: var(--muted); }
.ts-zoom-in { width: auto; min-width: 176px; font-size: 12px; padding: 5px 8px; }
/* Match the field's 26px box exactly so the row has one baseline, not three. */
.ts-zoom .btn-icon { padding: 5px 12px; }
.ts-zoom-hint { font-size: 12px; color: var(--muted); padding-bottom: 6px; }
.ts-zoom button[disabled] { opacity: 0.45; cursor: default; }
.ts-zoom button[disabled]:hover { background: transparent; color: var(--muted); }
@media (max-width: 600px) {
  .ts-zoom-f { flex: 1 1 148px; }
  .ts-zoom-in { min-width: 0; width: 100%; }
  .ts-zoom-hint { flex: 1 1 100%; padding-bottom: 0; }
}
/* v6.3.0: drawer posture radar (the last wave-10 deferral). */
.posture-radar-wrap { display: flex; justify-content: center; }
.posture-radar { max-width: 320px; width: 100%; height: auto; }

/* v3.4.2: Settings → Install checklist */
.setup-step { display: flex; align-items: flex-start; gap: 12px; padding: 12px;
  border: 1px solid var(--border); border-radius: 8px; margin-bottom: 8px; }
/* v6.4.2 a11y (SC 1.4.3): "done" used to be `opacity: 0.75`, which dimmed every
   descendant — the step's hint text fell to 3.38:1 and its status badge to
   3.23:1. Opacity cannot be salvaged here: --muted is only 4.56:1 at FULL
   opacity in the light theme, so any fade at all fails. De-emphasise with the
   surface and border instead, which leaves text contrast untouched. */
.setup-step.done { background: var(--surface2); border-color: var(--border); }
.setup-step-ico { flex: 0 0 auto; margin-top: 1px; }
.setup-step-body { flex: 1 1 auto; min-width: 0; }
.setup-step .btn-secondary { flex: 0 0 auto; align-self: center; }

/* v3.4.2: install / batch-job tracker */
.batch-detail { border-top: 1px solid var(--border); padding-top: 8px; }
.batch-host { display: flex; align-items: center; gap: 8px; padding: 3px 0; font-size: 13px; }
.batch-host svg { flex: 0 0 auto; }

/* v3.11.0: posture batch — settings subsection + wrapping input rows */
.settings-subsection { margin-top: 14px; border-top: 1px solid var(--border); padding-top: 12px; }
.form-row-wrap { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; align-items: center; }
.input-narrow { max-width: 140px; }
/* v3.12.0 fix: inputs inside an inline control row must not each take 100%
   width (the global .form-input default) and stack into separate boxes — let
   them flex inline and wrap gracefully. The input-narrow cap still wins. */
.form-row-wrap > .form-input { width: auto; flex: 1 1 160px; min-width: 120px; }
/* v3.12.0 fix: space between stacked cards (e.g. Software policy's rules card
   and the Current-violations card, which previously touched). */
.table-card + .table-card { margin-top: 22px; }   /* v4.10.0: match the roomier card rhythm */
.cmdb-list-row { margin-bottom: 6px; }

/* v5.0.0: multi-NIC / multi-NAT interface editor + visual tree. */
.cmdb-iface-row {
  display: flex; gap: 8px; align-items: center; margin-bottom: 6px;
  padding: 6px 8px; border: 1px solid var(--border); border-radius: 8px;
  flex-wrap: wrap;
}
.cmdb-iface-row.is-primary { border-color: var(--accent); background: var(--accent-soft); }
.cmdb-iface-row > .form-input { flex: 1 1 120px; min-width: 100px; width: auto; }
.cmdb-iface-row .iface-arrow { color: var(--muted); font-weight: 700; }
.iface-star {
  background: none; border: 0; cursor: pointer; font-size: 16px; line-height: 1;
  color: var(--border); padding: 2px 4px;
}
.iface-star.on { color: #f5b301; }   /* gold = primary */
.cmdb-iface-tree { max-height: 360px; overflow-y: auto; }
/* v6.4.3: same uppercase eyebrow, same fold onto .section-title's values. */
.cmdb-iface-tree-title { font-size: 13px; font-weight: 620; color: var(--muted); margin-bottom: 4px; }
.iface-node { padding: 3px 0; font-size: 13px; }
.iface-node.primary { font-weight: 600; }
.iface-node .iface-nat { padding-left: 18px; color: var(--muted); font-size: 12px; }
.nat-tag {
  display: inline-block; font-size: 10px; padding: 0 5px; border-radius: 3px;
  background: rgba(59,126,255,0.16); color: var(--accent2-ink); border: 1px solid var(--border);
}

/* ── v3.12.0: uniform toolbar control height ──────────────────────────────────
   Buttons, filter inputs and selects that share a control row must line up at
   the SAME height. Scoped to the known toolbar / inline-row containers (never
   form fields in modals), and given enough specificity via :is() that it wins
   regardless of source order. 34px matches a comfortable button height. */
/* v6.4.1: .settings-row and .modal-actions were missing from both lists, so
   a settings row with direct-child controls bottom-aligned a ~32px
   .btn-primary, a ~33px input and a ~34px .btn-icon at three different
   heights. Direct-child scoped, and textareas are not in the
   target list, so multi-line fields are untouched. */
/* v6.4.3: the contract is opt-in BY EXACT CLASS NAME, so it only ever applied
   to rows someone remembered to add. A rendered sweep of every container with
   >=2 sibling controls found 11 rows spreading >=2px, the worst at 5.0px
   (form-input 36 / btn-secondary 33 / btn-primary 31). Added the row wrappers
   actually in use — .row-8 alone is 108 markup sites — plus the four
   device-action buttons and select.select-sm, which are controls that sit in
   these rows but were never in the child list. Still an allowlist rather than
   `> :is(button, input, select)`: a blanket child selector would also capture
   controls in card bodies and modals that are taller. */
:is(.toolbar, .toolbar-mb24, .isl-173, .isl-186, .isl-178, .row-8-center, .row-6-center,
    .row-8, .batch-bar, .home-ai-ask, .sb-controls, .section-header, .form-row-wrap,
    .settings-row, .modal-actions)
  > :is(.btn, .btn-icon, .btn-secondary, .btn-primary, .enroll-btn, .btn-update,
        .btn-upgrade, .btn-shutdown, .btn-reboot, input.form-input, select.form-input,
        select.select-sm) {
  height: 34px;
  min-height: 34px;
  padding-top: 0;
  padding-bottom: 0;
  box-sizing: border-box;
}
/* v7.0.2: the same height, for a select that app.js has WRAPPED.
   `_wrapSelects()` inserts a <span class="rp-ddwrap"> between every <select>
   and its parent at runtime, so the select stops being a direct child and the
   `>` above stops matching it — measured on the seeded instance, 102 of 110
   visible selects are wrapped, which made the two `select.*` clauses in that
   list unreachable. The select then fell back to .input-auto / .select-sm and
   rendered 35-36px (and .isl-16 31px) beside buttons and text inputs pinned at
   34px: 25 toolbar rows across 20 pages, each visibly out of line.
   Mirrors the .dev-combo arm further down, which pierces its own wrapper the
   same way. The wrapper is the flex item now, so it takes the height too —
   otherwise a `display:block` wrapper stretches to the row's tallest control
   and the select sits at its top edge. */
:is(.toolbar, .toolbar-mb24, .isl-173, .isl-186, .isl-178, .row-8-center, .row-6-center,
    .row-8, .batch-bar, .home-ai-ask, .sb-controls, .section-header, .form-row-wrap,
    .settings-row, .modal-actions)
  > .rp-ddwrap {
  align-self: center;
}
:is(.toolbar, .toolbar-mb24, .isl-173, .isl-186, .isl-178, .row-8-center, .row-6-center,
    .row-8, .batch-bar, .home-ai-ask, .sb-controls, .section-header, .form-row-wrap,
    .settings-row, .modal-actions)
  > .rp-ddwrap > :is(select.form-input, select.select-sm) {
  height: 34px;
  min-height: 34px;
  padding-top: 0;
  padding-bottom: 0;
  box-sizing: border-box;
}
/* non-flex buttons in those rows: centre icon + label within the fixed height */
:is(.toolbar, .toolbar-mb24, .isl-173, .isl-186, .isl-178, .row-8-center, .row-6-center,
    .row-8, .batch-bar, .home-ai-ask, .sb-controls, .section-header, .form-row-wrap,
    .settings-row, .modal-actions)
  > :is(.btn, .btn-secondary, .btn-primary) {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 5px;   /* v6.4.3: was 6px — one icon/label gap app-wide (.btn-icon and
                 the global .btn-primary/.btn-secondary rule both use 5px) */
}

/* ── v3.12.0: device combobox (filterbox replacing a device <select>) ─────────
   The native select is kept (value holder) but visually hidden; a text input +
   filtered list overlays it. Full-width in forms/modals; a fixed-width filterbox
   at the shared 34px control height inside toolbars. */
.dev-combo { position: relative; display: block; }
.dev-combo .dev-combo-input { width: 100%; }
.dev-combo-native {
  position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px;
  opacity: 0; pointer-events: none; overflow: hidden; clip: rect(0 0 0 0); border: 0;
}
.dev-combo-list {
  position: absolute; z-index: 60; top: calc(100% + 2px); left: 0; right: 0;
  min-width: 200px; max-height: 260px; overflow-y: auto;
  background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
  box-shadow: var(--shadow-2);
}
.dev-combo-item {
  padding: 7px 12px; font-size: 13px; cursor: pointer;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.dev-combo-item:hover, .dev-combo-item.active { background: var(--bg); }
.dev-combo-item.sel { color: var(--accent-ink); }
.dev-combo-empty { padding: 8px 12px; font-size: 13px; color: var(--muted); }
:is(.toolbar, .toolbar-mb24, .row-8-center, .isl-186, .isl-173, .section-header) > .dev-combo {
  width: 240px; max-width: 100%; flex: 0 0 auto;
}
:is(.toolbar, .toolbar-mb24, .row-8-center, .isl-186, .isl-173, .section-header) > .dev-combo .dev-combo-input {
  height: 34px; min-height: 34px; padding-top: 0; padding-bottom: 0; box-sizing: border-box;
}

/* v3.12.0: table pager (Prev / x–y of N / Next) under paginated tables */
.table-pager { display: flex; align-items: center; gap: 12px; justify-content: flex-end; margin: 10px 0 4px; }
.table-pager .btn-icon[disabled] { opacity: .4; cursor: default; }
.table-pager-info { font-size: 12px; color: var(--muted); }

/* ════════════════════════════════════════════════════════════════════════════
   v6.0.0 "ClarityMatters" — the Industrial skin is FULLY REMOVED. Its live
   palette/font/status tokens were consolidated into :root / body.light at the
   top of this file; the skin attribute is gone for good. (The v4.6.0→v5.8.0
   "Industrial" era: graphite, chamfers, IBM Plex/Space Grotesk — see git.)
   ════════════════════════════════════════════════════════════════════════════ */

.stat-value,
.tile-value { font-variant-numeric:tabular-nums; }

/* ── Sidebar + topbar: MIGRATED to Clarity (v6 phase 1) — no industrial
   rules left (machined nav, uppercase logo-text, squared logo-img all gone). */

/* (v6: page-header industrial overrides gone — base .page-title/.page-subtitle
   carry the design.) */

/* (v6: the industrial sharp-corner lists are gone — components keep their own
   base radii; migrated panels sit on the design 11px.) */

/* ── Buttons read like panel controls ───────────────────────────────────────
   Keep buttons at the 14px body/input size so a button never sits SMALLER than
   the input beside it (the enforcement-policy row had a 12px `fs-12` button next
   to 14px inputs). This selector outranks the .fs-12 utility, so every
   industrial form control lands on one size project-wide. Tabs are their own
   strip and stay at the dense 12px. ── */
/* (v6: uppercase button/tab treatments gone — sentence-case controls.) */
/* Settings tabs read like the sidebar: rounded pill highlight (not an accent
   underline). The radius overrides the industrial skin's grouped 2px so the
   pills match the sidebar nav in both skins; the keyboard-focus state gets the
   same greyish highlight plus an accent ring. */
.settings-tab { border-radius: 6px; }
.settings-tab:focus-visible {
  color: var(--text);
  background: var(--surface2);
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
/* The tab strip scrolls horizontally when the tabs overflow. Two fixes:
   (1) its scrollbar thumb used the global border-radius:10px → a grey ROUNDED
   pill; square it. (2) give the pills a little spacing + clearance under the
   scrollbar so it isn't crammed against them. */
.settings-tabs {
  gap: 6px;
  padding-bottom: 10px;
  scrollbar-width: thin;
  scrollbar-color: var(--border) transparent;   /* Firefox */
}
.settings-tabs::-webkit-scrollbar { height: 6px; }
.settings-tabs::-webkit-scrollbar-track { background: transparent; }
.settings-tabs::-webkit-scrollbar-thumb {
  background: var(--border);
  border-radius: 0;                              /* square, not the rounded pill */
}

/* (v6: the corner-tick card framing is gone — flat hairline panels.) */

/* (v6: the chamfered warm-amber patch-panel card TAB is gone — card titles
   are plain 13px/620 ink headings (chosen-design .gt) via the base
   .section-title / .dash-card h3 / .settings-section h3 rules.) */

/* (v6: mono-uppercase eyebrow labels + instrument stat-card overrides gone —
   base styles carry the design.) */

/* (v6: tables use the base th/td styling — hairline rows, 11px muted headers.) */

/* (v6: the universal 3px-radius "one shape language" is gone — components
   keep their own radii.) */

/* ── Icons: one consistent weight + round caps across the whole UI (v6: the
   design's 1.9 stroke — chosen-design svg rule; was industrial-scoped 1.75).
   CSS overrides the SVG presentation attrs; filled icons are unaffected. ── */
svg { stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }

/* (v6.0.0: the v4.6.0 New UI / Old UI selector CSS is gone with the toggle.) */

/* v4.7.0: Docker-host enrollment block inside the Enroll modal. */
.enroll-or { text-align:center; color:var(--muted); font-size:12px;
  margin:18px 0 10px; text-transform:uppercase; letter-spacing:0.04em; }
.enroll-docker { text-align:left; }
.enroll-docker-pre { max-height:280px; overflow:auto; white-space:pre;
  background:var(--surface2); color:var(--text); padding:10px 12px;
  border:1px solid var(--border); border-radius:6px; margin-top:8px; }
/* v4.7.0: homelab software integrations (Settings → Integrations). */
/* Status is shown with the canonical .status-pill (themed, with a dot). The
   card flows naturally; the LIST is scroll-capped (.scroll-cap) per the
   box-overflow rule, like the sibling webhook-destinations list. */
.integration-card { border:1px solid var(--border); border-radius:8px; padding:12px 14px;
  margin-bottom:12px; background:var(--surface); }
.integration-card-head { display:flex; align-items:center; gap:10px; margin-bottom:6px; }
.integration-card .settings-row { gap:14px; flex-wrap:wrap; }
/* Connector-catalog rows: label + meta share a text baseline (not the
   form-row flex-end that bottom-aligns differently-sized text). */
.connector-row { display:flex; align-items:baseline; gap:6px; padding:3px 0; flex-wrap:wrap; }
.connector-row > strong { font-weight:600; }
/* Server-status "Restart the stack" runbook command */
.self-restart-cmd { font-family:var(--font-mono); font-size: 13px; background:var(--surface2); border:1px solid var(--hair); border-radius:6px; padding:4px 8px; overflow-x:auto; flex:1; min-width:0; }
.ff-bold { font-weight:600; }
.spacer  { flex:1 1 auto; }
.mw-100 { max-width:100px; } .mw-220 { max-width:220px; } .mw-260 { max-width:260px; }
.ml-12 { margin-left:12px; }

/* v4.7.0: rich integration TILES (Dashy-style) — the dedicated Integrations page
   + the dashboard widget. Theme-token only, so they track every theme incl.
   Industrial light/dark. */
/* Box-overflow rule: cap the full-fleet integration list (one row per
   connector, 26+ possible) and scroll internally instead of growing unbounded. */
.integ-tiles { display:flex; flex-direction:column; gap:6px; max-height:480px; overflow-y:auto; }
.integ-row { display:flex; align-items:center; gap:10px; padding:8px 12px;
  border:1px solid var(--border); border-radius:8px; background:var(--surface);
  font-size:13px; }
.integ-row-link { cursor:pointer; transition:border-color .15s; }
.integ-row-link:hover { border-color:var(--accent); }
.integ-row-pill { flex:none; }
.integ-row-name { font-size: 13px; font-weight:600; color:var(--text);
  white-space:nowrap; flex:none; }
.integ-row-stats { display:flex; align-items:center; gap:6px; flex-wrap:wrap;
  flex:1 1 auto; min-width:0; color:var(--muted); }
.integ-stat { white-space:nowrap; }
.integ-sep { color:var(--muted); opacity:.5; }
.integ-stat b { color:var(--text); font-weight:600; font-variant-numeric:tabular-nums; }
.integ-row-detail { color:var(--muted); white-space:nowrap; overflow:hidden;
  text-overflow:ellipsis; }
.integ-row-meta { margin-left:auto; font-size:11px; color:var(--muted);
  white-space:nowrap; flex:none; }
/* Mini list inside the dashboard widget — capped + scrolled (box-overflow rule). */
.integ-tiles-mini { max-height:300px; overflow:auto; }

/* v4.7.0: fleet GPU page (NVIDIA + AMD) — rich cards with util/VRAM meters. */
/* Box-overflow rule: cap the full-fleet GPU card grid and scroll internally
   (a large GPU fleet would otherwise grow the page unbounded). */
.gpu-grid { display:grid; gap:14px; grid-template-columns:repeat(auto-fill, minmax(280px, 1fr));
            max-height:640px; overflow-y:auto; }
.gpu-card { border:1px solid var(--border); border-radius:10px; padding:14px 16px;
  background:var(--surface); display:flex; flex-direction:column; gap:10px; }
.gpu-card.gpu-off { opacity:.6; }
.gpu-card-top { display:flex; align-items:center; gap:8px; }
.gpu-vendor { font-size:10px; font-weight:700; letter-spacing:.04em; padding:2px 7px;
  border-radius:4px; text-transform:uppercase; background:var(--surface2); color:var(--muted); }
.gpu-vendor-nvidia { background:rgba(118,185,0,.16); color:#76b900; }
.gpu-vendor-amd    { background:rgba(237,28,36,.14); color:#e0313b; }
.gpu-name { font-size: 13px; font-weight:600; color:var(--text);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.gpu-host { font-size:12px; margin-top:-4px; }
.gpu-meter-h { display:flex; justify-content:space-between; font-size:12px;
  color:var(--muted); margin-bottom:3px; }
.gpu-bar { height:8px; border-radius:999px; background:var(--surface2);
  overflow:hidden; border:1px solid var(--border); }
.gpu-bar-fill { height:100%; border-radius:999px; transition:width .4s ease; }
.gpu-bar-util { background:var(--accent); }
.gpu-bar-mem  { background:#9b6dff; }
.gpu-mem-txt { font-size:11px; margin-top:3px; }
.gpu-stats { display:flex; gap:18px; border-top:1px solid var(--border); padding-top:9px; }
.gpu-stat { display:flex; flex-direction:column; line-height:1.2; }
.gpu-stat-l { font-size:11px; color:var(--muted); }
.gpu-stat b { font-size:16px; font-weight:600; font-variant-numeric:tabular-nums; }
.gpu-trend { display:flex; flex-direction:column; gap:4px; margin-top:9px;
  border-top:1px solid var(--border); padding-top:9px; }
.gpu-trend-row { display:flex; align-items:center; gap:8px; }
.gpu-trend-l { font-size:11px; color:var(--muted); width:34px; flex:none; }
.gpu-trend .sparkline { display:block; }

/* v5.0.0: Thermal page — expandable per-sensor breakdown + caret. */
.rot-180 { transform: rotate(180deg); }
.thermal-expand { display:inline-flex; align-items:center; gap:5px; }
.thermal-expand svg { transition: transform 0.15s ease; }
.thermal-detail-row > td { background: var(--surface-2, rgba(127,127,127,0.05)); padding: 6px 12px 10px; }
.thermal-sensor-table { font-size: 13px; }
.thermal-sensor-table th { font-size: 11px; }
/* v6.4.3: give Thermal health real room.
   Both of its surfaces sat at the shared 360px cap: the fleet table (one row
   per host) and the expanded drill-down (one row per SENSOR — a NAS with a
   dozen disks overflows immediately). The result was a small scrolling window
   in the middle of a large empty page, on the one page whose whole job is
   comparing many hosts at once.
   Viewport-relative rather than a bigger fixed number: 360px was a guess that
   is wrong on every screen, while 72vh is roughly double on a 1000px viewport
   and keeps growing on a tall monitor instead of needing another bump. These
   still CAP AND SCROLL, so the box-overflow rule and its ratchet hold, and
   .scrollable-table-wrap's sticky header is retained — which matters more the
   longer the table gets. Page-scoped, the same shape #page-netmap already
   uses, so the shared utility (and every other table) is untouched. */
#page-thermal > .table-card { max-height: none; }
#page-thermal .scrollable-table-wrap.audit-scroll { max-height: 72vh; }
/* The drill-down nests INSIDE the table above, so it needs its own smaller
   cap or the inner scroller would fight the outer one. */
#page-thermal .thermal-detail-row .scrollable-table-wrap.audit-scroll {
  max-height: 46vh;
}

/* Hide the page subtitle by DEFAULT so it never flashes as raw (untranslated)
   text on reload before JS runs; _applyPageSubtitleInfo adds .subtitle-shown
   to reveal it inline (v6: subtitles are always inline). */
.subtitle-hidden { display:none !important; }
.page-subtitle { display:none; }
.page-subtitle.subtitle-shown { display:block; }
/* (v6: the page-title info-icon + tooltip are gone — subtitles render inline.) */

/* ════════════════════════════════════════════════════════════════════════════
   v5.0.0 "CTRLMatters" — Industrial design pass. Sharpens the control-panel
   language for big fleets: aligned digits, machine-vs-prose typography, a single
   semaphore system, hard-edged controls, a compact density mode, and the NOC
   status board. Appended last so it wins by source order. CSP-safe (classes
   only). ════════════════════════════════════════════════════════════════════ */

/* #2 Tabular numerals — digits that don't jitter as metrics tick every 60s.
   Applied to every place numbers stack into columns. */
td, .num, .tnum, .stat-value, .metric-value, .meta-sm, .meta-sm-nm,
input[type="number"], .mono-12, .fs-13, .fs-12, .fs-11 {
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
}

/* #1 Machine voice — IDs, IPs, versions, ports, hashes, paths render in the
   mono "instrument" face; prose stays Inter. Use .machine on value cells. */
.machine, .mono-val { font-family: var(--font-mono); }

/* #10 Compact density — operators on 1000+ nodes want max rows per screen.
   Toggled via body.density-compact (header button, persisted). */
body.density-compact td { padding: 5px 9px; }
body.density-compact .card,
body.density-compact .dash-card { padding: 11px 13px; }
body.density-compact .drawer-section { padding: 9px 14px 0; }
body.density-compact .section-title,
body.density-compact .group-title { margin-bottom: 6px; }
.density-toggle-btn.active { color: var(--accent2-ink); border-color: var(--accent-edge); }

/* #3 Semaphore — ONE status-dot system. Always paired with text in markup, and
   carries an aria-label, so it's never colour-only. */
.sem {
  display: inline-block; width: 8px; height: 8px; border-radius: 50%;
  vertical-align: middle; margin-right: 6px; flex: 0 0 auto;
  box-shadow: 0 0 0 1px rgba(0,0,0,0.25) inset;
}
.sem-ok   { background: var(--green); }
.sem-warn { background: var(--amber); }
.sem-down { background: var(--red); }
.sem-idle { background: var(--muted); }
/* square variant for a second, shape-based cue (colour-blind safety) */
.sem.sq { border-radius: 1px; }

/* #7 Segmented control — hard-edged, mono labels; replaces soft pills for
   filter/grouping toggles. */
.segmented {
  display: inline-flex; border: 1px solid var(--border); border-radius: 6px;
  overflow: hidden; background: var(--surface);
}
.segmented button {
  padding: 5px 12px; font-family: var(--font-mono); font-size: 12px;
  background: transparent; border: 0; border-right: 1px solid var(--border);
  color: var(--muted); cursor: pointer; transition: background .12s, color .12s;
}
.segmented button:last-child { border-right: 0; }
.segmented button:hover { color: var(--text); }
.segmented button.active { background: var(--accent-soft); color: var(--accent2-ink); }

/* #8 Header vitals strip — a thin instrument readout of fleet vitals. */
.vitals { display: inline-flex; align-items: center; gap: 14px; font-family: var(--font-mono); font-size: 12px; color: var(--muted); }
.vitals .vital { display: inline-flex; align-items: center; gap: 5px; }
.vitals .vital b { color: var(--text); font-weight: 600; }
.vitals .vital.bad b { color: var(--red); }
.vitals-sep { width: 1px; height: 14px; background: var(--border); }
@media (max-width: 980px) { .vitals { display: none; } }

/* v5.6.0: top-bar site-health pill. Green "Healthy" at rest; red "N issues"
   (offline + open alerts + monitors down) when anything needs attention. */
/* v6: flat (chosen-design .health) — coloured text + icon, no pill chrome. */
.site-health {
  display: inline-flex; align-items: center; gap: 7px; white-space: nowrap;
  font-family: var(--font-ui); font-size: 12px; font-weight: 400; line-height: 1;
  padding: 4px 7px; border-radius: 6px; cursor: pointer; border: none; background: none;
}
/* v6.4.3: hide the pill until it has something to say. It ships EMPTY —
   _paintSiteHealth writes its content, and that only runs when /nav-counts
   returns on the 60s refresh cycle — so for the first ~71 seconds of every
   session it rendered as a bare 14x8px styled box in the topbar. `:empty`
   makes it self-maintaining: no class to add on load, none to remove on
   paint, and it also covers any future path that clears the content. It
   cannot default to "Healthy" instead: that is a claim about the control
   plane made before the control plane has been checked. */
.site-health:empty { display: none; }
.site-health b { font-weight: 600; }
.site-health.ok  { color: var(--green); }
.site-health.bad { color: var(--red); }
.site-health:hover { background: var(--hair-2); }
@media (max-width: 720px) { .site-health { display: none; } }

/* #5 NOC status board — a glanceable control-room wall view, built for scale:
   group/site ROLLUP tiles (not 1000 raw host tiles) + a problem-host strip. */
.board-vitals { display: flex; flex-wrap: wrap; gap: 14px; margin-bottom: 16px; }
.board-stat {
  border: 1px solid var(--border); border-radius: 8px; padding: 12px 16px;
  background: var(--surface); min-width: 132px;
}
.board-stat .meta-label { display: block; margin-bottom: 4px; }
.board-stat .big { font-size: 28px; font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1; }
.board-problem-strip {
  border: 1px solid var(--red); border-radius: 8px; padding: 12px 14px;
  background: rgba(239,68,68,0.06); margin-bottom: 16px;
}
.board-problem-strip .meta-label { color: var(--red); }
.board-prob-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; max-height: 132px; overflow-y: auto; }
.board-prob-chip {
  display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
  border: 1px solid var(--border); border-radius: 6px; padding: 4px 10px;
  background: var(--surface); font-size: 12px;
}
.board-prob-chip .machine { color: var(--text); }
.board-prob-chip .reason { color: var(--muted); }
.board-grid {
  display: grid; gap: 12px;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  /* Cap unbounded growth (box-overflow rule) — generous for a wallboard but
     scroll past ~15 rows of tiles instead of pushing the page down forever. */
  max-height: 70vh; overflow-y: auto;
}
.board-tile {
  border: 1px solid var(--border); border-radius: 8px; padding: 14px;
  background: var(--surface); cursor: pointer; transition: border-color .12s, transform .12s;
  contain: layout style;   /* v5.6.x perf: isolate per-tile recalc from the wallboard grid */
}
.board-tile:hover { border-color: var(--accent-edge); transform: translateY(-1px); }
.board-tile.has-down { border-left: 3px solid var(--red); }
.board-tile.has-warn { border-left: 3px solid var(--amber); }
/* v6.3.1: same severity-tint treatment as the advisory cards (see .adv-sev-*). */
@supports (background: color-mix(in srgb, red 6%, white)) {
  .board-tile.has-down {
    background: color-mix(in srgb, var(--red) 6%, var(--surface));
    border-color: color-mix(in srgb, var(--red) 26%, var(--border));
    border-left-color: var(--red);
  }
  .board-tile.has-warn {
    background: color-mix(in srgb, var(--amber) 6%, var(--surface));
    border-color: color-mix(in srgb, var(--amber) 24%, var(--border));
    border-left-color: var(--amber);
  }
}
.board-tile-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.board-tile-name { font-family: var(--font-mono); font-size: 13px; font-weight: 600; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.board-tile-count { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
.board-bar { display: flex; height: 6px; border-radius: 3px; overflow: hidden; margin-top: 10px; background: var(--surface2); }
.board-bar > span { display: block; height: 100%; }
.board-bar .seg-ok   { background: var(--green); }
.board-bar .seg-warn { background: var(--amber); }
.board-bar .seg-down { background: var(--red); }
.board-bar .seg-idle { background: var(--border); }

/* ── v5.0.1 "TemperMatters" — render-perf isolation ──────────────────────────
   Encourage the browser to keep the fixed sidebar on its own compositor layer
   so scrolling the main content (or the nav list) doesn't repaint it every
   frame — the cause of the sidebar scroll/FPS sluggishness on long sessions.
   will-change (not transform: translateZ(0)) is used on purpose: it's a layer
   HINT on a separate property, so it never overrides the responsive
   `transform: translateX(...)` rules that drive the mobile slide-in.
   overscroll-behavior stops scroll-chaining repaints into the page. */
.sidebar { will-change: transform; overscroll-behavior-y: contain; }

/* Layout + style containment isolates each dashboard card's reflow / style
   recalc from its siblings (a change in one card no longer reflows the rest).
   NOT `contain: paint`/`content` — cards host overflowing ⋯ action dropdowns
   that must not be clipped; layout+style contains without clipping. */
.dash-card { contain: layout style; }

/* ── v5.0.1 "TemperMatters" — tabular numerals ───────────────────────────────
   Align digits to a fixed width in tables + big-number displays so columns of
   numbers line up and read cleanly (proportional text is unaffected — this only
   changes digit glyph width). A precise, technical touch that fits the
   industrial language and improves the at-a-glance overview feel. */
table { font-variant-numeric: tabular-nums; }
.status-num, .hh-num, .pin-code, .board-num, .stat-num,
.metric-value, .big-num, td .mono-12 { font-variant-numeric: tabular-nums; }

/* ── v5.0.1 "TemperMatters" — design polish ──────────────────────────────────
   Weight/contrast + tactile feedback on the dense data tables, within the
   industrial language. (Keyboard focus rings, KPI-row density and tabular
   numerals are handled elsewhere already, so this stays focused.) */

/* Weight & contrast: column headers as crisp, tracked eyebrows — faster to scan
   and reads intentional (mirrors the mono-uppercase label language used for
   stat/form labels). Text/weight only — doesn't touch the sticky-header bg. */
.table-card thead th {
  text-transform: uppercase;
  letter-spacing: 0.05em;
  font-size: 11px;
  font-weight: 600;
}

/* Depth & micro-interaction: row hover on the big tables (CVE / patches / drawer
   tables had none — only the minimal device table did). GPU-cheap: background
   transition only, no layout. */
.table-card tbody tr { transition: background .12s ease; }
.table-card tbody tr:hover { background: var(--surface2); }

/* Overview tiles: a quiet accent edge on hover so the KPI tiles feel responsive
   without breaking the flat instrument-readout look. Interactive tiles (those
   wired to a drill-down) also lift slightly. */
.stat-card { transition: border-color .15s ease, transform .15s ease, box-shadow .15s ease; }
.stat-card:hover { border-color: var(--accent-edge); }
.stat-card[data-action]:hover, .stat-card[data-nav]:hover {
  transform: translateY(-2px); box-shadow: var(--shadow-1); border-color: var(--accent);
}
@media (prefers-reduced-motion: reduce) {
  .stat-card[data-action]:hover, .stat-card[data-nav]:hover { transform: none; }
}

/* (v6: the "TemperMatters" chamfer-button block is GONE — action buttons use
   the flat v6 base styles above: hairline border, 7px radius, accent fill for
   primary/enroll. Device-drawer colour buttons keep their base tints.) */

/* ── v5.0.1 "TemperMatters" — page header → content spacing (proactive sweep) ─
   In the industrial theme the inline page subtitle is folded into the title
   info-icon (.subtitle-hidden → display:none), so the 40px gap that subtitle
   used to put between the header and the first content vanished — leaving only
   the title's 8px. Content elements space themselves with margin-BOTTOM, so
   nothing restored that top gap and every page sat a little tight (Network
   Metrics' heavy RX/TX/Reporting vitals made it obvious). Restore one consistent
   header→content gap for every page, in one place — no per-page patches, and no
   double-spacing since first-content margins are all bottom-side. Scoped to the
   industrial theme; the classic UI keeps its visible-subtitle spacing. */
/* (v6: base .page-title margins own the header→content gap.) */

/* ===== v5.1.0: RTL (dir="rtl") layout overrides ===========================
   The document sets dir="rtl" for the Arabic (ar) locale and the TEXT is
   already correct, but none of the chrome layout was mirrored. These rules
   mirror only the structural left/right that read wrong in RTL — they are
   ADDITIVE (no existing rule is touched) and reference only selectors that
   already exist above. Flexbox/grid/text flow already follow `direction`,
   so we override only the explicit physical (left/right) properties the
   layout hard-codes. No new design tokens; the canonical scale is unchanged.
   ========================================================================== */

/* 1. Fixed sidebar → right edge; main content margin swaps to the right. ---- */
[dir="rtl"] .sidebar {
  left: auto;
  right: 0;
  border-right: none;
  border-left: 1px solid var(--hair);
}
[dir="rtl"] .app-content {
  margin-left: 0;
  margin-right: 248px;  /* lockstep with .sidebar width */
}
[dir="rtl"] header {
  margin-left: 0;
  margin-right: 248px;  /* lockstep with .sidebar width */
}
[dir="rtl"] body.sidebar-collapsed header,
body.sidebar-collapsed[dir="rtl"] header {
  margin-left: 0;
  margin-right: 56px;
}
/* Collapsed icon-rail keeps the swapped side at its narrow width. */
[dir="rtl"] body.sidebar-collapsed .app-content,
body.sidebar-collapsed[dir="rtl"] .app-content {
  margin-left: 0;
  margin-right: 56px;
}

/* 2. Sidebar nav + search: text reads right-aligned; the group tree rail and
      the active-branch accent move to the right side. ----------------------- */
[dir="rtl"] .nav-btn,
[dir="rtl"] .sidebar-group-toggle,
[dir="rtl"] .sidebar-search-hit { text-align: right; }
/* Icon→label flex rows visually order icon-then-label in LTR; reverse so the
   icon sits on the leading (right) side in RTL. */
[dir="rtl"] .nav-btn,
[dir="rtl"] .sidebar-group-toggle { flex-direction: row-reverse; }
/* The tree connector rail + child indent move to the right. */
[dir="rtl"] .sidebar-group-items {
  margin-left: 0;
  margin-right: 9px;
  padding-left: 0;
  padding-right: 8px;
  border-left: none;
  border-right: 1px solid var(--hair);
}
[dir="rtl"] .sidebar-group.collapsed .sidebar-group-items {
  border-right-color: transparent;
}
/* The active row's short accent bar moves to the (now-right) rail. */
[dir="rtl"] .nav-btn.active::before {
  left: auto;
  right: -9px;
}
/* margin-left:auto spacers (group badge + chevron) push to the leading edge. */
[dir="rtl"] .sidebar-group-toggle .nav-group-badge,
[dir="rtl"] .sidebar-group-toggle .chevron {
  margin-left: 0;
  margin-right: auto;
}
[dir="rtl"] .sidebar-group-toggle .nav-group-badge {
  margin-left: 6px;   /* mirror the LTR margin-right:6px gap after the badge */
}

/* 3. Headings / table cells: left-aligned text → right. -------------------- */
[dir="rtl"] th { text-align: right; }

/* 4. Device drawer slides in from the LEFT in RTL (mirror of the right slide). */
[dir="rtl"] .device-drawer .drawer-panel {
  right: auto;
  left: 0;
  border-left: none;
  border-right: 1px solid var(--border);
  transform: translateX(-100%);
}
[dir="rtl"] .device-drawer.open .drawer-panel { transform: translateX(0); }
/* Drawer action buttons: icon on the leading (right) side, text right-aligned. */
[dir="rtl"] .drawer-action-btn {
  flex-direction: row-reverse;
  text-align: right;
}

/* 5. Toasts slide in from the LEFT and stack at the left edge in RTL. ------- */
[dir="rtl"] .toast-container {
  right: auto;
  left: 24px;
}
[dir="rtl"] .toast { transform: translateX(-120%); }
[dir="rtl"] .toast.show { transform: translateX(0); }
/* The status accent rail moves to the trailing (right) side. */
[dir="rtl"] .toast.success { border-left: none; border-right: 3px solid var(--green); }
[dir="rtl"] .toast.error   { border-left: none; border-right: 3px solid var(--red); }
[dir="rtl"] .toast.info    { border-left: none; border-right: 3px solid var(--accent); }
[dir="rtl"] .toast-action  { margin-left: 0; margin-right: auto; }

/* 6. Chrome margin utilities actually used in the nav/header — swap the side. */
[dir="rtl"] .ml-8  { margin-left: 0; margin-right: 8px; }
[dir="rtl"] .ml-12 { margin-left: 0; margin-right: 12px; }
/* 7. Content-area status/indent accent rails — mirror the left border + pad so
   the rail sits on the leading (right) edge under RTL, not the trailing one. */
[dir="rtl"] .board-tile.has-down { border-left: none; border-right: 3px solid var(--red); }
[dir="rtl"] .board-tile.has-warn { border-left: none; border-right: 3px solid var(--amber); }
[dir="rtl"] .alerts-row.alert-symptom .alert-symptom-cell {
  padding-left: 0; padding-right: 22px; border-left: none; border-right: 2px solid var(--border);
}
[dir="rtl"] .isl-520 { border-left: none; border-right: 3px solid var(--border); }
/* ===== end v5.1.0 RTL overrides =========================================== */

.icon-sprite { display: none; }

/* ════════════════════════════════════════════════════════════════════════
   v5.1.0 INDUSTRIAL — curated treatment set (12 items, industrial skin only).
   CSP-safe (classes only), font-size on the {28,16,14,13,12,11,10} scale,
   motion gated by prefers-reduced-motion. Tokens (--warm/--accent-edge) live
   in the industrial dark+light blocks above.
   ════════════════════════════════════════════════════════════════════════ */
/* (v6: the chamfered-button machinery — --rp-cham octagons, per-variant
   fills, uppercase — is gone; base buttons carry the flat design.) */
/* (v6: icon-button chamfer gone — base .btn-icon is the flat hairline control.) */

/* #4 · Scanline-sweep skeletons */
.skeleton,
.skeleton-line,
.skeleton-card{ position:relative; overflow:hidden; background:var(--surface2); }
.skeleton::after,
.skeleton-line::after,
.skeleton-card::after{
  content:""; position:absolute; inset:0; transform:translateX(-100%);
  background:linear-gradient(90deg,transparent,var(--accent-soft) 40%,var(--accent2),var(--accent-soft) 60%,transparent);
  animation:rp-sweep 1.5s linear infinite;
}
@keyframes rp-sweep{ to{ transform:translateX(100%); } }

/* #5 · Schematic empty state */
.empty-state:not(td){ border:1px dashed var(--hair); border-radius:7px; }
.empty-icon svg{ stroke:var(--accent-edge); }

/* #6 · Keycap shortcuts */
kbd{
  background:none; border:1px solid var(--hair);
  border-radius:4px; padding:1px 5px; font-family:var(--font-mono); font-size:9.5px; color:var(--muted);
}

/* #1 · Animated icons — pulsing status dots + spin-on-load utility */
/* v5.6.x perf: removed the per-online-device infinite pulse (rp-pulse). At fleet
   scale every online dot was an always-animating compositor layer, keeping the
   GPU busy at idle and starving scroll. Static coloured dots convey status just
   as well. rp-pulse is kept for the bounded critical-state indicator only. */
@keyframes rp-pulse{ 50%{ opacity:.35; } }
.rp-spin{ animation:rp-spin 1.1s linear infinite; transform-origin:50% 50%; }
@keyframes rp-spin{ to{ transform:rotate(360deg); } }

/* #8 · Divider with centre label */
.rp-divider{ display:flex; align-items:center; gap:12px; color:var(--muted);
  font-family:var(--font-mono); font-size:10px; text-transform:uppercase; letter-spacing:.15em; margin:14px 0; }
.rp-divider::before,.rp-divider::after{ content:""; height:1px; background:var(--border); flex:1; }

/* #10 · Tag & filter chips */
/* (v6: chamfered tag/badge chips are gone — base chip styles apply.) */

/* #11 · Hazard stripe (critical / destructive zones) */
.rp-haz{ height:12px; border-radius:2px;
  background:repeating-linear-gradient(45deg,#1a1205,#1a1205 9px,var(--warm) 9px,var(--warm) 18px); }

/* #9 · Top scanline progress bar (pinned to viewport top, shown during loads) */
#rp-topbar{ position:fixed; left:0; top:0; height:2px; width:100%; z-index:9999;
  background:transparent; overflow:hidden; opacity:0; transition:opacity .2s; pointer-events:none; }
#rp-topbar.on{ opacity:1; }
#rp-topbar::after{ content:""; position:absolute; inset:0; transform:translateX(-100%);
  background:linear-gradient(90deg,transparent,var(--accent2,#5c9fff),transparent); }
/* v5.6.x perf: the sweep MUST only run while the bar is shown (`.on`, during a
   load). It used to animate unconditionally — but the bar is `opacity:0` and
   present at body level on every page, so an invisible infinite animation kept
   the compositor generating a frame ~every 13ms forever (idle GPU ~20%, ~76
   animation-frames/s in a DevTools trace), starving scroll of frames. Gating it
   to `.on` lets the compositor go idle when nothing is loading. */
#rp-topbar.on::after{ animation:rp-sweep 1.1s linear infinite; }

/* #7 · Path breadcrumb readout */
.rp-crumb{ font-family:var(--font-mono); font-size:12px; color:var(--muted); }
.rp-crumb b{ color:var(--text); } .rp-crumb i{ color:var(--accent2-ink); font-style:normal; padding:0 4px; }

@media (prefers-reduced-motion: reduce){
  .skeleton::after,
  .skeleton-line::after,
  .skeleton-card::after,
  .status-dot.online,
  .rp-spin, #rp-topbar::after, #rp-topbar.on::after{ animation:none; }
}

/* #8 · login divider shows only when an alt sign-in method is visible (CSS :has, no JS) */
#login-sso-divider{ display:none; }
.login-card:has(.login-btn-oidc:not(.d-none)) #login-sso-divider{ display:flex; }

/* #2 · SSO/SAML/passkey login buttons stay GHOST (they also carry .login-btn, which
   would otherwise give them the primary accent fill) — surface fill + accent border. */


/* v6 (restored by request): full-bleed ultra-faint per-page icon watermark.
   Behind content; never intercepts clicks; the opaque sidebar covers its left
   strip so it reads as centred in the content area. */
#page-watermark{
  display:block; position:fixed; inset:auto; right:0; bottom:0; z-index:0; pointer-events:none;
}
.app-content{ position:relative; z-index:1; }   /* content above the watermark */
#page-watermark svg{
  width:min(56vw,740px); height:min(56vw,740px);
  transform:translate(20%,20%);
  fill:none; stroke:rgba(150,168,195,.05); stroke-width:1;
}
/* child paths with their own fill="currentColor" would render solid — force
   every descendant stroke-only + faint */
#page-watermark svg *{
  fill:none; stroke:rgba(150,168,195,.05); stroke-width:1;
}
body.light #page-watermark svg, body.light #page-watermark svg *{
  stroke:rgba(40,60,90,.05);
}


/* (v6: the card-gradient consistency sweep is gone — panels are flat
   var(--surface); un-migrated ones keep their base borders.) */

/* v6 · a contrasting text surface inside a card — flat on the page ground
   (the recessed inset shadows are gone) */
.rp-well{
  background:var(--bg); border:1px solid var(--hair); border-radius:7px;
  padding:12px 14px;
}

/* (v6: the recessed input wells are gone — inputs are flat hairline fields.) */

/* v6: devices list/table view sits on a flat design panel like every card */
#devices-container{
  background:var(--surface); border:1px solid var(--hair); border-radius:11px; padding:8px;
}



/* (v6: chamfered selects / rp-ddwrap accent ring / dev-combo chamfer are gone —
   the base select rule draws a flat field with a muted chevron; .rp-ddwrap and
   .dev-combo fall back to their plain base styling.) */

/* (v6: the industrial 30/34px section spacing + its :not(#page-home)
   specificity-trap resets are gone — base card margins own the rhythm.) */

/* button-styled anchors must not be underlined (v6: base, no skin scope) */
a.btn, a.btn-primary, a.btn-secondary, a.btn-icon, a.login-btn{ text-decoration:none; }

/* native <select> open menu honours dark mode (v6: base, no skin scope) */
body{ color-scheme:dark; }
body.light{ color-scheme:light; }
option, optgroup{ background:var(--surface2); color:var(--text); }

/* (v6: the warm-amber chamfered checkboxes are gone — see the base
   input[type="checkbox"] rule with the form controls.) */

/* ════════════════════════════════════════════════════════════════════════
   v5.4.0 "RackMatters": time-tracking + billing (Timesheet + Billing pages,
   ticket "Log hours", invoice print). CSP-safe (no inline styles); sizes fold
   onto the canonical scale {28,16,14,13,12,11}.
   ════════════════════════════════════════════════════════════════════════ */
.ml-auto { margin-left: auto; }
.btn-chip {
  font-size: 12px; padding: 2px 9px; border: 1px solid var(--border);
  background: var(--bg); color: var(--text); border-radius: 4px; cursor: pointer;
}
.btn-chip:hover { border-color: var(--accent); }
.form-group-btn { display: flex; align-items: flex-end; }
.ts-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; }
.ts-day { padding: 10px 12px; }
.ts-day-head { align-items: center; margin-bottom: 6px; }
.ts-entry, .tk-hours-row {
  align-items: center; gap: 6px; padding: 3px 0;
  border-top: 1px solid var(--border); font-size: 13px;
}
.ts-entry:first-child, .tk-hours-row:first-child { border-top: none; }
.ws-totals { display: flex; flex-direction: column; gap: 4px; align-items: flex-end; font-size: 13px; }
.ws-grand { font-size: 16px; border-top: 1px solid var(--border); padding-top: 4px; margin-top: 2px; }
#billing-host .num, #invoice-view-body .num, #ws-result .num { text-align: right; }
.inv-head { display: flex; justify-content: space-between; align-items: flex-start; }
.inv-no { font-size: 16px; font-weight: 600; }
.inv-party { display: flex; justify-content: space-between; gap: 16px; font-size: 13px; }

/* Invoice print: when invoicePrint() adds .printing-invoice to <body>, show only
   the invoice on a forced-light page (mirrors the #print-report approach above). */
@media print {
  body.printing-invoice #app,
  body.printing-invoice .modal-overlay:not(#invoice-view-modal),
  body.printing-invoice #invoice-view-modal .modal-title,
  body.printing-invoice #invoice-view-modal .modal-actions,
  body.printing-invoice #toast-container { display: none !important; }
  body.printing-invoice, body.printing-invoice #invoice-view-modal,
  body.printing-invoice #invoice-view-modal > .modal {
    position: static !important; inset: auto !important; transform: none !important;
    background: #fff !important; color: #111 !important; box-shadow: none !important;
    max-width: none !important; width: auto !important; max-height: none !important;
    overflow: visible !important; display: block !important;
  }
  body.printing-invoice #invoice-print-area,
  body.printing-invoice #invoice-print-area * {
    color: #111 !important; color-scheme: light !important; background: transparent !important;
  }
}

/* ── v5.6.x: narrow-viewport (mobile) audit fixes ─────────────────────────────
   Systematic 390px/768px Playwright audit. Acceptance bar: no horizontal
   document scroll on any page at 390px. All fixes are CSS-only (CSP: no
   inline styles) and reuse the existing breakpoints (720/900). */

/* 1. Grid children must be shrinkable. Grid items default to min-width:auto,
   so ONE wide widget (heat map, health card, ticket rows) forces its 1fr
   track — and the whole dashboard — wider than the viewport. This was the
   root cause of the 819px document width on the home page at 390px. */
.dash-grid > *,
.dash-cols > * { min-width: 0; }

/* 2. Capped lists already scroll vertically; let them scroll horizontally
   too instead of blowing their content out through the card edge. */
.scroll-cap, .scroll-cap-sm, .scroll-cap-lg {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

/* 3. Health widget: the 140px+1fr score/detail grid can't fit at phone
   width; stack it. Its columns must also be shrinkable (same min-width:auto
   trap as #1) so long device names ellipsize instead of widening the card. */
.hh-wrap > * { min-width: 0; }
@media (max-width: 720px) {
  .hh-wrap { grid-template-columns: 1fr; }
}

/* 4. The 4-/5-up stat rows (patches, CVE, summary) only collapse to a single
   column at ≤600px; between 601 and 900px five cards clipped off-screen
   (patches page scrolled to 1040px at a 768px viewport). Step through 2-up
   first. */
@media (max-width: 900px) {
  .isl-134, .isl-136, .isl-139, .isl-142 { grid-template-columns: repeat(2, 1fr); }
}

/* 5. Non-wrapping flex utility rows (toolbars, card headers, inline filter/
   form rows) pushed buttons and inputs past the right edge on the devices,
   monitor, cmdb, reports and patches pages at 390px — and STILL clipped at
   768px, where the docked 240px sidebar leaves only ~500px of content width
   (cmdb scoped-cred row, reports export buttons, patches actions). Let them
   wrap on phone + tablet widths; desktop keeps the single-line layout. */
@media (max-width: 1024px) {
  .row-8, .row-8-mb8, .row-8-center, .section-header { flex-wrap: wrap; }
}

@media (max-width: 720px) {
  /* 6. Devices minimal table: even with the low-priority columns hidden the
     name+status+actions set is ~466px; scroll the wrap instead of the page.
     (The wrap has no overflow on desktop so row menus can pop
     out — on mobile the row menu is drawer-driven, so clipping is fine.) */
  .devices-minimal-wrap {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
  }

  /* 7. Nav tap targets: the drawer nav rows measured 36px and the burger
     shrank to 18px wide (flex squeeze — its width is not a flex minimum).
     40px is the audit bar; real touch devices additionally get the 44px
     pointer:coarse bump below. */
  .sidebar .nav-btn, .sidebar .sidebar-group-toggle { min-height: 40px; }
  .mobile-burger {
    width: 40px;
    min-width: 40px;
    height: 40px;
    flex-shrink: 0;
  }
}

/* 8. Sidebar group toggles were missing from the coarse-pointer tap-target
   bump (nav-btn/btn-icon already get 44px there). */
@media (hover: none) and (pointer: coarse) {
  .sidebar-group-toggle { min-height: 44px; }
}

/* 9. Header touch density (the structural follow-up the 390px audit deferred):
   a.logo (36px), .theme-btn (38×36), .acct-btn (36px) and the #site-health
   pill (~22px) were sub-40px tap targets on phones / touch screens. Grow the
   HIT AREA only — no font growth — to the same 40px audit bar as item 7.
   The header bar cannot grow from this: at ≤720px its flex line is already
   40px tall (the .mobile-burger, item 7), and on coarse-pointer tablets the
   always-present header .btn-icon controls (density / banner toggles) hold a
   44px line — every bump here stays ≤ that. (#site-health is display:none at
   ≤720px; its bump matters on the >720px coarse-pointer band, where the pill
   was a ~22px sliver.) */
@media (max-width: 720px), (hover: none) and (pointer: coarse) {
  a.logo {
    min-height: 40px;
    min-width: 40px;
    align-items: center;   /* .logo is already display:flex */
  }
  .theme-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 40px;
    min-height: 40px;
  }
  .acct-btn { min-height: 40px; }
  .site-health { min-height: 40px; min-width: 40px; justify-content: center; }
}
/* The grown logo/theme targets cost ~6px of row width at 390px, which tipped
   the bar 1px past the viewport: the phone header carried a DOUBLE side
   inset (header 0 10px from the ≤720 compact block + .header-inner's desktop
   0 24px). Fold the inner inset to 12px on phones — 22px effective, and the
   row fits with ~12px to spare. Desktop keeps the 24px. */
@media (max-width: 720px) {
  .header-inner { padding: 0 12px; }
}

/* 10. Devices minimal density → stacked rows on phones (the structural
   follow-up to item 6, which merely let the ~466px-wide name+status+actions
   table PAN inside .devices-minimal-wrap at a 390px viewport). Phones now
   get one stacked block per device instead: line 1 = checkbox + status dot +
   name + actions menu, line 2 = the key columns (hostname / group / OS / IP /
   version / last seen) as a muted 12px strip. CSS-only: each tr becomes a
   wrapping flex line and the tr::after break (flex-basis:100%, order:1)
   splits the two lines; the width-based column-drop rules further up are
   overridden back to visible because the detail strip has room for all of
   them (it wraps onto extra lines when it doesn't). Desktop (>720px) keeps
   the real table untouched, and the >20-row .scrollable-table-wrap height
   cap still applies — the stack scrolls vertically inside it. */
@media (max-width: 720px) {
  .devices-minimal-table,
  .devices-minimal-table tbody { display: block; }
  .devices-minimal-table thead { display: none; }
  .devices-minimal-table tbody tr {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    column-gap: 8px;
    row-gap: 2px;
    padding: 8px 12px;
  }
  /* the line break between the name line (order 0) and the detail strip
     (order 2) — an empty full-width flex item forces the wrap */
  .devices-minimal-table tbody tr::after {
    content: '';
    flex-basis: 100%;
    order: 1;
    height: 0;
  }
  .devices-minimal-table tbody td {
    display: block;
    width: auto;
    padding: 0;
    border: none;
  }
  /* .selected paints per-cell top/bottom borders in table mode — cell
     borders are wrong for the stacked flex line (the row keeps its own
     border-bottom + the .selected background). */
  .devices-minimal-table tbody tr.selected td { border: none; }
  .devices-minimal-table .dev-name-cell {
    flex: 1 1 0;
    min-width: 0;
    text-overflow: ellipsis;
  }
  .devices-minimal-table .dev-actions-cell {
    margin-left: auto;
    padding: 0;
  }
  /* detail strip: re-show what the column-drop rules hid, demote to the
     12px muted role */
  .devices-minimal-table .dev-host-cell,
  .devices-minimal-table .dev-group-cell,
  .devices-minimal-table .dev-os-cell,
  .devices-minimal-table .dev-ip-cell,
  .devices-minimal-table .dev-version-cell,
  .devices-minimal-table .dev-lastseen-cell {
    display: block;
    order: 2;
    font-size: 12px;
    color: var(--muted);
    max-width: 100%;
    text-overflow: ellipsis;
  }
}

/* 11. CMDB scoped-credentials entry row: 5 fixed-width .input-auto fields +
   the Add button ≈ 1104px, which a docked-240px-sidebar desktop only fits at
   a ≥ ~1450px viewport — from 1025px (where item 5's generic ≤1024 wrap
   stops) up to that point the Password field and Add button clipped through
   the card edge. Let THIS row wrap across the whole band; flex-wrap is inert
   once the line fits, so wide desktops render pixel-identically. */
@media (max-width: 1460px) {
  #cmdb-scoped-card .row-8-center { flex-wrap: wrap; }
}

/* ── v5.6.x Settings IA: the 12 flat tabs are clustered into labeled groups
   (Setup / Monitoring / Connections / System). Buttons and ids unchanged —
   the grouping is visual scent only. Scoped to .settings-tab-group so the
   other .settings-tabs bars (billing) are unaffected. Labels use the 11px
   eyebrow role from the canonical type scale. */
.settings-tab-group { display: flex; align-items: center; gap: 0; }
.settings-tab-group + .settings-tab-group { margin-left: 14px; padding-left: 14px;
  border-left: 1px solid var(--border); }
/* Hierarchy flip (user request): the GROUP label is the bold uppercase
   anchor; the tab buttons inside a group drop the sidebar-nav uppercase and
   sit non-bold under it. Scoped to .settings-tab-group so the other
   .settings-tabs bars (billing) keep the original tab styling. */
.settings-tab-group-label { font-size: 11px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .06em; color: var(--text);
  margin-right: 8px; white-space: nowrap; user-select: none; }
.settings-tab-group .settings-tab { text-transform: none; font-weight: 400;
  letter-spacing: 0; }
@media (max-width: 900px) {
  .settings-tabs { flex-wrap: wrap; row-gap: 4px; }
  .settings-tab-group { flex-wrap: wrap; }
  .settings-tab-group + .settings-tab-group { margin-left: 0; padding-left: 0;
    border-left: 0; }
}

/* ── v6.0.0 phase 2: LEFT-NAV SETTINGS (chosen-design .setnav / .sc / .h).
   Desktop only — the horizontal strip above stays the narrow-viewport
   fallback. Scoped to #page-settings; the other .settings-tabs bars
   (billing) keep the strip everywhere. Buttons/ids/deep-links unchanged —
   this is layout + skin, switchSettingsTab still just swaps .active. ── */
@media (min-width: 901px) {
  /* .active-scoped: a bare #page-settings{display:grid} (ID specificity 1,0,0)
     BEAT .page{display:none} (0,1,0), so Settings stayed painted on every
     page after a visit — the "can't leave Settings" bug. */
  #page-settings.active {
    display: grid;
    grid-template-columns: 230px minmax(0, 1fr);
    gap: 0 22px;
    align-items: start;
  }
  #page-settings.active > * { grid-column: 1 / -1; }   /* title/subtitle/search span */
  #page-settings > .settings-tabs {
    grid-column: 1;
    grid-row: 4;
    position: sticky;
    top: 60px;                     /* below the 48px topbar */
    display: block;
    background: var(--surface);
    border: 1px solid var(--hair);
    border-bottom: 1px solid var(--hair);
    border-radius: 11px;
    padding: 8px 6px;
    margin-bottom: 0;
    overflow: visible;
    max-height: calc(100vh - 76px);
    overflow-y: auto;
  }
  #page-settings > .settings-pane { grid-column: 2; grid-row: 4; }
  #page-settings .settings-tab-group { display: block; }
  #page-settings .settings-tab-group + .settings-tab-group {
    margin: 10px 0 0;
    padding: 8px 0 0;
    border-left: none;
    border-top: 1px solid var(--hair-2);
  }
  /* group label → the design-language mono eyebrow (chosen-design .h) */
  #page-settings .settings-tab-group-label {
    display: block;
    font-family: var(--font-mono);
    font-size: 9.5px;
    font-weight: 400;
    letter-spacing: .14em;
    color: var(--muted);
    padding: 6px 10px 5px;
    margin-right: 0;
  }
  /* tab → the design's category row (chosen-design .sc) */
  #page-settings .settings-tab {
    display: block;
    width: 100%;
    text-align: left;
    padding: 7px 10px;
    border-radius: 8px;
    font-size: 13px;
    color: var(--text);
  }
  #page-settings .settings-tab:hover { background: var(--hair-2); color: var(--text); }
  #page-settings .settings-tab.active {
    background: var(--accent-soft);
    color: var(--text);
    font-weight: 560;
  }
}

/* W5-5: geographic site map (NOC world view) */
.site-map-wrap { width: 100%; max-width: 720px; margin: 8px auto 0; }
#site-map-svg { width: 100%; height: auto; display: block; border-radius: 8px; }
.site-map-bg { fill: var(--bg-elev, #0d1117); }
.site-map-land path { fill: var(--surface-2, #2a3547); fill-rule: evenodd; stroke: var(--hair, #3a4557); stroke-width: 0.15; stroke-linejoin: round; shape-rendering: geometricPrecision; }
.site-map-grat line { stroke: var(--border, #2b3543); stroke-width: 0.3; stroke-dasharray: 2 2; opacity: 0.55; }
/* v6.0.1 (item 6): equator + prime meridian drawn a touch stronger for reference */
.site-map-grat line.grat-eq { stroke-width: 0.4; stroke-dasharray: none; opacity: 0.45; }
.site-map-dot { cursor: pointer; stroke: #0d1117; stroke-width: 0.6; }
.site-map-dot.ok { fill: var(--c-green, #3fb950); }
.site-map-dot.degraded { fill: var(--c-amber, #d29922); }
.site-map-dot.down { fill: var(--c-red, #f85149); }
.site-map-dot:hover { stroke: var(--accent, #58a6ff); stroke-width: 1; }

/* W5-3: rack elevation view */
/* One 18px row per rack unit, so a 42U rack is 756px and a 48U rack 864px —
   measured at 758px here. That is a variable-row panel by the project's rule,
   and the cap goes on the row of columns rather than on #rack-elevation-wrap so
   the rack name and the overlap warning stay put while the units scroll.
   340px is .scroll-cap's value; the column is column-reverse, so it opens
   showing U1 at the bottom, which is where an operator looks first. */
.rack-elev { display: flex; gap: 16px; flex-wrap: wrap; max-height: 340px; overflow-y: auto; }
.rack-elev-col { display: flex; flex-direction: column-reverse; width: 220px;
  border: 1px solid var(--border, #2b3543); border-radius: 6px; overflow: hidden; }
.rack-u { position: relative; height: 18px; border-top: 1px solid var(--border, #2b3543);
  font-size: 10px; color: var(--text-dim, #7d8590); padding-left: 4px; line-height: 18px; }
.rack-u:first-child { border-top: 0; }
.rack-asset { position: absolute; left: 22px; right: 2px; border-radius: 3px;
  background: var(--surface-2, #1e2530); border: 1px solid var(--accent, #58a6ff);
  color: var(--text, #e6edf3); font-size: 11px; line-height: 1.1; padding: 2px 4px;
  overflow: hidden; z-index: 1; cursor: default; }
.rack-asset.conflict { border-color: var(--c-red, #f85149); background: rgba(248,81,73,0.15); }
.rack-u-num { position: absolute; left: 2px; top: 0; width: 18px; text-align: right; }

/* ── v6.1.2 (#43): kiosk mode ────────────────────────────────────────────────
   For the wall-mounted rack tablet / the NOC board on the second monitor. Hides
   every navigation affordance and gives the content the full viewport. It is a
   DISPLAY mode, not a security boundary: the API still enforces the token's role,
   so a kiosk left on a viewer token is read-only because the ROLE says so, not
   because the buttons are hidden. Toggle with ?kiosk=1 or the Esc-able button. */
body.kiosk .sidebar,
body.kiosk header,
body.kiosk #rp-topbar { display: none !important; }
body.kiosk .app-content { margin-left: 0 !important; }
body.kiosk .container { padding-top: 12px; }
/* The exit affordance must survive: a kiosk you cannot leave without knowing the
   URL trick is a trap, especially on a touch screen with no keyboard. */
.kiosk-exit { display: none; }
body.kiosk .kiosk-exit {
  display: flex; align-items: center; gap: 6px;
  position: fixed; top: 10px; right: 12px; z-index: 200;
  opacity: 0.25; transition: opacity 120ms ease;
}
body.kiosk .kiosk-exit:hover,
body.kiosk .kiosk-exit:focus-within { opacity: 1; }

/* ── v7.0.2: alert wall ──────────────────────────────────────────────────────
   The Alert inbox card, full screen and nothing else — a spare monitor or a
   wall panel. Kiosk shows a whole PAGE and can cycle several; this shows one
   card.

   The fullscreen request goes on the DOCUMENT ROOT, not on the card. A
   fullscreen element paints above everything outside it, and every
   .modal-overlay here is a body-level sibling of #app — so fullscreening the
   card alone hides the ack-with-note dialog, the mute dialog and every toast
   behind it. Promoting the card with a class keeps them inside the fullscreen
   root. Same reason kiosk does it this way.

   A DISPLAY mode, not a security boundary: the API still enforces the token's
   role. */
body.alertwall .sidebar,
body.alertwall header,
body.alertwall #rp-topbar,
body.alertwall .refresh-bar { display: none !important; }
body.alertwall .app-content { margin-left: 0 !important; }
body.alertwall .container { padding: 0; max-width: none; }
/* The wall covers the viewport, so the page behind it must not keep a live
   scrollbar — it renders as a dead strip down the right edge and the card
   measures 1910 instead of 1920 on a 1920 screen. `html` reserves that gutter
   permanently via scrollbar-gutter: stable, so hiding body's overflow is not
   enough on its own; the gutter has to be released as well. */
body.alertwall { overflow: hidden; }
html:has(body.alertwall) { scrollbar-gutter: auto; }

/* Every page except alerts, and everything on the alerts page except the inbox
   card itself — the page title, the subtitle and its doc link are chrome here.

   The alerts page has THREE sibling .dash-cards: the inbox, the MTTR timeline
   and "What happened last time". Promoting `#page-alerts > .dash-card` made all
   three position:fixed inset:0, stacked — and the LAST in DOM order paints on
   top, so the wall opened on "What happened last time". Reported from the field
   within the hour. The rendered test had one card in its fixture and passed. */
body.alertwall .page:not(#page-alerts) { display: none !important; }
body.alertwall #page-alerts > .dash-card:not(#alerts-inbox-card) { display: none !important; }

/* The page-in animation (`rp-page-in`) puts a transform on .page.active, and a
   transformed ancestor becomes the CONTAINING BLOCK for its position:fixed
   descendants. So while that 140ms animation runs, the card below sizes itself
   against #page-alerts — 30px tall mid-flight — instead of the viewport, and
   the wall opens as a sliver. Measured: with the animation settled the card is
   1920x1080; caught mid-animation it is 1910x30, and which one you get depends
   on how fast the machine is.
   No transform here, so `fixed` means the viewport again. */
body.alertwall #page-alerts { animation: none !important; transform: none !important; }
body.alertwall #page-alerts > .page-title,
body.alertwall #page-alerts > .page-subtitle { display: none !important; }

/* The card becomes the viewport. It is a flex column so the table takes the
   space the controls do not, and scrolls inside instead of running off the
   bottom of a screen nobody is sitting at. */
body.alertwall #alerts-inbox-card {
  position: fixed; inset: 0;
  margin: 0; border: 0; border-radius: 0;
  display: flex; flex-direction: column;
  padding: 18px 22px 12px;
  background: var(--surface);
}
/* The table takes whatever height the controls above it do not, and scrolls
   inside rather than running off the bottom of a screen nobody is sitting at.
   `max-height: none` overrides the 480px .table-card cap, which is right for a
   card among others and wrong for the only thing on the screen.
   No min-height:0 here: a flex item's automatic minimum size is zero once its
   overflow is not `visible`, which `overflow-y: auto` on this same rule already
   guarantees. Adding it changed nothing measurable — a declaration whose
   comment claims it is load-bearing and is not costs more than it saves. */
body.alertwall #alerts-inbox-card > .table-card,
body.alertwall #alerts-inbox-card > .scrollable-table-wrap {
  flex: 1 1 auto; max-height: none; overflow-y: auto;
}

/* Readable from across a room. Only inside the wall — the document-wide 13px
   body size and the 13px .section-title stand everywhere else. */
body.alertwall #alerts-inbox-card > .section-title { font-size: 19px; }
/* 16px, not 15: the canonical scale has no 15px stop and the gate says so.
   16 is the modal/empty-title size and reads better at distance anyway. */
body.alertwall #page-alerts table { font-size: 16px; }
body.alertwall #page-alerts th { font-size: 13px; }
body.alertwall #page-alerts td { padding-top: 9px; padding-bottom: 9px; }

/* Age of the inbox on screen. Left corner, opposite the exit button, and
   always visible — unlike the exit control it is information, not chrome, so it
   does not fade until hover. */
.alertwall-stamp { display: none; }
body.alertwall .alertwall-stamp {
  display: block;
  position: fixed; top: 12px; left: 16px; z-index: 200;
  font-family: var(--font-mono); font-size: 11px; color: var(--muted);
}
body.alertwall .alertwall-stamp.stale { color: var(--red); }

.alertwall-exit { display: none; }
body.alertwall .alertwall-exit {
  display: flex; align-items: center; gap: 6px;
  position: fixed; top: 10px; right: 12px; z-index: 200;
  opacity: 0.25; transition: opacity 120ms ease;
}
body.alertwall .alertwall-exit:hover,
body.alertwall .alertwall-exit:focus-within { opacity: 1; }

/* v6.3.1: log-alert evidence. The matched line IS the alert — showing only the
   rule's regex tells the operator nothing they didn't already write. Wraps
   rather than truncating mid-token, and stays clearly secondary to the title. */
.alert-evidence {
  margin-top: 3px;
  color: var(--muted);
  word-break: break-word;
  max-width: 60ch;
}
/* The clear-a-line modal shows the raw line and its normalized form. Both are
   untrusted log text, so they get a bounded, scrollable, monospaced well. */
.code-block {
  background: var(--surface2);
  border: 1px solid var(--hair);
  border-radius: 8px;
  padding: 8px 10px;
  margin: 4px 0 10px;
  white-space: pre-wrap;
  word-break: break-word;
}

/* v6.3.1: baseline-picker "already applied" note. The picker used to be
   write-only — with dozens of templates and several scopes there was no way to
   read the current coverage back out. */
.bc-row-applied {
  display: block;
  margin-top: 3px;
  font-size: 11px;
  color: var(--green, var(--muted));
}
.bc-row-applied svg { vertical-align: -2px; margin-right: 2px; }
/* v6.4.0: inline (non-flex) icon+text elements — pills, badges and action
   links — otherwise render the Lucide SVG on the text baseline (sitting low).
   Nudge them to optical centre, matching the .bc-row-applied idiom above. */
.patch-badge svg, .hw-pill svg, .compliance-fix svg, .meta-label svg, .isl-708 svg { vertical-align: -2px; }

/* ── v6.3.1: Security Advisory ──────────────────────────────────────────────
   Order is the product here: the list is already sorted by what to fix first,
   so the rank number is prominent and nothing invites re-sorting. */
.adv-summary {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 12px;
}
.adv-tile {
  background: var(--surface);
  border: 1px solid var(--hair);
  border-radius: 11px;
  padding: 14px 16px;
}
.adv-tile-val { font-size: 28px; font-weight: 700; line-height: 1; }
.adv-tile-lbl { font-size: 12px; color: var(--muted); margin-top: 4px; }
.adv-t-crit .adv-tile-val { color: var(--red); }
.adv-t-high .adv-tile-val { color: var(--orange, var(--red)); }

/* .ta-center and .ta-right existed; .ta-left did not, so an override of a
   centred block (the scan "no findings" explanation) silently did nothing. */
.ta-left { text-align: left; }

/* The advisory card IS a .dash-card — it just carries a severity stripe.
   v6.3.1: the bare stripe read as the generic "AI card" look — severity cards
   now also carry a faint severity tint + tinted hairline so red/amber cards are
   distinct at a glance. Flat surfaces only (no gradients); browsers without
   color-mix keep the stripe-only fallback. */
.adv-card { margin-bottom: 12px; border-left: 3px solid var(--hair-2, var(--hair)); }
.adv-sev-critical { border-left-color: var(--red); }
.adv-sev-high     { border-left-color: var(--orange, var(--red)); }
.adv-sev-medium   { border-left-color: var(--amber, var(--muted)); }
@supports (background: color-mix(in srgb, red 6%, white)) {
  .adv-sev-critical {
    background: color-mix(in srgb, var(--red) 6%, var(--surface));
    border-color: color-mix(in srgb, var(--red) 26%, var(--hair));
    border-left-color: var(--red);
  }
  .adv-sev-high {
    background: color-mix(in srgb, var(--orange, var(--red)) 6%, var(--surface));
    border-color: color-mix(in srgb, var(--orange, var(--red)) 26%, var(--hair));
    border-left-color: var(--orange, var(--red));
  }
  .adv-sev-medium {
    background: color-mix(in srgb, var(--amber, var(--muted)) 6%, var(--surface));
    border-color: color-mix(in srgb, var(--amber, var(--muted)) 24%, var(--hair));
    border-left-color: var(--amber, var(--muted));
  }
}
.adv-head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 6px;
}
.adv-rank {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--muted);
  min-width: 1.5em;
}
.adv-title { flex: 1 1 240px; }   /* v6.4.1: sizing comes from .section-title */
.adv-hosts { margin-left: auto; }
/* v6.4.1: both are BODY copy (the 'why it matters' sentence and the
   remediation instruction) — they inherit the 13px anchor now. */
.adv-why { color: var(--muted); margin-bottom: 6px; }
.adv-fix { margin-bottom: 6px; }
.adv-ev ul { margin: 2px 0 6px; padding-left: 18px; }
.adv-foot { font-size: 12px; }

/* v6.3.1: collapsed, filterable host list. A scope can fan out across a whole
   fleet, so the COUNT is the headline and the names open on demand — inlining
   them flooded every catalog row. */
.host-list { margin-top: 4px; }
.host-list > summary {
  cursor: pointer;
  font-size: 12px;
  color: var(--muted);
  list-style: none;
}
.host-list > summary::-webkit-details-marker { display: none; }
.host-list > summary::before {
  content: '▸ ';
  display: inline-block;
  transition: transform .12s ease;
}
.host-list[open] > summary::before { content: '▾ '; }
.host-list-filter {
  margin: 6px 0 4px;
  width: 100%;
  max-width: 280px;
  font-size: 13px;
}
.host-list-items { font-size: 12px; }
.host-list-item {
  padding: 2px 0;
  font-family: var(--font-mono);
  color: var(--muted);
}

/* v6.3.1: six utilities that were USED but never defined, so they silently
   rendered as nothing — a `c-success` badge came out in body colour, a `p-6`
   box had no padding. Found by the utility-class guardrail
   (tests/test_css_utilities.py), which now requires this set to stay empty. */
.c-success { color: var(--green); }
.c-warning { color: var(--amber); }
.flex-wrap { flex-wrap: wrap; }
.mt-0 { margin-top: 0; }
.mb-0 { margin-bottom: 0; }
.p-6 { padding: 6px; }

/* ── v6.4.0: KMIP page ─────────────────────────────────────────────────── */
/* Server-status key/value grid: label column sized to content, value fills. */
.kmip-status-grid {
  display: grid;
  grid-template-columns: minmax(120px, max-content) 1fr;
  gap: 6px 16px;
  align-items: baseline;
  font-size: 13px;
}
/* Per-appliance setup steps in the client wizard. */
.kmip-steps { margin: 8px 0 0; padding-left: 20px; }
.kmip-steps li { margin-bottom: 8px; line-height: 1.55; }

/* v6.4.2: enrollment confirmation line in the Enroll-device modal. Empty until
   a token is minted, then live status while we wait for the host's first
   check-in. role="status" + aria-live so a screen reader hears the result — the
   whole point is that the operator should not have to guess. */
.enroll-watch:empty { display: none; }
.enroll-watch {
  margin-top: 10px;
  padding: 8px 10px;
  border: 1px solid var(--hair);
  border-radius: 8px;
  background: var(--surface2);
  font-size: 13px;
  line-height: 1.45;
}


/* ── v6.4.2: cross-page "Related" chips on the page subtitle ─────────────────
   Thirty pages had zero inbound link from anywhere else in the app. These sit
   at the end of the .page-subtitle line, alongside the Documentation link that
   77 of 78 pages already carry, so no page grows a new row. */
.rel-pages { display: inline-flex; align-items: center; flex-wrap: wrap;
             gap: 6px; margin-left: 10px; }
.rel-label { font-size: 11px; color: var(--muted); text-transform: uppercase;
             letter-spacing: .04em; }
.rel-chip { font-size: 12px; line-height: 1; padding: 3px 8px; border-radius: 999px;
            border: 1px solid var(--hair); color: var(--text); text-decoration: none;
            background: var(--surface-2, transparent); white-space: nowrap; }
/* --accent-ink, not --accent: the fill accent fails contrast as text on the
   surface, which tests/test_v642_css.py enforces across every theme x accent
   pair. The BORDER may use the fill colour; the label may not. */
.rel-chip:hover, .rel-chip:focus-visible { border-color: var(--accent);
                                           color: var(--accent-ink); }
