/* Sea Direct Compass — customer portal theme, "Chartroom" language.

   ── THE FONT POLICY ──────────────────────────────────────────────────────
   Three faces, assigned by what a piece of text IS, never by where it sits.
   Mixing by role reads as considered; mixing by location reads as indecision.

     --f-display   Archivo          Names and headings. Anything that titles
                                    something: page titles, panel headings,
                                    capability and product names, step names,
                                    the brand, the actor and object in a log
                                    line. Industrial grotesque — it gives the
                                    page a spine the softer face cannot.

     --f-text      Instrument Sans  Language. Anything read as a sentence:
                                    body copy, hints, state words, nav links,
                                    buttons, table cells, form labels.

     --f-data      JetBrains Mono   Measurements. Counts, ratios, deltas,
                                    elapsed time, timestamps, IDs, keyboard
                                    hints. Tabular, so a changing figure does
                                    not shift its neighbours.

   Field labels are the deliberate hybrid: display face, 11px, 500, .045em
   tracking, sentence case — an engraved field name rather than a shouted one.
   Vendored under /assets/fonts, loaded by /assets/css/fonts.css: the CSP is
   first-party only and a test enforces it.

   ── SURFACES ─────────────────────────────────────────────────────────────
   Panels, not glass. A 1px border and a soft shadow, no backdrop blur — the
   blur made every card its own stacking context, which is what kept putting
   dropdowns underneath their own siblings. `.glass-card` keeps its name
   because ~200 templates say it; it is a panel now.

   Dual theme: dark by default, light via [data-theme="light"]. Matches
   next_v2_landing's token block — the two are hand-maintained twins.        */

/* TWIN: assets/css/tokens.css copies these palette values (dark and light) for
   the standalone Bootstrap pages; change both, and bump the tokens.css ?v= in
   deploy.css plus deploy.css's ?v= in the templates that link it. */
:root {
  --f-display: "Archivo", system-ui, -apple-system, "Segoe UI", sans-serif;
  --f-text: "Instrument Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
  --f-data: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;

  --primary: #3d9dff;
  --primary-2: #7cbcff;
  --success: #2ecf9a;
  --danger: #ff6478;
  --warning: #edb339;

  --bg-0: #06090f;
  --bg-1: #0b1018;
  --ink: #e9eef8;
  --ink-muted: #909db5;
  /* 5.47:1 on a panel. The old #64718a was 3.88 and failed AA on every
     table header and field label. */
  --ink-faint: #7d8aa2;

  /* Interaction tokens. `--primary` is a brand fill tuned for glows and
     meters; white text on it is 2.8:1 in dark, so text and buttons get their
     own values. */
  --link: #7cbcff;
  --focus-ring: #7cbcff;
  --input-border: rgba(255, 255, 255, .34);
  --btn-primary-bg: #1a73d1;
  --btn-primary-bg-hover: #1766bb;
  --on-primary: #ffffff;
  /* Text in the section colour (`--accent` is set inline per section).
     Mixing partner and share change per theme; see the
     `[style*="--accent"]` rule that re-resolves it where an accent is set. */
  --accent-ink-mix: #ffffff;
  --accent-ink-pct: 70%;
  --accent-ink: color-mix(in srgb, var(--accent, var(--primary)) var(--accent-ink-pct), var(--accent-ink-mix));

  /* The one severity ramp. Medium is the warning hue, high is orange,
     critical is red; blue never means severity, only info or brand. Each has a
     wash for pill and row backgrounds. */
  --sev-critical: var(--chip-crit-fg);
  --sev-high: #ff8a4c;
  --sev-medium: var(--chip-warn-fg);
  --sev-low: var(--chip-neutral-fg);
  --sev-info: var(--chip-brand-fg);
  --sev-wash: 13%;
  --sev-critical-wash: color-mix(in srgb, var(--sev-critical) var(--sev-wash), transparent);
  --sev-high-wash: color-mix(in srgb, var(--sev-high) var(--sev-wash), transparent);
  --sev-medium-wash: color-mix(in srgb, var(--sev-medium) var(--sev-wash), transparent);
  --sev-low-wash: color-mix(in srgb, var(--sev-low) var(--sev-wash), transparent);
  --sev-info-wash: color-mix(in srgb, var(--sev-info) var(--sev-wash), transparent);

  /* Panel surfaces. `--glass*` keep their names because the templates do. */
  --glass: #0b1018;
  --glass-strong: #141a26;
  --glass-border: rgba(255, 255, 255, .088);
  --line-soft: rgba(255, 255, 255, .05);
  --grid: rgba(255, 255, 255, .022);

  /* Tinted chips: an icon sits on a wash of its own colour, so a row of tiles
     reads as one family instead of four traffic lights. `-fg` is lifted off the
     base token because the base was chosen for text on the page ground, not for
     a 13% wash of itself. */
  --chip-brand-bg: rgba(61, 157, 255, .13);
  --chip-brand-fg: #3d9dff;
  --chip-ok-bg: rgba(46, 207, 154, .12);
  --chip-ok-fg: #2ecf9a;
  --chip-warn-bg: rgba(237, 179, 57, .13);
  --chip-warn-fg: #edb339;
  --chip-crit-bg: rgba(255, 100, 120, .13);
  --chip-crit-fg: #ff6478;
  --chip-neutral-bg: rgba(255, 255, 255, .06);
  --chip-neutral-fg: #909db5;

  --sidebar-bg: #0b1018;
  --header-bg: rgba(6, 9, 15, .84);
  --input-bg: #0e131d;
  --pop-bg: #0e131d;
  --hover-bg: rgba(255, 255, 255, .055);
  --row-border: rgba(255, 255, 255, .05);
  --track: rgba(255, 255, 255, .08);
  --card-sheen: rgba(255, 255, 255, .048);
  /* Elevations. 1 rests a panel on the page, 2 lifts a hovered tile or an
     open popover, 3 floats a dialog or drawer over everything. Per theme. */
  --shadow-1: 0 12px 32px -20px rgba(0, 0, 0, .9);
  --shadow-2: 0 16px 40px rgba(0, 0, 0, .3);
  --shadow-3: 0 24px 60px rgba(0, 0, 0, .5);
  --card-shadow: var(--shadow-1);
  --watermark-opacity: .035;

  /* ── SCALES ── the same in both themes, so only here.
     Type: 1rem is the 14.5px body, so the steps are px to stay exact. Nothing
     sits under --fs-xs; a literal font size in this sheet fails
     tests/css_scales.rs. Weights are 400, 500, 600 and 700 only. */
  --fs-xs: 11px;      /* field labels, eyebrows, badges, table meta */
  --fs-sm: 12px;      /* hints, small buttons, chips, secondary cells */
  --fs-md: 13px;      /* buttons, form labels, table cells, nav */
  --fs-base: 14.5px;  /* body copy */
  --fs-lg: 16px;      /* lead text, card titles, inputs on phones */
  --fs-xl: 18px;      /* section headings */
  --fs-2xl: 21px;     /* panel headings */
  --fs-3xl: 24px;     /* page titles */
  --fs-4xl: 28px;     /* stat figures */
  --fs-figure: 32px;  /* hero figures */
  --lh-tight: 1.2;    /* headings and figures */
  --lh-snug: 1.4;     /* short wrapped labels, list items */
  --lh-body: 1.55;    /* sentences */

  /* Radius: inputs and buttons share --r-sm so a field and its button line
     up; panels, popovers and dialogs take --r-md. Marks under 4px (a 2px LED
     or tick) keep their literal. `--radius*` are the old names. */
  --r-xs: 4px;
  --r-sm: 8px;
  --r-md: 12px;
  --r-pill: 999px;
  --radius: var(--r-md);
  --radius-sm: var(--r-sm);

  /* Spacing, first used for card padding and stack gaps. */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 24px;
  --sp-6: 32px;
}

/* ---------- light theme ---------- */
:root[data-theme="light"] {
  --primary: #0a63c0;
  --primary-2: #2f8ae0;
  --success: #0a8a5f;
  --danger: #c81f37;
  --warning: #96620a;

  --bg-0: #f2f5f9;
  --bg-1: #ffffff;
  --ink: #0f1828;
  --ink-muted: #4c5b74;
  /* 5.17:1 on white, 4.73 on the page ground (was #7a8aa3, 3.50). */
  --ink-faint: #5f6e86;

  --link: #0a63c0;
  --focus-ring: #0a63c0;
  --input-border: rgba(14, 26, 48, .5);
  --btn-primary-bg: #0a63c0;
  --btn-primary-bg-hover: #084f9a;
  --on-primary: #ffffff;
  --accent-ink-mix: #000000;
  --accent-ink-pct: 72%;

  /* Light high is a shade under the agreed #b4470f so it still clears 4.5:1
     on the page ground and on `--glass-strong`. The lighter wash keeps the
     same headroom for every step. */
  --sev-high: #a8420e;
  --sev-wash: 9%;

  --glass: #ffffff;
  --glass-strong: #eaeff6;
  --glass-border: rgba(14, 26, 48, .105);
  --line-soft: rgba(14, 26, 48, .06);
  --grid: rgba(14, 26, 48, .03);

  --chip-brand-bg: rgba(10, 99, 192, .09);
  --chip-brand-fg: #0a63c0;
  --chip-ok-bg: rgba(10, 138, 95, .10);
  --chip-ok-fg: #077a53;
  --chip-warn-bg: rgba(150, 98, 10, .12);
  --chip-warn-fg: #8a5a05;
  --chip-crit-bg: rgba(200, 31, 55, .09);
  --chip-crit-fg: #b81c32;
  --chip-neutral-bg: rgba(14, 26, 48, .05);
  --chip-neutral-fg: #4c5b74;

  --sidebar-bg: #ffffff;
  --header-bg: rgba(242, 245, 249, .84);
  --input-bg: #ffffff;
  --pop-bg: #ffffff;
  --hover-bg: rgba(14, 26, 48, .045);
  --row-border: rgba(14, 26, 48, .06);
  --track: rgba(14, 26, 48, .08);
  --card-sheen: rgba(255, 255, 255, .9);
  /* Light elevations are softer and ink-tinted: a black drop reads as dirt
     on white. */
  --shadow-1: 0 10px 28px -18px rgba(14, 26, 48, .38);
  --shadow-2: 0 14px 34px -10px rgba(14, 26, 48, .22);
  --shadow-3: 0 24px 56px -12px rgba(14, 26, 48, .3);
  --watermark-opacity: .04;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg-0);
  color: var(--ink);
  font-family: var(--f-text);
  font-size: var(--fs-base);
  line-height: var(--lh-body);
  min-height: 100vh;
}
/* Headings are names, so they take the display face wherever they appear. */
h1, h2, h3, h4, h5, h6 { font-family: var(--f-display); letter-spacing: -.025em; text-wrap: balance; }
/* Anything measured. Apply to a figure, never to a sentence. */
.num, .mono { font-family: var(--f-data); font-variant-numeric: tabular-nums; letter-spacing: -.02em; }
/* Two label voices, no third. `.lbl` names a field or a figure, in sentence
   case ("Open incidents"). `.eyebrow` is the uppercase kicker over a section
   or group ("NIS2 POSTURE"). Older rules that play either part share these
   declarations; each keeps only its own colour and spacing below. */
.lbl, .meta-grid dt, .host-fact dt, .reg-record dt, .kpi-hero-label { font-family: var(--f-display); font-size: var(--fs-xs); font-weight: 500; letter-spacing: .045em; color: var(--ink-faint); }
.eyebrow, .dash-kicker, .dash-sub, .cap-kind, .dchart-title, .reg-group-title,
.ex-todo h4, .ex-records-h, .cmdk-section {
  font-family: var(--f-display); font-size: var(--fs-xs); font-weight: 600;
  letter-spacing: .06em; text-transform: uppercase; color: var(--ink-faint);
}
html { color-scheme: dark; transition: background-color .35s ease, color .35s ease; }
html[data-theme="light"] { color-scheme: light; }

/* `--link`, not a light-only `a` override: `:root[data-theme] a` would outrank
   `.btn-primary` and `.nav-link` and paint blue text on blue buttons. */
a { color: var(--link); text-decoration: none; }
a:hover { text-decoration: underline; }
h1 { font-size: var(--fs-3xl); margin: 0 0 .4rem; font-weight: 700; line-height: var(--lh-tight); }
h3 { margin: 0 0 .7rem; font-weight: 600; }
code { background: var(--glass); padding: .15em .4em; border-radius: var(--r-sm); font-size: .9em; word-break: break-all; }
.muted { color: var(--ink-muted); }
.small { font-size: var(--fs-sm); }

.aurora {
  position: fixed; inset: 0; z-index: -1; pointer-events: none;
  /* A drawn grid under a single brand light, faded out below the fold. The old
     three-blob aurora made every page look like weather; this reads as the
     chart table the panels sit on. */
  background:
    radial-gradient(88vw 44vh at 26% -16%, var(--chip-brand-bg), transparent 64%),
    linear-gradient(180deg, transparent, var(--bg-0) 74%),
    repeating-linear-gradient(0deg, var(--grid) 0 1px, transparent 1px 72px),
    repeating-linear-gradient(90deg, var(--grid) 0 1px, transparent 1px 72px),
    var(--bg-0);
  transition: background .35s ease;
}

/* ---- panels + buttons ---- */
/* Kept the name because ~200 templates use it; it is a panel now. The
   backdrop-filter is gone on purpose — it made every card its own stacking
   context, which is why dropdowns kept painting underneath their own siblings
   and why `.dbd-head` and `.welcome` needed z-index rescues. */
.glass-card {
  background: var(--glass);
  border: 1px solid var(--glass-border);
  border-radius: var(--r-md);
  box-shadow: inset 0 1px 0 var(--card-sheen), var(--shadow-1);
}

.btn {
  display: inline-flex; align-items: center; gap: var(--sp-2); cursor: pointer;
  padding: .55rem 1rem; border-radius: var(--r-sm);
  border: 1px solid var(--glass-border);
  background: var(--glass); color: var(--ink);
  font-family: var(--f-text); font-size: var(--fs-md); font-weight: 600; text-align: center;
  letter-spacing: -.01em;
  transition: transform .08s ease, background .15s ease, box-shadow .15s ease, border-color .15s ease;
}
.btn:hover { text-decoration: none; background: var(--hover-bg); transform: translateY(-1px); }
.btn:active { transform: translateY(1px); }
.btn-primary {
  background: var(--btn-primary-bg); border-color: transparent; color: var(--on-primary);
  box-shadow: var(--shadow-1);
}
/* Hover goes darker, never lighter, so the label's contrast only rises. */
.btn-primary:hover { background: var(--btn-primary-bg-hover); }
.btn-ghost { background: transparent; box-shadow: none; }
.btn-sm { padding: .32rem .65rem; font-size: var(--fs-sm); border-radius: var(--r-sm); }
/* Read-only "ghost" of an action the plan doesn't permit. Managed · vCISO
   customers see every option greyed out (their MSP acts on their behalf) rather
   than the option vanishing. Covers our span twins, natively-disabled buttons,
   and any control inside a disabled <fieldset>. */
.btn.is-disabled, .btn:disabled, fieldset[disabled] .btn {
  opacity: .45; cursor: not-allowed; pointer-events: none;
  box-shadow: none; transform: none;
}
/* Layout-neutral fieldset used purely to make a whole create form read-only
   (disabled) for Managed customers without altering its spacing. */
.ro-fieldset { border: 0; margin: 0; padding: 0; min-width: 0; }

/* ---- forms ---- */
.stack { display: flex; flex-direction: column; gap: var(--sp-3); }
label { display: flex; flex-direction: column; gap: var(--sp-1); font-weight: 500; font-size: var(--fs-md); color: var(--ink-muted); }
input[type=text], input[type=email], input[type=password], input[type=number],
input[type=date], input[type=datetime-local], input[type=time], input[type=tel],
input[type=url], input[type=search], textarea, select {
  width: 100%; padding: .65rem .8rem;
  background: var(--input-bg); color: var(--ink);
  border: 1px solid var(--input-border); border-radius: var(--r-sm);
  font: inherit; transition: border-color .15s ease, box-shadow .15s ease;
}
/* Prose by default (descriptions, notes, rationales); a field that holds code or
   one-address-per-line lists opts into the `.mono` face. */
textarea { resize: vertical; font-family: inherit; }
/* A tick box belongs *beside* its text, not under it: `.check` / `.check-row`
   opt a label out of the column layout above. `flex-start` keeps the box on the
   first line when the text wraps. */
label.check, label.check-row {
  flex-direction: row; align-items: flex-start; gap: var(--sp-2);
  color: var(--ink); cursor: pointer;
}
label.check input, label.check-row input {
  flex: none; width: 1.05rem; height: 1.05rem; margin: .08rem 0 0;
  accent-color: var(--primary-2); cursor: pointer;
}
/* Several tick boxes in a row read as one group, not as separate fields. */
.check-group { display: flex; flex-direction: column; gap: var(--sp-2); }
/* File pickers are the one control browsers will not let us restyle wholesale;
   at least give the surrounding field the portal's box. */
input[type=file] {
  width: 100%; padding: .55rem .8rem;
  background: var(--input-bg); color: var(--ink-muted);
  border: 1px dashed var(--input-border); border-radius: var(--r-sm); font: inherit;
}
input[type=file]::file-selector-button {
  margin-right: .7rem; padding: .35rem .7rem; border-radius: var(--r-sm); cursor: pointer;
  border: 1px solid var(--glass-border); background: var(--glass); color: var(--ink); font: inherit;
}
input:focus, textarea:focus, select:focus { border-color: var(--primary-2); box-shadow: 0 0 0 3px color-mix(in srgb, var(--focus-ring) 28%, transparent); }
/* Keyboard focus on a field is a real ring, not only the 16% glow: that
   measured 1.18:1 and was invisible in light. */
input:focus-visible, textarea:focus-visible, select:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.btn-danger { background: color-mix(in srgb, var(--danger) 16%, transparent); border-color: color-mix(in srgb, var(--danger) 40%, transparent); color: var(--danger); }
.btn-danger:hover { background: color-mix(in srgb, var(--danger) 28%, transparent); }
/* Row-level destructive action in a list (Delete on every register row): quiet
   until hovered so a column of them does not out-shout the row that needs
   review. The confirm step keeps the solid .btn-danger. */
.btn-quiet-danger { background: transparent; border-color: transparent; box-shadow: none; color: var(--ink-faint); }
.btn-quiet-danger:hover, .btn-quiet-danger:focus-visible { background: color-mix(in srgb, var(--danger) 12%, transparent); border-color: color-mix(in srgb, var(--danger) 40%, transparent); color: var(--danger); }
/* Caution action (Ban device in the quick actions): amber, one step below
   danger. Undo actions (Unblock, Unban, Release) use .btn-ghost. */
.btn-warn { background: var(--chip-warn-bg); border-color: color-mix(in srgb, var(--chip-warn-fg) 40%, transparent); color: var(--chip-warn-fg); }
.btn-warn:hover { background: color-mix(in srgb, var(--chip-warn-fg) 16%, transparent); }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-3); }
@media (max-width: 620px) { .grid-2 { grid-template-columns: 1fr; } }
/* A row of buttons/controls that must not run into each other. */
.row-gap { display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-2); }
/* Small utilities templates already reach for by name. */
.f-w-600 { font-weight: 600; }
.mt-1 { margin-top: .4rem; }
.mb-0 { margin-bottom: 0; }
.ta-center { text-align: center; }
.field-help { margin: -.1rem 0 0; }
/* Register form (app/register/form.html): a box group reads as one field with
   its own frame, and required fields carry a visible mark beside the label. */
fieldset.field-group {
  margin: 0; padding: .7rem .9rem .85rem; min-width: 0;
  border: 1px solid var(--glass-border); border-radius: var(--r-md);
}
fieldset.field-group legend { padding: 0 .35rem; font-weight: 500; font-size: var(--fs-md); color: var(--ink-muted); }
.field-name { display: inline; }
.req-mark { margin-left: .2rem; color: var(--danger); font-weight: 600; }
.form-req-note { margin: 0; }
.form-req-note .req-mark { margin: 0 .15rem 0 0; }

/* ---- pills ---- */
.pill { display: inline-flex; align-items: center; gap: var(--sp-1); padding: .12rem .5rem; border-radius: var(--r-pill); font-size: var(--fs-xs); font-weight: 600; border: 1px solid var(--glass-border); }
/* Status pills sit on the chip tokens (4.5:1+ in both themes). Severity
   pills that use .pill-critical/.pill-high/.pill-medium/.pill-low sit on the
   `--sev-*` ramp. Pages whose common/ severity_class maps still emit
   danger/warning/info (AI systems, IMO register, account detail) show High
   as red and Low as blue until those maps move to the ramp names. */
.pill-secondary, .pill-unknown { background: var(--chip-neutral-bg); color: var(--chip-neutral-fg); }
.pill-info, .pill-primary { background: var(--chip-brand-bg); color: var(--chip-brand-fg); }
.pill-success { background: var(--chip-ok-bg); color: var(--chip-ok-fg); }
.pill-danger { background: var(--chip-crit-bg); color: var(--chip-crit-fg); }
.pill-warning { background: var(--chip-warn-bg); color: var(--chip-warn-fg); }
.pill-critical { background: var(--sev-critical-wash); color: var(--sev-critical); }
.pill-high { background: var(--sev-high-wash); color: var(--sev-high); }
.pill-medium { background: var(--sev-medium-wash); color: var(--sev-medium); }
.pill-low { background: var(--sev-low-wash); color: var(--sev-low); }

/* ---- read-only capability views ---- */
.stat-row { display: flex; flex-wrap: wrap; gap: var(--sp-4); margin-bottom: 1.4rem; }
.stat { padding: .95rem 1.3rem; min-width: 130px; }
/* The one `.stat-num` rule (a second one near the end of the file used to set a
   different size). The figure is the thing being read; give it the room. */
.stat-num { font-family: var(--f-data); font-variant-numeric: tabular-nums; font-size: var(--fs-4xl); font-weight: 500; line-height: 1.05; letter-spacing: -.035em; }
.cap-list { display: flex; flex-direction: column; gap: var(--sp-3); }
.cap-row { padding: 1rem 1.2rem; display: flex; justify-content: space-between; align-items: center; gap: var(--sp-4); flex-wrap: wrap; transition: transform .1s ease, border-color .15s ease, background .15s ease; }
a.cap-row:hover { text-decoration: none; transform: translateX(3px); border-color: var(--primary); }
/* The left-hand block of a `.cap-row` (title, subtitle, badges). `min-width: 0`
   stops a long title from pushing the meta column off the card. */
.cap-row-main { flex: 1 1 20rem; min-width: 0; }
.cap-row-title { font-weight: 600; font-size: var(--fs-base); display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.cap-row-meta { display: flex; flex-wrap: wrap; gap: var(--sp-1) var(--sp-3); align-items: center; max-width: 60%; justify-content: flex-end; }
.meta-chip { font-size: var(--fs-sm); color: var(--ink-muted); }
.meta-chip b { color: var(--ink-muted); font-weight: 600; }
.badge-row { margin-top: .4rem; display: flex; flex-wrap: wrap; gap: var(--sp-1); }
@media (max-width: 700px) { .cap-row-meta { max-width: 100%; justify-content: flex-start; } }

/* On-screen toolbar that carries the "Save as PDF" button above a result. */
.result-actions { display: flex; justify-content: flex-end; gap: var(--sp-2); margin-bottom: .8rem; }
/* Print masthead is screen-hidden; revealed only when printing (see @media print). */
.print-masthead { display: none; }

/* Print: drop the app chrome and force a light ink-on-white palette so any result
   summary (vuln / IP / user-agent lookups, compliance exports, …) saves to a clean
   PDF regardless of the on-screen theme. */
@media print {
  /* Override the design tokens so every component renders light without per-class
     restyling — they all read these custom properties. */
  :root, :root[data-theme="light"] {
    --ink: #111827; --ink-muted: #374151; --ink-faint: #4b5563;
    --bg-0: #ffffff; --bg-1: #ffffff;
    --glass: #ffffff; --glass-strong: #ffffff; --glass-border: #cbd5e1;
    --input-bg: #ffffff; --hover-bg: #ffffff; --row-border: #e5e7eb;
    --track: #e5e7eb; --card-shadow: none; --watermark-opacity: 0;
    --shadow-1: none; --shadow-2: none; --shadow-3: none;
  }
  @page { margin: 14mm; }
  html, body, .app-body { background: #fff !important; color: #111827 !important; }
  .sidebar, .app-header, .report-tip, .managed-banner, .theme-toggle,
  .aurora, .form-card, .htmx-indicator, .result-actions, [data-print], .no-print { display: none !important; }
  .app-shell { display: block; grid-template-columns: 1fr; }
  .app-main { display: block; }
  .app-content { padding: 0; max-width: none; overflow: visible; }
  .page-content-anim { animation: none; }
  .glass-card {
    background: #fff !important; border: 1px solid #cbd5e1 !important;
    box-shadow: none !important; backdrop-filter: none !important; -webkit-backdrop-filter: none !important;
  }
  .cap-row, .vuln-item, .glass-card, .detail, .feature-card { break-inside: avoid; }
  a { color: #111827 !important; text-decoration: none; }
  /* Keep severity tints so the printed report stays colour-coded. */
  .pill, .meta-chip, .badge-row span { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
  /* Branded masthead at the top of every printed page-content. */
  .print-masthead {
    display: flex !important; align-items: baseline; justify-content: space-between;
    gap: var(--sp-4); flex-wrap: wrap; margin: 0 0 1.3rem; padding: 0 0 .7rem;
    border-bottom: 2px solid #111827; color: #111827;
  }
  .print-masthead .pm-brand { font-weight: 700; font-size: var(--fs-base); }
  .print-masthead .pm-meta { text-align: right; font-size: var(--fs-sm); color: #374151; line-height: var(--lh-snug); }
  .print-masthead .pm-org { display: block; font-weight: 600; font-size: var(--fs-base); color: #111827; }
  .print-masthead .pm-title { display: block; }
}

/* Cards laid out as a responsive grid (IAM roles, capability tiles). */
/* `min(20rem, 100%)`: a 20rem floor is wider than the content column of a
   320px phone, so a single card overflowed it and was cut off. */
.card-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(20rem, 100%), 1fr)); gap: var(--sp-4); }
/* "Scope → level" list inside a role card: label left, pill right, one per row. */
.grant-list { display: flex; flex-direction: column; gap: .1rem; margin: .6rem 0; }
.grant-row { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); font-size: var(--fs-sm); padding: .3rem 0; border-bottom: 1px solid var(--row-border); }
.grant-row:last-child { border-bottom: none; }
/* Heading that introduces a section of a page (a card, a table). */
.section-title { font-size: var(--fs-base); font-weight: 600; margin: 1.5rem 0 .6rem; }

/* ---- tables ---- */
.row-between { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-4); flex-wrap: wrap; }
.table-wrap { padding: .3rem; overflow-x: auto; }
.data-table { width: 100%; border-collapse: collapse; font-size: var(--fs-md); }
.data-table th { text-align: left; font-family: var(--f-display); font-size: var(--fs-xs); font-weight: 600; letter-spacing: .045em; color: var(--ink-muted); padding: .7rem .9rem; border-bottom: 1px solid var(--glass-border); }
/* `break-word`, not `anywhere`: a long value stays inside its cell without
   inviting the browser to hyphenate the mono ID / IP columns that fit today. */
.data-table td { padding: .7rem .9rem; border-bottom: 1px solid var(--row-border); vertical-align: top; overflow-wrap: break-word; }
/* Timestamps and other short atoms keep their own line rather than being
   squeezed into three by a neighbouring wall of text. */
.data-table td.nowrap, .data-table th.nowrap, .data-table span.nowrap { white-space: nowrap; }
/* A free-text cell (notes, findings, a command's output): show the opening lines
   and leave the rest to the row's detail view. `anywhere` lets the column shrink
   so one unbroken 500-character answer cannot stretch the whole table. */
.cell-note { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; line-clamp: 3; overflow: hidden; overflow-wrap: anywhere; max-width: 34rem; }
.data-table tbody tr { transition: background .12s ease; }
.data-table tbody tr:hover { background: var(--hover-bg); }
.data-table tr:last-child td { border-bottom: none; }
.cell-title { font-family: var(--f-display); font-weight: 600; letter-spacing: -.015em; color: var(--ink); }
.ta-right, .data-table th.num, .data-table td.num { text-align: right; }
.data-table td.num { font-family: var(--f-data); font-variant-numeric: tabular-nums; letter-spacing: -.02em; }
/* ---- Fleet lists: aligned tables, cards on a phone (`.table-stack`) ----
   Routers and assets are compared across rows, and as cards their pills and
   figures landed at a different x on every row. On a desktop they are aligned
   tables; below 700px each row becomes the compact card it used to be:
   `.stack-head` is the card title, the other cells flow as "Label value"
   chips (label from `data-label`), and `.stack-wide` (an availability bar) and
   the actions take a line of their own. `.usage-*` is the router resource
   threshold (amber from 80%, red from 90%). */
.usage-warn { color: var(--warning); font-weight: 600; }
.usage-crit { color: var(--danger); font-weight: 600; }
.data-table.table-stack td, .data-table.table-stack td.actions { vertical-align: middle; }
.table-stack .stack-head .cell-title { display: inline-flex; align-items: center; gap: var(--sp-2); white-space: nowrap; }
.table-stack .stack-head .ti { opacity: .7; }
.table-stack td.stack-wide { min-width: 13rem; }
@media (max-width: 700px) {
  /* (0,2,0)/(0,3,0): must beat the later `.table-wrap` and
     `.glass-card .table-wrap` well, padding and scroll rules. */
  .table-wrap.table-stack-wrap, .glass-card .table-wrap.table-stack-wrap { max-height: none; overflow: visible; padding: 0; background: none; border: 0; margin-top: 0; }
  .data-table.table-stack, .table-stack tbody { display: block; width: 100%; }
  .table-stack thead { display: none; }
  .table-stack tbody tr { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-1) var(--sp-3); padding: .85rem 1rem; margin-bottom: .7rem; border: 1px solid var(--glass-border); border-radius: var(--r-sm); background: var(--glass); }
  .table-stack tbody tr:last-child { margin-bottom: 0; }
  .data-table.table-stack td { display: inline-flex; align-items: baseline; gap: var(--sp-1); padding: 0; border: 0; text-align: left; font-size: var(--fs-sm); }
  .table-stack td[data-label]::before { content: attr(data-label); flex: none; color: var(--ink-muted); font-family: var(--f-text); font-size: var(--fs-xs); font-weight: 600; }
  .data-table.table-stack td.stack-head { flex: 1 0 100%; font-size: var(--fs-base); }
  .data-table.table-stack td.stack-wide { display: block; flex: 1 0 100%; min-width: 0; margin-top: .2rem; }
  .table-stack td.stack-wide::before { display: none; }
  .data-table.table-stack td.actions { display: flex; flex: 1 0 100%; justify-content: flex-start; margin-top: .3rem; }
  .data-table.table-stack td.actions > * { margin: 0; }
}
.actions { display: flex; gap: var(--sp-1); justify-content: flex-end; flex-wrap: wrap; align-items: center; }
.inline-form { display: inline; margin: 0; }
/* One-click enforcement form (partials/quick_action.html): the button with its
   own reply line under it, sitting inline next to sibling actions. */
.qa-form { display: inline-flex; flex-direction: column; align-items: flex-start; gap: var(--sp-1); margin: 0; }
.qa-status:empty { display: none; }
.qa-status .alert { padding: .35rem .6rem; margin: 0; font-size: var(--fs-sm); }
/* Text tone for a figure that needs action (overdue count and similar). */
.tone-crit { color: var(--danger); }
.empty { padding: 2.5rem; text-align: center; color: var(--ink-muted); }

/* ---- detail + form ---- */
.detail { padding: 1.5rem; }
.detail .lead { font-size: var(--fs-base); color: var(--ink); }
.meta-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--sp-4); margin: 1.2rem 0; }
.meta-grid dd { margin: .15rem 0 0; }
/* A block of stored free text (a policy body, a reviewer's notes): keep the
   author's line breaks, and break an unbroken run of characters rather than let
   it overflow the card. `.detail-text` is the same block outside a policy. */
.policy-body, .detail-text { white-space: pre-wrap; overflow-wrap: anywhere; background: var(--input-bg); padding: var(--sp-4); border-radius: var(--r-md); font-family: ui-monospace, monospace; font-size: var(--fs-md); line-height: var(--lh-body); border: 1px solid var(--glass-border); }
.form-card { padding: var(--sp-5); max-width: 760px; }
details.raw-status { margin-top: 1.2rem; }
details.raw-status > summary { cursor: pointer; color: var(--ink-muted); font-size: var(--fs-sm); font-weight: 500; padding: .3rem 0; user-select: none; }
details.raw-status > summary:hover { color: var(--ink); }
details.raw-status[open] > summary { margin-bottom: .6rem; }

/* ---- role editor: permission groups with fine-tune sub-areas ---- */
.perm-list { display: flex; flex-direction: column; gap: var(--sp-3); }
.perm-group { margin: 0; padding: var(--sp-3) var(--sp-4); border: 1px solid var(--glass-border); border-radius: var(--r-md); background: var(--glass); }
.perm-area { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-4); }
.perm-area-info { min-width: 0; }
.perm-area-level { flex: 0 0 auto; min-width: 175px; }
.perm-area-level select { margin-top: .25rem; }
.perm-fine { margin-top: .75rem; border-top: 1px dashed var(--glass-border); padding-top: .55rem; }
.perm-fine > summary { cursor: pointer; color: var(--ink-muted); font-size: var(--fs-sm); font-weight: 500; user-select: none; }
.perm-fine > summary:hover { color: var(--ink); }
.perm-fine[open] > summary { margin-bottom: .4rem; }
.perm-fine .sub-table td { padding: .35rem .5rem; }
.perm-fine .sub-table td:first-child { color: var(--ink-muted); width: 58%; }
.perm-fine .sub-table select { padding: .4rem .55rem; border-radius: var(--r-sm); }
@media (max-width: 560px) {
  .perm-area { flex-direction: column; align-items: stretch; }
  .perm-area-level { min-width: 0; }
}

/* ---- alerts ---- */
.alert { padding: .7rem .9rem; border-radius: var(--r-md); border: 1px solid var(--glass-border); font-size: var(--fs-md); }
.alert-err { background: var(--chip-crit-bg); border-color: color-mix(in srgb, var(--chip-crit-fg) 40%, transparent); color: var(--chip-crit-fg); }
.alert-ok { background: var(--chip-ok-bg); border-color: color-mix(in srgb, var(--chip-ok-fg) 40%, transparent); color: var(--chip-ok-fg); }
.alert-managed { background: var(--chip-brand-bg); border-color: color-mix(in srgb, var(--chip-brand-fg) 40%, transparent); color: var(--chip-brand-fg); margin-bottom: 1.2rem; }
/* Templates reach for the Bootstrap names as often as the portal's own; an
   undefined variant is not an error, it just silently renders grey. */
.alert-danger { background: var(--chip-crit-bg); border-color: color-mix(in srgb, var(--chip-crit-fg) 40%, transparent); color: var(--chip-crit-fg); }
.alert-success { background: var(--chip-ok-bg); border-color: color-mix(in srgb, var(--chip-ok-fg) 40%, transparent); color: var(--chip-ok-fg); }
.alert-warning { background: var(--chip-warn-bg); border-color: color-mix(in srgb, var(--chip-warn-fg) 40%, transparent); color: var(--chip-warn-fg); }
.alert-info { background: var(--chip-brand-bg); border-color: color-mix(in srgb, var(--chip-brand-fg) 40%, transparent); color: var(--chip-brand-fg); }

/* ---- brand ---- */
/* The official SeaDirect Protect logo, set as an image (tile, shield and
   wordmark are one file). `partials/brand_lockup.html` carries two copies, one
   per wordmark colour, and the active theme picks one: dark is the default,
   `data-theme="light"` on <html> swaps to the dark-ink wordmark. The bare tile
   stands in for both on the collapsed sidebar rail. Every rule is scoped to
   `.brand-mark` so a logo placed anywhere else keeps its own sizing. */
.brand-mark { display: flex; align-items: center; gap: var(--sp-2); }
.brand-mark .sd-logo { display: block; flex: none; height: 40px; width: auto; }
.brand-mark .sd-logo--light { display: none; }
:root[data-theme="light"] .brand-mark .sd-logo--dark { display: none; }
:root[data-theme="light"] .brand-mark .sd-logo--light { display: block; }
.brand-mark .sd-logo-tile { display: none; flex: none; width: 34px; height: 34px; }
/* The portal's marketing name, Compass, set small under the logo and indented
   to the wordmark: the tile is 62 of the lockup's 145 units, so at 40px tall
   the wordmark starts 62 / 48 x 40px in. */
.brand-mark .brand-lockup { display: flex; flex-direction: column; gap: .15rem; min-width: 0; }
.brand-mark .brand-product {
  padding-left: calc(40px * 62 / 48);
  font-family: var(--f-display);
  font-size: var(--fs-xs); font-weight: 600; letter-spacing: .02em;
  color: var(--ink-muted);
}

/* ---- auth screens ---- */
.auth-body { display: grid; place-items: center; padding: var(--sp-6) var(--sp-4); }
.auth-shell { width: 100%; max-width: 420px; display: flex; flex-direction: column; gap: var(--sp-4); align-items: center; }
/* A shared form is not a login box: a security course carries several lessons
   and a graded quiz, so give it a readable column instead of 420px. */
.share-shell { max-width: 760px; }
.share-body { white-space: pre-wrap; }
/* Security-responsibilities form: one duty per row, with the requirement it
   meets set beneath rather than inline, so each duty still reads as a
   sentence. */
.share-duty-head { font-size: var(--fs-base); margin: 1.25rem 0 .5rem; }
.share-duties { list-style: none; margin: 0 0 .75rem; padding: 0; }
.share-duties li {
  border: 1px solid var(--line, rgba(255, 255, 255, .12));
  border-radius: var(--r-sm);
  padding: .7rem .9rem;
  margin-bottom: .45rem;
  font-size: var(--fs-md);
  line-height: var(--lh-snug);
}
.share-duty-ref { display: block; margin-top: .35rem; font-size: var(--fs-xs); opacity: .65; }
.quiz-q { display: flex; flex-direction: column; gap: var(--sp-2); }
.auth-card { width: 100%; padding: var(--sp-5); }
.auth-foot { text-align: center; }
.recent-orgs { margin: 0 0 1rem; }
.recent-org-list { list-style: none; margin: .4rem 0 0; padding: 0; display: flex; flex-direction: column; gap: var(--sp-1); }
.recent-org {
  display: flex; align-items: stretch; border-radius: var(--r-sm);
  border: 1px solid var(--glass-border); background: var(--glass);
  transition: background .18s ease, border-color .18s ease;
}
.recent-org:hover { background: var(--hover-bg); }
.recent-org.active { border-color: var(--primary); }
.recent-org-pick {
  flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: .1rem;
  padding: .45rem .7rem; color: var(--ink); text-decoration: none;
}
.recent-org-name { font-weight: 600; font-size: var(--fs-md); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.recent-org-id { font-family: ui-monospace, monospace; font-size: var(--fs-xs); color: var(--ink-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.recent-org-forget { display: flex; margin: 0; }
.recent-org-forget button {
  border: 0; background: none; color: var(--ink-muted); cursor: pointer;
  padding: 0 .75rem; font-size: var(--fs-lg); line-height: 1; border-radius: 0 var(--r-sm) var(--r-sm) 0;
}
.recent-org-forget button:hover { color: var(--ink); background: var(--hover-bg); }
.qr { display: grid; place-items: center; background: #fff; padding: var(--sp-3); border-radius: var(--r-md); margin: 1rem 0; }
.qr svg { width: 220px; height: 220px; }
.codes { list-style: none; padding: var(--sp-3) var(--sp-4); margin: 1rem 0; columns: 2; background: var(--input-bg); border-radius: var(--r-md); font-family: ui-monospace, monospace; }
.codes li { padding: .15rem 0; }

/* ---- app shell ---- */
/* Skip link: the first Tab stop on every page, off screen until focused, so a
   keyboard user does not walk the whole sidebar to reach the content. */
.skip-link {
  position: fixed; top: .6rem; left: .6rem; z-index: 4100;
  padding: .55rem .9rem; border-radius: var(--r-sm);
  background: var(--pop-bg); color: var(--ink); border: 1px solid var(--glass-border);
  font-weight: 600; font-size: var(--fs-md); text-decoration: none;
  transform: translateY(-200%);
}
.skip-link:focus, .skip-link:focus-visible { transform: none; text-decoration: none; outline: 2px solid var(--focus-ring, var(--primary-2)); outline-offset: 2px; }
/* #page-main takes focus from the skip link (tabindex="-1"); the whole column
   outlined would only be noise. */
#page-main:focus { outline: none; }
.app-shell { display: grid; grid-template-columns: 264px 1fr; min-height: 100vh; }
.sidebar {
  padding: 1.2rem 1rem; border-right: 1px solid var(--glass-border);
  background: var(--sidebar-bg);
  position: sticky; top: 0; height: 100vh; overflow-y: auto;
  transition: background .35s ease;
  /* Column flexbox so the theme control can sit at the bottom on a short nav
     and still be pushed below the links on a long one. */
  display: flex; flex-direction: column;
}
.sidebar .side-nav { flex: 1 1 auto; }
.sidebar .brand-mark { margin-bottom: 1.4rem; padding-left: .3rem; }
.app-main { display: flex; flex-direction: column; min-width: 0; }
.app-header {
  display: flex; align-items: center; gap: var(--sp-4);
  padding: .7rem 1.6rem; border-bottom: 1px solid var(--glass-border);
  position: sticky; top: 0; z-index: 20;
  background: var(--header-bg); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
  transition: background .35s ease;
}
.header-left { display: flex; align-items: center; gap: var(--sp-3); min-width: 0; flex: 0 0 auto; }
.header-tenant { display: flex; flex-direction: column; line-height: var(--lh-tight); min-width: 0; }
.header-tenant .small { font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: .08em; color: var(--ink-faint); }
.header-tenant strong { font-size: var(--fs-base); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 22vw; }
.header-right { display: flex; align-items: center; gap: var(--sp-3); flex: 0 0 auto; }
.user-name { color: var(--ink-muted); font-size: var(--fs-md); font-weight: 500; }

/* content column. The 21rem rotated feature-icon watermark that used to sit
   behind it is gone: it read as a rendering smear behind controls, and the
   section accent already shows on the `.page-head::after` rule. */
.app-content { position: relative; padding: 2rem 2.2rem; max-width: 1200px; width: 100%; overflow: hidden; }
.app-content > * { position: relative; z-index: 1; }
.page-content-anim { animation: fadeInUp .4s cubic-bezier(.22, 1, .36, 1) both; }
@media (max-width: 700px) { .app-content { padding: 1.4rem 1.1rem; } }

.theme-toggle {
  display: inline-grid; place-items: center; width: 2.1rem; height: 2.1rem;
  border-radius: var(--r-sm); border: 1px solid var(--glass-border); background: var(--glass);
  color: var(--ink-muted); cursor: pointer; font-size: var(--fs-lg); line-height: 1;
  transition: color .15s ease, background .15s ease, transform .2s ease;
}
.theme-toggle:hover { color: var(--ink); background: var(--hover-bg); transform: rotate(18deg); }


/* ---- side nav ---- */
/* Each group is a <details>: shut, its heading is one ~32px row, so the 13
   groups fit a 900px screen; the open group gets a little room below. */
.side-nav { display: flex; flex-direction: column; gap: .2rem; }
.nav-group-title {
  display: flex; align-items: center; gap: var(--sp-2);
  font-family: var(--f-display); font-size: var(--fs-xs); letter-spacing: .045em;
  color: var(--ink-faint); font-weight: 500; padding: .42rem .6rem;
  border-radius: var(--r-sm); cursor: pointer; list-style: none; user-select: none;
  transition: background .15s ease;
}
.nav-group-title::-webkit-details-marker { display: none; }
.nav-group-title:hover { background: var(--hover-bg); }
.nav-group-title:hover .ngt-kind { color: var(--ink); }
.nav-group-title:focus-visible { outline: 2px solid var(--focus-ring, var(--primary-2)); outline-offset: -2px; }
.nav-group-title .ti { font-size: var(--fs-base); color: var(--accent, var(--primary)); }
.nav-group-title .ngt-caret { margin-left: auto; font-size: var(--fs-sm); color: var(--ink-faint); transform: rotate(-90deg); transition: transform .16s ease; }
.nav-group-title .ngt-name + .ngt-caret { margin-left: .3rem; }
.nav-group[open] > .nav-group-title .ngt-caret { transform: none; }
.nav-group[open] > ul { margin: .1rem 0 .6rem; }
/* Recently deleted and any other footer link: below the groups, set apart. */
.side-nav .nav-footer { margin-top: .5rem; padding-top: .5rem; border-top: 1px solid var(--glass-border); }
.nav-link-footer { font-size: var(--fs-sm); }
/* Group heading = plain kind first (legible: it is what a new customer reads),
   then the Compass codename, faint and pushed right. The kind may wrap on the
   longest headings rather than be cut off. */
.nav-group-title .ngt-kind { font-size: var(--fs-xs); font-weight: 600; letter-spacing: 0; color: var(--ink-muted); min-width: 0; }
.nav-group-title .ngt-name { margin-left: auto; font-size: var(--fs-xs); letter-spacing: 0; color: var(--ink-faint); white-space: nowrap; }
.side-nav ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 1px; }
.nav-link {
  display: flex; align-items: center; gap: var(--sp-2); padding: .45rem .6rem; border-radius: var(--r-sm);
  color: var(--ink-muted); font-family: var(--f-text); font-size: var(--fs-sm); font-weight: 400;
  position: relative; transition: background .15s ease, color .15s ease;
}
.nav-link .ti { font-size: var(--fs-base); flex: none; width: 1.15rem; text-align: center; opacity: .85; }
.nav-link:hover { background: var(--hover-bg); color: var(--ink); text-decoration: none; }
.nav-link.active { background: var(--chip-brand-bg); color: var(--ink); font-weight: 500; }
.nav-link.active::before {
  content: ""; position: absolute; left: -.8rem; top: 22%; bottom: 22%;
  width: 2px; border-radius: 0 2px 2px 0; background: var(--primary);
}
.nav-link.active .ti { opacity: 1; color: var(--primary); }

/* The dashboard sits above the Compass groups with no heading of its own, so it
   gets a divider instead to keep it from reading as part of the first group. */
.nav-pinned { list-style: none; margin: 0 0 .2rem; padding: 0 0 1.1rem; border-bottom: 1px solid var(--glass-border); }
.nav-link-pinned { font-family: var(--f-display); font-size: var(--fs-md); font-weight: 600; color: var(--ink); }
.nav-link-pinned .ti { opacity: 1; color: var(--accent, var(--primary-2)); }
/* Only bold and ink-coloured while it IS the page: on every other page a
   permanently strong Dashboard link read as "you are here" while the real
   active item sat out of view further down the sidebar. */
.nav-link-pinned:not(.active) { font-weight: 500; color: var(--ink-muted); }
.nav-link-pinned:not(.active):hover { color: var(--ink); }

/* ---- dashboard ---- */
.page-head { margin-bottom: 1.5rem; }
.page-head h1 { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.capability-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: var(--sp-4); }
.cap-card { padding: var(--sp-4); display: flex; flex-direction: column; gap: var(--sp-1); color: var(--ink); transition: transform .12s ease, border-color .15s ease, box-shadow .15s ease; position: relative; overflow: hidden; }
.cap-card .cap-card-icon { position: absolute; right: -.4rem; bottom: -.8rem; font-size: 4.6rem; /* fs-exempt: watermark icon */ line-height: 1; color: var(--accent, var(--primary-2)); opacity: .12; transition: opacity .15s ease, transform .2s ease; }
.cap-card:hover { text-decoration: none; transform: translateY(-3px); border-color: var(--accent, var(--primary-2)); box-shadow: var(--shadow-2); }
.cap-card:hover .cap-card-icon { opacity: .22; transform: scale(1.08) translateY(-2px); }
.cap-kind { color: var(--accent-ink); }
.cap-name { font-size: var(--fs-lg); font-weight: 600; }
.cap-choices { display: flex; flex-wrap: wrap; gap: var(--sp-1); margin-top: .35rem; }

/* ---- dashboard: hero + rail + tiles ---- */
/* Compliance hero left, bandwidth rail right; the rail collapses under the hero
   on narrow screens rather than squeezing the score illegibly. */
.dash-top { display: grid; grid-template-columns: minmax(0, 1fr) minmax(210px, 280px); gap: var(--sp-4); margin-bottom: 1rem; }
@media (max-width: 900px) { .dash-top { grid-template-columns: minmax(0, 1fr); } }

.dash-kicker { color: var(--link); }
.dash-sub { margin-top: 1rem; }

.dash-hero { padding: 1.3rem; display: flex; flex-direction: column; color: var(--ink); }
.dash-hero:hover { text-decoration: none; border-color: var(--primary-2); }
/* Kicker above the score, not at the far end of the row: spread across the
   hero the score sat ~690px from the label that says what it is. */
.dash-hero-head { display: flex; flex-direction: column; align-items: flex-start; gap: var(--sp-1); }
.dash-score { font-size: 2.9rem; /* fs-exempt: hero dial figure, above the scale */ font-weight: 700; line-height: 1; }
.dash-score-pct { font-size: var(--fs-xl); font-weight: 600; color: var(--ink-muted); margin-left: .1rem; }
.dash-score-none { color: var(--ink-faint); }
.dash-meter { height: 9px; border-radius: var(--r-sm); background: var(--track); overflow: hidden; margin: .8rem 0 .5rem; }
.dash-meter-fill { display: block; height: 100%; border-radius: var(--r-sm); background: linear-gradient(90deg, var(--primary), var(--primary-2)); }

.dash-weak { list-style: none; margin: .5rem 0 0; padding: 0; display: flex; flex-direction: column; gap: var(--sp-2); }
.dash-weak li { display: grid; grid-template-columns: minmax(0, 9rem) 1fr auto; align-items: center; gap: var(--sp-2); }
.dash-weak-name { font-size: var(--fs-md); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dash-weak-bar { height: 6px; border-radius: var(--r-xs); background: var(--track); overflow: hidden; }
.dash-weak-bar > span { display: block; height: 100%; border-radius: var(--r-xs); background: var(--warning); }

.dash-due { list-style: none; margin: .5rem 0 0; padding: 0; display: flex; flex-direction: column; gap: var(--sp-1); }
.dash-due li { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-md); }
.dash-due li .ti { color: var(--warning); flex: none; }
.dash-due li.is-overdue .ti { color: var(--danger); }
.dash-due-item { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }

.dash-rail { padding: 1.1rem; display: flex; flex-direction: column; gap: var(--sp-2); color: var(--ink); }
.dash-rail:hover { text-decoration: none; border-color: var(--primary-2); }
.dash-spark { display: flex; align-items: flex-end; gap: 3px; height: 62px; }
/* min-height keeps an idle bucket visible as a baseline tick rather than nothing. */
.dash-spark > span { flex: 1; min-height: 2px; border-radius: 2px 2px 0 0; background: linear-gradient(180deg, var(--primary-2), var(--primary)); opacity: .85; }
/* The sparkline has no axis, so its two ends are named: oldest left, now right. */
.dash-spark-axis { display: flex; justify-content: space-between; margin-top: -.35rem; font-size: var(--fs-xs); color: var(--ink-muted); }
/* The rail stretches to the hero's height; let the sparkline take that height
   instead of leaving ~150px empty under the totals. */
.dash-rail .dash-spark { flex: 1; min-height: 62px; height: auto; }
.dash-rail-totals { display: flex; gap: var(--sp-4); font-size: var(--fs-md); font-weight: 600; }
.dash-rail-totals .ti { color: var(--primary-2); }

.dash-checks { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: var(--sp-4); margin-bottom: 1.5rem; }
.dash-check { padding: 1rem 1.1rem; display: flex; flex-direction: column; gap: .2rem; color: var(--ink); border-left: 3px solid var(--track); }
.dash-check:hover { text-decoration: none; border-color: var(--primary-2); }
.dash-check-value { font-size: var(--fs-4xl); font-weight: 700; line-height: var(--lh-tight); }
.dash-check-ok { border-left-color: var(--success); }
.dash-check-warn { border-left-color: var(--warning); }
.dash-check-crit { border-left-color: var(--danger); }
.dash-check-crit .dash-check-value { color: var(--danger); }

/* Announced to screen readers, invisible to everyone else. The skeletons are
   decorative, so the loading state has to be conveyed some other way. */
.visually-hidden {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* Skeletons: hold each panel's shape while its fragment loads so the page does
   not reflow underneath the reader. */
.dash-skeleton { display: flex; flex-direction: column; gap: var(--sp-3); padding: 1.2rem; pointer-events: none; }
.skel { display: block; border-radius: var(--r-sm); background: var(--track); position: relative; overflow: hidden; }
.skel-score { width: 6rem; height: 2.6rem; }
.skel-value { width: 3.5rem; height: 1.8rem; }
.skel-spark { width: 100%; height: 62px; }
.skel-line { width: 100%; height: .7rem; }
.skel-short { width: 55%; }
.skel::after {
  content: ""; position: absolute; inset: 0; transform: translateX(-100%);
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .07), transparent);
  animation: skel-sweep 1.4s ease-in-out infinite;
}
@keyframes skel-sweep { to { transform: translateX(100%); } }
/* Respect a reader who has asked the OS for less motion. */
@media (prefers-reduced-motion: reduce) { .skel::after { animation: none; } }

/* A capability the operator has switched off: named, never populated. */
.dash-locked { position: relative; overflow: hidden; opacity: .72; display: flex; gap: var(--sp-4); align-items: center; padding: 1.3rem; }
.dash-locked-icon { position: absolute; right: -.4rem; bottom: -.8rem; font-size: 4.2rem; /* fs-exempt: watermark icon */ line-height: 1; opacity: .08; }
.dash-locked-title { display: flex; align-items: center; gap: var(--sp-1); font-weight: 600; margin-bottom: .25rem; }

/* ---- dashboard: The Voyage strip ---- */
/* The compact stage itself is styled in voyage.css — that sheet loads after this
   one on the dashboard, so an override here would lose the cascade. */

/* ---- router host-metric gauges ---- */
.gauge-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: var(--sp-4); margin: .3rem 0 1.2rem; }
.gauge { padding: 1.1rem .8rem; display: flex; flex-direction: column; align-items: center; gap: var(--sp-2); text-align: center; }
.gauge-ring { width: 116px; height: 116px; transform: rotate(-90deg); }
.gauge-ring .track { fill: none; stroke: var(--track); stroke-width: 11; }
.gauge-ring .val { fill: none; stroke-width: 11; stroke-linecap: round; stroke: var(--primary-2); stroke-dasharray: 326.726; stroke-dashoffset: calc(326.726 * (1 - var(--pct, 0) / 100)); animation: gauge-fill 1.3s cubic-bezier(.22, 1, .36, 1) forwards; }
.gauge.is-ok .val { stroke: var(--success); }
.gauge.is-warn .val { stroke: var(--warning); }
.gauge.is-crit .val { stroke: var(--danger); }
.gauge-center { margin-top: -82px; height: 82px; display: flex; flex-direction: column; align-items: center; justify-content: center; }
.gauge-num { font-size: var(--fs-2xl); font-weight: 700; line-height: 1; }
.gauge-num span { font-size: var(--fs-md); font-weight: 600; color: var(--ink-muted); }
.gauge-label { font-size: var(--fs-sm); color: var(--ink-muted); font-weight: 500; }
.gauge-sub { font-size: var(--fs-xs); color: var(--ink-faint); }

.metric-bars { display: flex; flex-direction: column; gap: var(--sp-3); }
.metric-bar-row { display: grid; grid-template-columns: 9rem 1fr auto; align-items: center; gap: var(--sp-3); font-size: var(--fs-sm); }
.metric-bar-label { color: var(--ink-muted); }
.metric-bar-track { height: 9px; border-radius: var(--r-pill); background: var(--track); overflow: hidden; }
/* The final width is static, so a fill still reads right when its animation is
   cancelled (`.is-live` polls, reduced motion). An inline `width:` still wins. */
.metric-bar-fill { width: calc(var(--pct, 0) * 1%); height: 100%; border-radius: var(--r-pill); background: linear-gradient(90deg, var(--primary), var(--primary-2)); animation: bar-grow 1.1s cubic-bezier(.22, 1, .36, 1) forwards; }
.metric-bar-fill.is-warn { background: linear-gradient(90deg, var(--warning), #d79a2a); }
.metric-bar-fill.is-crit { background: linear-gradient(90deg, var(--danger), #ff6f80); }
.metric-bar-val { color: var(--ink); font-weight: 600; font-variant-numeric: tabular-nums; }
@media (max-width: 560px) { .metric-bar-row { grid-template-columns: 7rem 1fr auto; } }

.host-facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: var(--sp-3) var(--sp-5); margin-top: .4rem; }
.host-fact dd { margin: .1rem 0 0; font-weight: 600; }
.live-dot { display: inline-block; width: .55rem; height: .55rem; border-radius: 50%; background: var(--success); box-shadow: 0 0 0 0 color-mix(in srgb, var(--success) 50%, transparent); animation: pulse-dot 2s 3; vertical-align: middle; margin-right: .35rem; }

/* ---- animations ---- */
@keyframes fadeInUp { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
@keyframes gauge-fill { from { stroke-dashoffset: 326.726; } to { stroke-dashoffset: calc(326.726 * (1 - var(--pct, 0) / 100)); } }
/* No `to` frame: the grow ends at the element's own width (the --pct rule or
   an inline `width:`). A `to` of --pct overrode inline widths with 0. */
@keyframes bar-grow { from { width: 0; } }
@keyframes pulse-dot { 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--success) 50%, transparent); } 70% { box-shadow: 0 0 0 7px color-mix(in srgb, var(--success) 0%, transparent); } 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--success) 0%, transparent); } }

/* HTMX swap transitions: fade results in/out as fragments arrive. */
.htmx-swapping { opacity: 0; transition: opacity .12s ease; }
.htmx-settling { opacity: 0; }
.htmx-added { opacity: 0; }
[hx-get], [hx-post] { transition: opacity .15s ease; }
.htmx-request.htmx-indicator, .htmx-request .htmx-indicator { opacity: 1; }
.htmx-indicator { opacity: 0; transition: opacity .15s ease; }
.spinner { display: inline-block; width: 1rem; height: 1rem; border: 2px solid var(--glass-border); border-top-color: var(--primary-2); border-radius: 50%; animation: spin .7s linear infinite; vertical-align: -2px; }
@keyframes spin { to { transform: rotate(360deg); } }

@media (prefers-reduced-motion: reduce) {
  *, .page-content-anim, .gauge-ring .val, .metric-bar-fill, .live-dot { animation: none !important; transition: none !important; }
}

/* ======================================================================
   Modern chrome: header search trigger, command palette, status pill,
   mobile nav drawer, and small responsive utilities.
   ====================================================================== */

/* Small icon-only buttons (nav toggle, etc.). */
.icon-btn {
  display: none; place-items: center; width: 2.3rem; height: 2.3rem; flex: none;
  border-radius: var(--r-sm); border: 1px solid var(--glass-border); background: var(--glass);
  color: var(--ink-muted); cursor: pointer; font-size: var(--fs-xl); line-height: 1;
  transition: color .15s ease, background .15s ease, border-color .15s ease;
}
.icon-btn:hover { color: var(--ink); background: var(--hover-bg); }

/* ---- header search trigger (opens the command palette) ---- */
.search-trigger {
  flex: 0 1 420px; min-width: 0; margin: 0 auto;
  display: flex; align-items: center; gap: var(--sp-2);
  padding: .5rem .85rem; border-radius: var(--r-sm);
  border: 1px solid var(--glass-border); background: var(--input-bg);
  color: var(--ink-faint); cursor: pointer; font: inherit; font-size: var(--fs-md);
  transition: border-color .15s ease, background .15s ease, box-shadow .15s ease, color .15s ease;
}
.search-trigger:hover { border-color: var(--primary-2); color: var(--ink-muted); box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 12%, transparent); }
.search-trigger > .ti { font-size: var(--fs-lg); flex: none; }
.search-trigger-label { flex: 1; text-align: left; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.search-kbd { display: inline-flex; gap: .2rem; flex: none; }
.search-kbd kbd, .cmdk-foot kbd {
  font: 600 var(--fs-xs)/1.5 ui-monospace, monospace; min-width: 1.3rem; text-align: center;
  padding: .05rem .35rem; border-radius: var(--r-sm); border: 1px solid var(--glass-border);
  background: var(--glass); color: var(--ink-muted);
}

/* ---- command palette modal ---- */
.cmdk-overlay {
  position: fixed; inset: 0; z-index: 200; display: flex; justify-content: center;
  align-items: flex-start; padding: 12vh 1rem 2rem;
  background: rgba(3, 6, 14, .55); backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
  animation: cmdk-fade .12s ease both;
}
.cmdk {
  width: 100%; max-width: 560px; max-height: 70vh; display: flex; flex-direction: column;
  border-radius: var(--r-md); overflow: hidden; animation: cmdk-pop .16s cubic-bezier(.22, 1, .36, 1) both;
}
.cmdk-input-row { display: flex; align-items: center; gap: var(--sp-2); padding: .85rem 1rem; border-bottom: 1px solid var(--glass-border); }
.cmdk-input-row > .ti { font-size: var(--fs-xl); color: var(--ink-faint); flex: none; }
.cmdk-input { flex: 1; border: none; background: transparent; padding: 0; font-size: var(--fs-base); color: var(--ink); }
.cmdk-input:focus { outline: none; box-shadow: none; }
.cmdk-esc { flex: none; padding: .2rem .5rem; border-radius: var(--r-sm); border: 1px solid var(--glass-border); background: var(--glass); color: var(--ink-muted); font: 600 var(--fs-xs)/1.4 var(--f-text); cursor: pointer; }
.cmdk-esc:hover { color: var(--ink); background: var(--hover-bg); }
.cmdk-results { list-style: none; margin: 0; padding: .4rem; overflow-y: auto; }
.cmdk-item {
  display: flex; align-items: center; gap: var(--sp-3); padding: .6rem .7rem; border-radius: var(--r-sm);
  cursor: pointer; color: var(--ink); scroll-margin: .4rem;
}
.cmdk-item > .ti { font-size: var(--fs-lg); color: var(--ink-muted); flex: none; width: 1.3rem; text-align: center; }
.cmdk-item.active { background: var(--glass-strong); }
.cmdk-item.active > .ti { color: var(--primary-2); }
/* Label and subtitle share the row's slack and both truncate, so a long
   hostname or policy title can never push the group chip off the edge. The
   label keeps priority: it only shrinks once the subtitle has. */
.cmdk-item-label { flex: 1 1 auto; min-width: 0; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Data hits carry a second line of context (OS, reference, vendor, …); nav hits
   omit it entirely and the label simply takes the whole row. */
.cmdk-item-subtitle { flex: 1 1 40%; min-width: 0; font-size: var(--fs-sm); color: var(--ink-faint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Severity/status chip on data hits. Kept visually quieter than the group chip
   so the row still reads title-first. */
.cmdk-item-badge { font-size: var(--fs-xs); font-weight: 600; letter-spacing: .02em; text-transform: uppercase; color: var(--ink-muted); padding: .1rem .45rem; border-radius: var(--r-pill); border: 1px solid var(--glass-strong); flex: none; }
.cmdk-item-group { font-size: var(--fs-xs); color: var(--ink-faint); padding: .1rem .5rem; border-radius: var(--r-pill); background: var(--glass); flex: none; }
/* "Recent" / "Popular" headings on the empty palette. Not options: arrow keys
   and Enter skip them (portal.js only walks .cmdk-item). */
.cmdk-section { list-style: none; padding: .7rem .7rem .25rem; color: var(--ink-muted); }
.cmdk-section:first-child { padding-top: .3rem; }
.cmdk-empty { padding: 2rem; text-align: center; color: var(--ink-muted); }
.cmdk-foot { display: flex; gap: var(--sp-4); padding: .55rem 1rem; border-top: 1px solid var(--glass-border); font-size: var(--fs-xs); color: var(--ink-faint); }
.cmdk-foot span { display: inline-flex; align-items: center; gap: var(--sp-1); }
body.cmdk-open { overflow: hidden; }
@keyframes cmdk-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes cmdk-pop { from { opacity: 0; transform: translateY(-8px) scale(.98); } to { opacity: 1; transform: none; } }

/* ---- header status indicator ---- */
.status-wrap { position: relative; }
.status-pill {
  display: inline-flex; align-items: center; gap: var(--sp-2); cursor: pointer;
  padding: .4rem .7rem; border-radius: var(--r-pill); font: inherit; font-size: var(--fs-sm); font-weight: 600;
  border: 1px solid var(--glass-border); background: var(--glass); color: var(--ink-muted);
  transition: background .15s ease, border-color .15s ease, color .15s ease;
}
.status-pill:hover { background: var(--hover-bg); color: var(--ink); }
.status-caret { font-size: var(--fs-sm); opacity: .7; }
.status-dot { width: .6rem; height: .6rem; border-radius: 50%; flex: none; background: var(--ink-faint); }
.status-ok .status-dot, .status-dot.status-ok { background: var(--success); box-shadow: 0 0 0 0 color-mix(in srgb, var(--success) 50%, transparent); animation: pulse-dot 2.4s 3; }
.status-warn .status-dot, .status-dot.status-warn { background: var(--warning); }
.status-crit .status-dot, .status-dot.status-crit { background: var(--danger); box-shadow: 0 0 0 0 color-mix(in srgb, var(--danger) 50%, transparent); animation: pulse-crit 1.6s infinite; }
.status-muted .status-dot, .status-dot.status-muted { background: var(--ink-faint); }
.status-pill.status-ok { color: var(--success); border-color: color-mix(in srgb, var(--success) 35%, transparent); }
.status-pill.status-warn { color: var(--warning); border-color: color-mix(in srgb, var(--warning) 40%, transparent); }
.status-pill.status-crit { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 40%, transparent); }
@keyframes pulse-crit { 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--danger) 50%, transparent); } 70% { box-shadow: 0 0 0 7px color-mix(in srgb, var(--danger) 0%, transparent); } 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--danger) 0%, transparent); } }

.status-panel {
  position: absolute; top: calc(100% + .55rem); right: 0; z-index: 40; width: 19rem; max-width: 86vw;
  padding: var(--sp-3); border-radius: var(--r-md); border: 1px solid var(--glass-border);
  background: var(--pop-bg); box-shadow: var(--shadow-2);
  animation: cmdk-pop .14s cubic-bezier(.22, 1, .36, 1) both;
}
.status-panel-head { display: flex; align-items: center; gap: var(--sp-2); padding: .1rem .3rem .6rem; border-bottom: 1px solid var(--glass-border); margin-bottom: .4rem; }
.status-panel-label { font-size: var(--fs-md); color: var(--ink); }
.status-checks { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 1px; }
.status-check-link { display: flex; align-items: center; gap: var(--sp-2); padding: .45rem .35rem; border-radius: var(--r-sm); color: var(--ink-muted); font-size: var(--fs-sm); }
.status-check-link:hover { background: var(--hover-bg); color: var(--ink); text-decoration: none; }
.status-check-label { flex: 1; }
.status-check-val { font-weight: 700; font-variant-numeric: tabular-nums; color: var(--ink); }
.status-check-val.status-warn { color: var(--warning); }
.status-check-val.status-crit { color: var(--danger); }
.status-check-empty { padding: .6rem .35rem; color: var(--ink-muted); font-size: var(--fs-sm); }
.status-panel-foot { display: flex; align-items: center; gap: var(--sp-1); margin-top: .5rem; padding: .5rem .35rem 0; border-top: 1px solid var(--glass-border); font-size: var(--fs-sm); font-weight: 500; }

/* ---- card refinement: a top accent bar grows in on hover ---- */
.cap-card::before {
  content: ""; position: absolute; left: 0; top: 0; height: 3px; width: 100%;
  background: linear-gradient(90deg, var(--accent, var(--primary-2)), transparent 75%);
  transform: scaleX(0); transform-origin: left; transition: transform .25s ease; opacity: .9;
}
.cap-card:hover::before { transform: scaleX(1); }

/* ---- small responsive utilities ---- */
.show-sm { display: none; }

/* The command palette declares display:flex when shown, so the [hidden] attribute
   it is toggled with needs an explicit rule to win and keep it closed by default. */
.cmdk-overlay[hidden] { display: none; }

/* ---- mobile nav drawer ---- */
/* The backdrop is shown/hidden purely via its [hidden] attribute (toggled in JS);
   it carries no default `display`, so it stays hidden until JS reveals it on mobile. */

@media (max-width: 1080px) {
  .search-trigger-label, .search-kbd { display: none; }
  .search-trigger { flex: 0 0 auto; padding: .5rem; margin: 0 0 0 auto; }
  .user-name.hide-md, .hide-md { display: none; }
  /* Once the search box is only an icon, the organisation's name is the part
     of the bar that gives: it ellipsises into whatever the controls leave.
     With a fixed cap instead, the bar ran wider than the screen from 561 to
     about 650px, and beside the desktop sidebar in the 820-1000px range. */
  .header-left { flex: 1 1 auto; }
  .header-tenant strong { max-width: none; }
}

@media (max-width: 820px) {
  .app-shell { display: block; }
  .icon-btn { display: inline-grid; }
  .sidebar {
    position: fixed; top: 0; left: 0; z-index: 30; width: 280px; max-width: 84vw; height: 100vh;
    border-right: 1px solid var(--glass-border); transform: translateX(-100%);
    transition: transform .25s cubic-bezier(.22, 1, .36, 1);
  }
  body.nav-open .sidebar { transform: none; box-shadow: var(--shadow-3); }
  .nav-backdrop { position: fixed; inset: 0; z-index: 29; background: rgba(3, 6, 14, .5); backdrop-filter: blur(2px); }
}

@media (max-width: 560px) {
  .app-header { padding: .6rem .9rem; gap: var(--sp-2); }
  .app-header .btn-primary .hide-sm, .hide-sm { display: none; }
  .show-sm { display: inline-grid; }
  .status-text, .status-caret { display: none; }
  .status-pill { padding: .45rem; }
}

/* ── Seawall → Bandwidth analytics ─────────────────────────────────────── */
.bw-controls { margin-bottom: 1rem; }
.bw-field { display: flex; flex-direction: column; gap: var(--sp-1); }
.input {
  appearance: none; background: var(--glass, rgba(255,255,255,.06));
  color: var(--ink, #eaf0fb); border: 1px solid var(--input-border, rgba(255,255,255,.34));
  border-radius: var(--r-sm); padding: .45rem .7rem; font: inherit; min-width: 16rem;
}
.bw-tabs, .bw-ranges { display: inline-flex; gap: var(--sp-1); padding: .2rem;
  background: var(--glass, rgba(255,255,255,.05)); border: 1px solid var(--glass-border, rgba(255,255,255,.12));
  border-radius: var(--r-md); }
.bw-tab, .bw-range {
  border: 0; background: transparent; color: var(--ink-muted, #9aa7c0);
  padding: .4rem .7rem; border-radius: var(--r-sm); font: inherit; cursor: pointer; display: inline-flex; align-items: center; gap: var(--sp-1);
}
.bw-tab.is-active, .bw-range.is-active { background: var(--btn-primary-bg); color: var(--on-primary); }
.bw-summary { display: flex; flex-wrap: wrap; gap: var(--sp-5); align-items: center; margin: .4rem 0 1rem; }
.bw-stat strong { font-size: var(--fs-lg); margin-left: .35rem; }
.bw-chart-wrap { position: relative; height: 280px; margin-bottom: 1rem; }
/* The router page's inline 24h panel (router_bandwidth.js). A blank box read as
   broken, so the panel always says something: a skeleton while it loads, a line
   when there was no traffic, and a retry when the read failed. */
.bw-chart-wrap.bw-chart-inline { height: 200px; margin-top: .6rem; }
.bw-chart-state { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; gap: var(--sp-2); flex-wrap: wrap; padding: 1rem; text-align: center; color: var(--ink-muted); font-size: var(--fs-md); }
.bw-chart-state[hidden], .bw-chart-wrap canvas[hidden] { display: none; }
.bw-chart-state .ti { font-size: var(--fs-xl); }
.bw-chart-skel { position: absolute; inset: 0; border-radius: var(--r-md); }
.bw-subhead { font-size: var(--fs-base); margin: 1.2rem 0 .6rem; }
.table-scroll { overflow-x: auto; }

/* Per-WAN connection-type badge + inline rename editor. */
.bw-name { font-weight: 600; }
.bw-badge {
  display: inline-block; margin-left: .5rem; padding: .08rem .5rem; border-radius: var(--r-pill);
  font-size: var(--fs-xs); font-weight: 600; letter-spacing: .01em; vertical-align: middle;
  background: var(--glass, rgba(255,255,255,.06)); border: 1px solid var(--glass-border, rgba(255,255,255,.14));
  color: var(--ink-muted, #9aa7c0);
}
.bw-badge--starlink { background: color-mix(in srgb, var(--primary) 16%, transparent); border-color: color-mix(in srgb, var(--primary) 40%, transparent); color: var(--chip-brand-fg); }
.bw-badge--satellite-vsat { background: rgba(155,107,255,.16); border-color: rgba(155,107,255,.4); color: #b18cff; }
.bw-badge--fiber { background: rgba(38,194,129,.16); border-color: rgba(38,194,129,.4); color: #2fc98a; }
.bw-badge--cable { background: rgba(255,176,32,.16); border-color: rgba(255,176,32,.4); color: #ffb020; }
.bw-badge--dsl { background: rgba(127,144,176,.16); border-color: rgba(127,144,176,.4); color: #9aa7c0; }
.bw-badge--cellular { background: rgba(255,99,132,.16); border-color: rgba(255,99,132,.4); color: #ff6384; }
.bw-badge--fixed-wireless { background: rgba(0,194,203,.16); border-color: rgba(0,194,203,.4); color: #00c2cb; }
.bw-badge--transit { background: rgba(160,174,192,.16); border-color: rgba(160,174,192,.4); color: #a0aec0; }
.bw-edit {
  margin-left: .5rem; border: 0; background: transparent; color: var(--ink-muted, #9aa7c0);
  cursor: pointer; font-size: var(--fs-sm); opacity: .65; padding: 0 .2rem;
}
.bw-edit:hover { opacity: 1; color: var(--primary); }
.bw-editor { display: inline-flex; gap: var(--sp-1); flex-wrap: wrap; align-items: center; }
.bw-editor .input { padding: .25rem .5rem; font-size: var(--fs-sm); }
.bw-editor .bw-alias { width: 9rem; }
.bw-editor .bw-type { width: 9rem; }

/* ---- Compliance "View examples" modal (CSS-only, :target — no JS / CSP-safe) - */
.ex-actions { margin: -.4rem 0 1rem; }
.ex-modal { display: none; }
.ex-modal:target {
  position: fixed; inset: 0; z-index: 220; display: flex; justify-content: center;
  align-items: flex-start; padding: 8vh 1rem 2rem;
  background: rgba(3, 6, 14, .55); backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
}
.ex-modal-backdrop { position: absolute; inset: 0; }
.ex-modal-card {
  position: relative; width: 100%; max-width: 720px; max-height: 82vh; overflow-y: auto;
  border-radius: var(--r-md); padding: 0;
}
.ex-modal-head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-4);
  padding: var(--sp-4) var(--sp-4); border-bottom: 1px solid var(--glass-border);
  position: sticky; top: 0; background: var(--glass-strong);
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); border-radius: var(--r-md) var(--r-md) 0 0;
}
.ex-modal-title { margin: 0; font-size: var(--fs-lg); }
.ex-modal-x { font-size: var(--fs-3xl); line-height: 1; color: var(--ink-muted); text-decoration: none; padding: 0 .3rem; }
.ex-modal-x:hover { color: var(--ink); }
.ex-modal-body { padding: var(--sp-4) var(--sp-4) var(--sp-5); }
.ex-intro { margin-top: 0; }
.ex-todo { background: var(--glass); border: 1px solid var(--glass-border); border-radius: var(--r-md); padding: var(--sp-2) var(--sp-3); margin: .8rem 0 1.2rem; }
.ex-todo h4, .ex-records-h { margin: 0 0 .5rem; }
.ex-todo ol { margin: 0; padding-left: 1.1rem; }
.ex-todo li { margin: .2rem 0; }
.ex-record { border: 1px solid var(--glass-border); border-radius: var(--r-md); padding: .7rem .9rem; margin-bottom: .8rem; }
.ex-record-title { font-weight: 600; margin-bottom: .4rem; }
.ex-record-table { width: 100%; border-collapse: collapse; }
.ex-record-table th { text-align: left; width: 38%; vertical-align: top; color: var(--ink-muted); font-weight: 500; padding: .15rem .5rem .15rem 0; font-size: var(--fs-sm); }
.ex-record-table td { padding: .15rem 0; font-size: var(--fs-sm); }
.ex-record-note { margin-top: .45rem; font-size: var(--fs-sm); color: var(--ink-faint); }
.ex-foot { margin-top: 1rem; }

/* ---- 404 (served by register::not_found, standalone — no app shell) ---- */
.nf-body { min-height: 100vh; display: grid; place-items: center; padding: var(--sp-6); }
.nf-wrap { max-width: 32rem; text-align: center; }
.nf-code { margin: 0; font-size: 4.5rem; /* fs-exempt: 404 hero code */ font-weight: 700; line-height: 1; letter-spacing: -.04em; background: linear-gradient(135deg, var(--primary), var(--primary-2)); -webkit-background-clip: text; background-clip: text; color: transparent; }
.nf-title { margin: .75rem 0 .5rem; font-size: var(--fs-2xl); font-weight: 600; }
.nf-text { margin: 0 0 1.5rem; color: var(--ink-muted); }

/* ---------- module pages (Safe Harbor / Radar / Quartermaster / Lookout /
   Logbook / Protection status) ---------- */

/* Sub-navigation tabs inside a module. */
.mod-tabs { display: flex; gap: var(--sp-2); flex-wrap: wrap; margin: 0 0 1.1rem; }
.mod-tab {
  display: inline-flex; align-items: center; gap: var(--sp-1);
  padding: .45rem .9rem; border-radius: var(--r-pill);
  border: 1px solid var(--glass-border); background: var(--glass);
  color: var(--ink-muted); text-decoration: none; font-size: var(--fs-md);
  transition: background .18s ease, color .18s ease, transform .18s ease;
}
.mod-tab:hover { background: var(--hover-bg); color: var(--ink); transform: translateY(-1px); }
.mod-tab.active { background: var(--btn-primary-bg); border-color: var(--btn-primary-bg); color: var(--on-primary); }

/* Filter bar above paginated lists. */
.filter-bar {
  display: flex; gap: var(--sp-3); align-items: flex-end; flex-wrap: wrap;
  margin-bottom: 1rem;
}
.filter-field { display: flex; flex-direction: column; gap: var(--sp-1); min-width: 10rem; }
.filter-grow { flex: 1 1 14rem; }

/* Filter chips (quick single-choice filters). */
.chip-row { display: flex; gap: var(--sp-2); flex-wrap: wrap; margin: 0 0 .9rem; }
.chip {
  padding: .3rem .8rem; border-radius: var(--r-pill); font-size: var(--fs-sm);
  border: 1px solid var(--glass-border); background: var(--glass);
  color: var(--ink-muted); text-decoration: none; cursor: pointer;
  transition: background .18s ease, color .18s ease;
}
.chip:hover { background: var(--hover-bg); color: var(--ink); }
.chip.active { background: var(--btn-primary-bg); border-color: var(--btn-primary-bg); color: var(--on-primary); }
/* Sibling-page navigation (Helm) drawn as tabs, so it never reads as the
   filter chips a page may show below it. */
.chip-row.subnav { gap: 0; border-bottom: 1px solid var(--glass-border); margin-bottom: 1.1rem; }
.subnav .chip {
  border: 0; border-bottom: 2px solid transparent; border-radius: 0;
  background: transparent; padding: .45rem .9rem; margin-bottom: -1px;
}
.subnav .chip:hover { background: transparent; color: var(--ink); border-bottom-color: var(--glass-border); }
.subnav .chip.active { background: transparent; color: var(--ink); border-bottom-color: var(--link); font-weight: 600; }

/* Prev/next pager. */
.pager { display: flex; align-items: center; gap: var(--sp-3); justify-content: center; margin-top: 1rem; }
.pager-page { min-width: 4.5rem; text-align: center; }

/* Stat tile accents. */
.stat-warn { color: var(--warning); }
.stat-critical .stat-num { color: var(--danger); }
.stat-high .stat-num { color: var(--warning); }
.stat-unit { font-size: .7em; color: var(--ink-muted); margin-left: .1rem; }
.stat-small { font-size: var(--fs-base); line-height: 2.2rem; }

/* Generic inline empty-state note inside a card. */
.empty-note {
  display: flex; align-items: center; gap: var(--sp-2); justify-content: center;
  padding: 1.4rem; color: var(--ink-muted);
  border: 1px dashed var(--glass-border); border-radius: var(--r-md);
}

/* Monospace cell + wrapped long text. */
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: .92em; }
.pre-wrap { white-space: pre-wrap; }

/* Vertical timeline (incident dossier, drill history). */
.tl { position: relative; margin: .4rem 0 0 .4rem; padding-left: 1.2rem; }
.tl::before {
  content: ""; position: absolute; left: 0; top: .4rem; bottom: .4rem;
  width: 2px; background: var(--track); border-radius: 2px;
}
.tl-item { position: relative; padding: 0 0 1.1rem .6rem; }
/* Entrance motion is gated on `html.js-motion` (motion.js, off under reduced
   motion) so a page without the script, or a polled `.is-live` panel, never
   replays it. */
html.js-motion .tl-item { animation: fadeInUp .45s ease both; }
.tl-item:nth-child(2) { animation-delay: .06s; }
.tl-item:nth-child(3) { animation-delay: .12s; }
.tl-item:nth-child(4) { animation-delay: .18s; }
.tl-item:nth-child(5) { animation-delay: .24s; }
.tl-item:nth-child(n+6) { animation-delay: .3s; }
.tl-dot {
  position: absolute; left: -1.2rem; top: .3rem; width: 10px; height: 10px;
  border-radius: 50%; background: var(--primary-2);
  box-shadow: 0 0 0 3px var(--glass);
}
.tl-dot-success { background: var(--success); }
.tl-dot-danger { background: var(--danger); }
.tl-dot-warning { background: var(--warning); }
.tl-title { font-weight: 600; }

/* Article 23 reporting stepper. */
.stepper { display: flex; gap: var(--sp-4); flex-wrap: wrap; margin-top: .6rem; }
.stepper .step {
  flex: 1 1 14rem; display: flex; gap: var(--sp-2); align-items: flex-start;
  padding: var(--sp-3); border-radius: var(--r-md); border: 1px solid var(--glass-border);
  background: var(--glass);
}
.step-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--ink-faint); margin-top: .35rem; flex: none; }
.step-success .step-dot { background: var(--success); }
.step-danger .step-dot { background: var(--danger); animation: pulse-dot 1.6s ease infinite; }
.step-warning .step-dot { background: var(--warning); }
.step-info .step-dot { background: var(--primary-2); }
.step-title { font-weight: 600; margin-bottom: .15rem; }

/* Threat pulse (Lookout). */
.pulse-card { border-left: 3px solid var(--success); }
.pulse-elevated { border-left-color: var(--warning); }
.pulse-critical { border-left-color: var(--danger); }
.pulse-unknown { border-left-color: var(--ink-faint); }
.pulse-head { display: flex; align-items: center; gap: var(--sp-2); }
.pulse-critical .live-dot { background: var(--danger); }
.pulse-elevated .live-dot { background: var(--warning); }
.pulse-unknown .live-dot { background: var(--ink-faint); animation: none; }
.pulse-stats { display: flex; gap: var(--sp-2); flex-wrap: wrap; margin-top: .6rem; }
.pulse-feed { margin-top: .8rem; display: grid; gap: var(--sp-1); }
.pulse-row {
  display: flex; gap: var(--sp-3); align-items: baseline; flex-wrap: wrap;
  padding: .45rem .6rem; border-radius: var(--r-md); background: var(--input-bg);
  border-left: 2px solid var(--warning);
}
html.js-motion .pulse-row { animation: fadeInUp .4s ease both; }
.pulse-row-critical { border-left-color: var(--danger); }

/* Score meter (indicator threat score). */
.score-meter {
  display: inline-block; vertical-align: middle; width: 64px; height: 6px;
  border-radius: var(--r-xs); background: var(--track); overflow: hidden; margin-right: .35rem;
}
.score-fill { height: 100%; max-width: 100%; border-radius: var(--r-xs); background: linear-gradient(90deg, var(--warning), var(--danger)); animation: bar-grow .8s ease both; }

/* Protection status (Seawall). */
.defence-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  gap: var(--sp-2); margin-top: .8rem;
}
.defence {
  display: flex; gap: var(--sp-2); align-items: flex-start;
  padding: var(--sp-2) var(--sp-3); border-radius: var(--r-md);
  border: 1px solid var(--glass-border); background: var(--glass);
}
html.js-motion .defence { animation: fadeInUp .4s ease both; }
.defence-light {
  width: 10px; height: 10px; border-radius: 50%; margin-top: .4rem; flex: none;
  background: var(--ink-faint); box-shadow: 0 0 0 3px var(--glass);
}
.defence-on .defence-light { background: var(--success); box-shadow: 0 0 8px var(--success); }
.defence-off { opacity: .65; }
.defence-label { font-weight: 600; font-size: var(--fs-md); }
.router-shield { margin-bottom: 1rem; }

/* Redundancy gap highlight. */
.cap-row-gap { border-left: 3px solid var(--danger); }

/* Plan detail step list. */
.plan-steps { margin: .4rem 0 .6rem 1.1rem; display: grid; gap: var(--sp-1); }

/* ═══════════════════════════════════════════════════════════════════════════
   2026-07 visual refresh — spacing rhythm, deeper glass, entrance motion.
   Token-driven so both themes inherit it; the print overrides still win.
   ═══════════════════════════════════════════════════════════════════════════ */

:root { --card-sheen: rgba(255, 255, 255, .07); }
:root[data-theme="light"] { --card-sheen: rgba(255, 255, 255, .75); }

/* ---- layout & margins ---- */
/* Centre the reading column instead of hugging the sidebar, and give the page
   real breathing room. */
.app-content {
  margin-inline: auto;
  max-width: 1280px;
  padding: 2.4rem 2.8rem 3.4rem;
}
@media (max-width: 700px) { .app-content { padding: 1.4rem 1.1rem 2rem; } }

.page-head { margin-bottom: 1.7rem; }
.page-head h1 { letter-spacing: -.015em; }
.page-head .muted { max-width: 70ch; }

/* Consistent vertical rhythm between top-level page sections. */
.page-content-anim > .glass-card,
.page-content-anim > .cap-list,
.page-content-anim > .capability-grid,
.page-content-anim > .table-wrap { margin-bottom: 1.25rem; }
.page-content-anim > .chip-row { margin-bottom: 1.1rem; }
.grid-2 { gap: var(--sp-4); margin-bottom: 1.25rem; }
.stat-row { gap: var(--sp-4); margin-bottom: 1.5rem; }

/* ---- deeper glass ---- */
/* Every bare section card gets padding (content used to sit right on the
   border), via `:where()` so it has ZERO specificity: any variant that sets
   its own padding (.stat, .cap-row, .table-wrap, .form-card, .detail, .empty,
   .auth-card, .ex-modal-card, dash-*) keeps winning no matter where it sits
   in the cascade. The surface treatment applies to all of them. */
:where(.glass-card) { padding: var(--sp-4) var(--sp-5); }
/* The 2026-07 pass re-applied a blur and a diagonal sheen here, at the tail of
   the file, where it silently overrode the surface rule above. Both are gone:
   the panel treatment is now declared once, next to `.glass-card` itself. */
.cmdk { padding: 0; } /* the palette lays itself out with its own rows */

/* Section headings inside cards. */
h2.cell-title { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; font-size: var(--fs-base); margin: 0 0 .3rem; }
h2.cell-title .ti { color: var(--primary-2); opacity: .9; }
.glass-card > h3.small { margin: 1.1rem 0 .35rem; }

/* Stat tiles: fill the row evenly. Only a tile that IS a link reacts to the
   pointer: a hover on a figure that goes nowhere promised a click. A linked
   tile keeps ink figures (the global link colour turned five of seven Training
   figures blue and drowned the one red "overdue"), loses the global `a:hover`
   underline, and carries a small chevron (`.stat-go`) to say it opens. */
.stat { flex: 1 1 150px; max-width: 250px; transition: transform .18s ease, border-color .18s ease, box-shadow .18s ease, background .16s ease; }
a.stat { color: var(--ink); }
a.stat:hover { text-decoration: none; transform: translateY(-2px); border-color: var(--primary); }
.stat-go { position: absolute; top: .7rem; right: .75rem; font-size: var(--fs-md); color: var(--ink-faint); transition: transform .16s ease, color .16s ease; }
a.stat:hover .stat-go { color: var(--ink-muted); transform: translateX(2px); }

/* Tables: an inset well inside their card. */
.table-wrap { padding: .45rem .55rem; border-radius: var(--r-md); }
.glass-card .table-wrap { background: var(--input-bg); border: 1px solid var(--row-border); margin-top: .6rem; }

/* Interactive rows glide right and pick up their accent. */
a.cap-row { transition: transform .16s ease, border-color .18s ease, background .18s ease, box-shadow .18s ease; }
a.cap-row:hover { transform: translateX(4px); box-shadow: var(--shadow-2); }

/* ---- entrance motion ---- */
/* Stagger the page sections in instead of animating the whole page as one
   slab; htmx-swapped fragments re-use the same curve so live updates feel
   native rather than flashy. */
.page-content-anim { animation: none; }
.page-content-anim > * { animation: fadeInUp .5s cubic-bezier(.22, 1, .36, 1) both; }
.page-content-anim > *:nth-child(2) { animation-delay: .05s; }
.page-content-anim > *:nth-child(3) { animation-delay: .1s; }
.page-content-anim > *:nth-child(4) { animation-delay: .15s; }
.page-content-anim > *:nth-child(5) { animation-delay: .2s; }
.page-content-anim > *:nth-child(6) { animation-delay: .25s; }
.page-content-anim > *:nth-child(n+7) { animation-delay: .3s; }

/* Swapped-in fragments fade rather than slide: the live polls (router gauges,
   threat pulse) re-render every 20-30s and repeated movement reads as jitter. */
@keyframes fragment-fade-in { from { opacity: 0; } to { opacity: 1; } }
.htmx-added { animation: fragment-fade-in .3s ease both; }
.htmx-swapping { opacity: 0; transition: opacity .12s ease; }
/* A polled container (Lookout pulse, router gauges, command progress,
   quarantine status) re-renders every few seconds. Nothing in it fades, slides
   or re-runs a fill on each poll: the numbers change in place. Pair with
   `settle:0ms` on the hx-swap. */
.is-live, .is-live *:not(.spinner) { animation: none !important; transition: none !important; }
/* The container itself also skips the scroll reveal's 18px offset, which an
   outerHTML swap would otherwise re-apply until motion.js adds `.is-in`. */
.is-live { opacity: 1 !important; transform: none !important; }
.is-live .htmx-added, .is-live .htmx-settling { opacity: 1 !important; }

@keyframes row-in {
  from { opacity: 0; transform: translateY(4px); }
  to { opacity: 1; transform: none; }
}
html.js-motion .data-table tbody tr { animation: row-in .3s ease both; }
.data-table tbody tr:nth-child(2) { animation-delay: .03s; }
.data-table tbody tr:nth-child(3) { animation-delay: .06s; }
.data-table tbody tr:nth-child(4) { animation-delay: .09s; }
.data-table tbody tr:nth-child(5) { animation-delay: .12s; }
.data-table tbody tr:nth-child(n+6) { animation-delay: .15s; }

/* ---- chrome details ---- */
/* Themed thin scrollbars. */
* { scrollbar-width: thin; scrollbar-color: var(--track) transparent; }
*::-webkit-scrollbar { width: 9px; height: 9px; }
*::-webkit-scrollbar-thumb { background: var(--track); border-radius: var(--r-sm); }
*::-webkit-scrollbar-thumb:hover { background: var(--hover-bg); }
*::-webkit-scrollbar-track { background: transparent; }

/* Visible, consistent keyboard focus. */
:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }

/* Respect users who asked for less motion — everything still lands, instantly. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
}

/* ---- margin follow-ups ---- */
/* A card's first/last children keep their browser default margins, which pads
   the top and bottom unevenly (the Lookout incident strip, headings in bare
   cards). Kill them — the card's own padding is the spacing. */
.glass-card > :first-child { margin-top: 0; }
.glass-card > :last-child { margin-bottom: 0; }
/* Top-level htmx fragment wrappers (the Lookout pulse) are plain divs, so the
   card-rhythm rule skips them; space the card inside instead. */
.page-content-anim > [hx-get] { margin-bottom: 1.25rem; }
.page-content-anim > [hx-get]:last-child { margin-bottom: 0; }

/* ---- HTMX modal layer ---- */
body.modal-open { overflow: hidden; }
.hx-modal-overlay {
  position: fixed; inset: 0; z-index: 80;
  display: grid; place-items: center; padding: 1.2rem;
  background: rgba(3, 6, 14, .55);
  backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
  animation: fragment-fade-in .18s ease both;
}
:root[data-theme="light"] .hx-modal-overlay { background: rgba(15, 23, 42, .35); }
.hx-modal {
  width: 100%; max-width: 640px; max-height: 86vh; overflow-y: auto;
  padding: var(--sp-4) var(--sp-5);
  animation: modal-pop .22s cubic-bezier(.22, 1, .36, 1) both;
}
@keyframes modal-pop {
  from { opacity: 0; transform: translateY(10px) scale(.985); }
  to { opacity: 1; transform: none; }
}
.hx-modal-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--sp-4); margin-bottom: .8rem;
}
.hx-modal-head h2 { margin: 0; font-size: var(--fs-lg); display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.hx-modal-x {
  flex: none; width: 2rem; height: 2rem; display: grid; place-items: center;
  border-radius: var(--r-sm); border: 1px solid var(--glass-border);
  background: var(--glass); color: var(--ink-muted); cursor: pointer; font: inherit;
}
.hx-modal-x:hover { color: var(--ink); background: var(--hover-bg); }
.hx-modal-foot { display: flex; gap: var(--sp-2); justify-content: flex-end; margin-top: 1rem; flex-wrap: wrap; }
/* The response-action modal is long (evidence, investigate, timeline), and its
   decision buttons sat below the fold at 1440x900. It is a flex column instead:
   the head and the decision footer stay put and only .hx-modal-body scrolls.
   Scoped to .lq-modal; the other .hx-modal users (and the loading card) are
   short and keep the single scrolling card. */
.hx-modal.lq-modal { display: flex; flex-direction: column; overflow: hidden; padding: 0; }
.lq-modal > .hx-modal-head { flex: none; padding: var(--sp-4) var(--sp-5) 0; }
.lq-modal > .hx-modal-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 0 var(--sp-5) var(--sp-4); }
.lq-modal > .hx-modal-foot {
  flex: none; margin: 0; padding: var(--sp-3) var(--sp-5);
  background: var(--glass); border-top: 1px solid var(--glass-border);
}

/* Rows that open a modal advertise it, as do rows that open their record
   (tr[data-href], handled in portal.js). */
.row-modal, .data-table tr[data-href] { cursor: pointer; }
.row-modal:hover .cell-title, .data-table tr[data-href]:hover .cell-title { color: var(--link); }

/* ======================================================================
   HTMX loading indicators

   htmx marks only the element that issued a request (.htmx-request); it has
   no notion of "the app is busy". Almost none of this portal's htmx call
   sites carried an hx-indicator, so a slow filter, a paged table or a detail
   modal gave no feedback at all. The states below are driven centrally from
   portal.js, so a newly added hx-get inherits them without opting in.
   ====================================================================== */

/* Route progress: a thin bar pinned to the top of the viewport for as long as
   a request is in flight. portal.js toggles .is-active (rather than htmx's own
   .htmx-request) because the bar is shared by every request, not bound to one
   hx-indicator, and it is held back ~140ms so quick responses never flash it. */
.route-progress {
  position: fixed; top: 0; left: 0; right: 0; height: 3px; z-index: 4000;
  pointer-events: none; overflow: hidden; opacity: 0; transition: opacity .2s ease;
}
.route-progress.is-active { opacity: 1; }
.route-progress::before {
  content: ""; position: absolute; top: 0; left: -40%; height: 100%; width: 40%;
  border-radius: 0 var(--r-xs) var(--r-xs) 0;
  background: linear-gradient(90deg, transparent, var(--primary-2) 35%, var(--primary) 75%, transparent);
  box-shadow: 0 0 10px var(--primary-2);
}
.route-progress.is-active::before { animation: route-progress-slide 1.05s cubic-bezier(.4, 0, .2, 1) infinite; }
@keyframes route-progress-slide { 0% { left: -40%; } 100% { left: 100%; } }

/* The region about to be replaced. Dimmed and inert so a half-stale table
   cannot be clicked or sorted while its replacement is on the way. portal.js
   never applies this to a region containing the element that triggered the
   request — the filter bars re-query on keyup, and disabling the input the
   user is still typing into would be worse than showing nothing. */
.hx-busy { opacity: .45; pointer-events: none; cursor: progress; transition: opacity .15s ease; }

/* The control that issued the request grows an inline spinner. htmx puts
   .htmx-request on the issuing element itself for a button, or on the <form>
   when a submit fires, so both shapes are covered here. pointer-events is the
   double-submit guard: unlike `disabled` it cannot drop the submitter's
   name/value from the serialised request. The four tool forms already render
   their own "Looking up…" indicator through hx-indicator; they are excluded so
   they do not end up with two. */
button.htmx-request:not([hx-indicator]),
a.btn.htmx-request:not([hx-indicator]),
form.htmx-request:not([hx-indicator]) button[type="submit"] {
  position: relative; pointer-events: none; opacity: .8;
}
button.htmx-request:not([hx-indicator])::after,
a.btn.htmx-request:not([hx-indicator])::after,
form.htmx-request:not([hx-indicator]) button[type="submit"]::after {
  content: ""; display: inline-block; width: .85em; height: .85em; margin-left: .45em;
  border: 2px solid currentColor; border-top-color: transparent; border-radius: 50%;
  vertical-align: -.1em; animation: spin .7s linear infinite;
}

/* Modal placeholder. Row clicks target the empty #modal-root, so without this
   the overlay stayed invisible until the fragment landed and the click read as
   having done nothing. portal.js injects this immediately and the response
   replaces it. */
.hx-modal-loading { display: grid; place-items: center; gap: var(--sp-3); padding: 2.8rem 3.4rem; text-align: center; color: var(--ink-muted); }
.hx-modal-loading .spinner { width: 1.7rem; height: 1.7rem; border-width: 3px; }

/* ==========================================================================
   Status timelines
   --------------------------------------------------------------------------
   The bar behind every "was it up?" question. Rendered from a run-length
   encoded history strip that rides on the subject's own row, so the markup is
   a handful of proportional spans and nothing here needs JavaScript.

   The 1px flex gap is load-bearing, not decoration: it draws a hairline at each
   state change, which is what makes a run of interruptions legible at a glance
   even when each one is a sliver of a 90-day window. For the same reason a
   non-healthy segment carries a minimum width — a one-hour outage is 0.05% of
   a quarter and would otherwise render as literally nothing.
   ========================================================================== */
.status-timeline { display: flex; flex-direction: column; gap: var(--sp-1); width: 100%; }

.hist-bar {
  display: flex;
  gap: 1px;
  width: 100%;
  height: 22px;
  border-radius: var(--r-sm);
  overflow: hidden;
  background: var(--track);
  box-shadow: inset 0 0 0 1px var(--glass-border);
}

/* A fragment is a real <button>: it is clickable, and the outages are keyboard
   reachable (healthy stretches carry tabindex="-1", so tabbing walks you from
   interruption to interruption instead of through 180 identical green slots). */
.hist-seg {
  display: block;
  height: 100%;
  min-width: 2px;
  padding: 0;
  border: 0;
  border-radius: 0;
  appearance: none;
  font: inherit;
  color: inherit;
  cursor: pointer;
  transition: filter .12s ease;
}
.hist-seg:hover { filter: brightness(1.35); }
.hist-seg:focus-visible { outline: 2px solid var(--ink); outline-offset: 1px; z-index: 1; }
.hist-seg.is-active { filter: brightness(1.5); box-shadow: inset 0 0 0 1px var(--ink); }

/* An interruption must stay clickable however short it was. */
.hist-seg.hist-event { min-width: 4px; }

.hist-ok { background: var(--success); }
.hist-degraded { background: var(--warning); }
.hist-down { background: var(--danger); }
/* Unsampled time is a gap in the record, and is drawn as one — never as green. */
.hist-unknown {
  background: repeating-linear-gradient(
    135deg,
    var(--track) 0 4px,
    transparent 4px 8px
  );
}

.hist-bar-empty {
  background: repeating-linear-gradient(
    135deg,
    var(--track) 0 4px,
    transparent 4px 8px
  );
}

.hist-axis {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  font-size: var(--fs-xs);
  color: var(--ink-faint);
}
.hist-axis-mid { opacity: .8; }

.hist-caption { display: flex; align-items: baseline; gap: var(--sp-2); flex-wrap: wrap; }
.hist-uptime { font-weight: 700; font-size: var(--fs-base); color: var(--ink); }
.hist-coverage { border-bottom: 1px dotted var(--ink-faint); cursor: help; }

/* -- In-row variant ------------------------------------------------------- */
.status-timeline-compact .hist-bar { height: 10px; border-radius: var(--r-xs); }
.hist-inline { display: flex; flex-direction: column; gap: var(--sp-1); min-width: 180px; flex: 1 1 220px; }
.hist-inline-meta { font-size: var(--fs-xs); }

/* -- Window selector ------------------------------------------------------ */
.hist-windows { display: flex; align-items: center; gap: var(--sp-1); flex-wrap: wrap; }
.hist-window {
  padding: .2rem .55rem;
  border: 1px solid var(--glass-border);
  border-radius: var(--r-pill);
  background: var(--glass);
  color: var(--ink-muted);
  font-size: var(--fs-xs);
  text-decoration: none;
  transition: background .15s ease, color .15s ease, border-color .15s ease;
}
.hist-window:hover { background: var(--hover-bg); color: var(--ink); }
.hist-window.is-active {
  background: var(--primary);
  border-color: var(--primary);
  color: #fff;
}

.hist-rollup { display: flex; flex-direction: column; gap: var(--sp-2); }

/* A device/router card grows a timeline row under its metadata. `flex-basis:100%`
   so it takes its own line rather than competing with the title and meta blocks
   for space on the card's single flex row. */
.cap-row-timeline { flex: 1 0 100%; margin-top: .5rem; }

@media (max-width: 700px) {
  .hist-inline { min-width: 0; }
  .hist-axis { font-size: var(--fs-xs); }
}

/* One host's protection history: a name above its own bar. */
.hist-host { display: flex; flex-direction: column; gap: var(--sp-1); padding: .55rem 0; border-top: 1px solid var(--row-border); }
.hist-host:first-of-type { border-top: 0; }
.hist-host-name { font-weight: 600; font-size: var(--fs-md); }

/* ==========================================================================
   Selectable dashboards (`/app`)
   ==========================================================================
   The front door is a picker over several plan-gated dashboards. Everything
   here is `dbd-*` (the dashboard *chooser* and its panels) or `dchart-*`/`dbar-*`
   (the charts). The charts are drawn from numbers the server already solved:
   the portal ships no charting library, so a donut is an SVG ring with two
   stroke properties per wedge and a ranking is a flexbox row with a width. Both
   take their colours from the theme tokens, so light and dark need no second
   palette.
   ========================================================================== */

/* The head is raised into its own stacking context on purpose. Every card
   below it is a `.glass-card`, and `backdrop-filter` makes each of those a
   stacking context of its own that composites above a plain z-index — so the
   open picker panel was painted *underneath* the banner and the dashboard
   cards (it looked like the page was bleeding through the menu). Raising the
   head lifts the whole picker, panel included, over its later siblings. The
   number stays small: `.page-content-anim` is itself a z-index:1 context, so
   nothing here can ever climb over the sticky app header. */
.dbd-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-4); flex-wrap: wrap;
  position: relative; z-index: 5; }

/* ---- The picker ---- */
.dbd-pick { position: relative; flex: none; }
.dbd-pick > summary {
  list-style: none; cursor: pointer; display: inline-flex; align-items: center; gap: var(--sp-2);
  padding: .6rem 1rem; border-radius: var(--r-sm); border: 1px solid var(--glass-border);
  background: var(--glass); color: var(--ink); font-weight: 600; user-select: none;
}
.dbd-pick > summary::-webkit-details-marker { display: none; }
.dbd-pick > summary::marker { content: ""; }
.dbd-pick > summary:hover { border-color: var(--primary-2); }
.dbd-pick[open] > summary { border-color: var(--primary-2); color: var(--link); }
.dbd-pick-current { white-space: nowrap; }
.dbd-pick-panel {
  position: absolute; right: 0; top: calc(100% + 8px); z-index: 60; width: 22rem; max-width: 88vw;
  background: var(--pop-bg); border: 1px solid var(--glass-border); border-radius: var(--r-md);
  padding: 6px; box-shadow: var(--shadow-2); max-height: 70vh; overflow-y: auto;
}
.dbd-pick-item {
  display: flex; align-items: center; gap: var(--sp-3); padding: .6rem .7rem; border-radius: var(--r-sm);
  color: var(--ink); text-decoration: none;
}
.dbd-pick-item:hover { background: var(--hover-bg); text-decoration: none; }
.dbd-pick-item > .ti:first-child { color: var(--accent, var(--primary-2)); font-size: var(--fs-lg); flex: none; }
.dbd-pick-item.is-active { background: var(--hover-bg); }
.dbd-pick-text { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.dbd-pick-blurb { font-size: var(--fs-xs); color: var(--ink-muted); }

/* ---- Panels ---- */
.dbd-panel { padding: var(--sp-4); margin-bottom: 1.2rem; }
.dbd-panel-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-4); flex-wrap: wrap; margin-bottom: 1rem; }
.dbd-panel-title { margin: .1rem 0 .2rem; font-size: var(--fs-xl); }
.dbd-stat-row-tight { margin-top: .2rem; }
.dbd-stat-sub { margin-top: .15rem; }
.dbd-note { margin: .9rem 0 0; display: flex; align-items: center; gap: var(--sp-1); }
.dbd-empty-icon { font-size: var(--fs-4xl); display: block; margin-bottom: .3rem; opacity: .6; }

/* Tone accents for a headline number. Bare `.dbd-tone-` (no tone given) is
   deliberately inert — it inherits the default ink. */
.dbd-tone-ok { color: var(--success); }
.dbd-tone-warn { color: var(--warning); }
.dbd-tone-crit { color: var(--danger); }

/* The threat-level banner on the Security dashboard. */
.dbd-level { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-3) var(--sp-4); border-radius: var(--r-md);
  border: 1px solid var(--glass-border); background: var(--glass); margin-bottom: 1.1rem; }
.dbd-level > .ti { font-size: var(--fs-4xl); flex: none; color: var(--ink-muted); }
.dbd-level p { margin: .1rem 0 0; }
.dbd-level-clear > .ti { color: var(--success); }
.dbd-level-elevated { border-color: var(--warning); }
.dbd-level-elevated > .ti { color: var(--warning); }
.dbd-level-critical { border-color: var(--danger); }
.dbd-level-critical > .ti { color: var(--danger); }
.dbd-level-unknown > .ti { color: var(--ink-faint); }

/* The access-review callout. */
.dbd-review { display: flex; align-items: center; gap: var(--sp-3); padding: .9rem 1.1rem; margin-top: .5rem; }
.dbd-review > .ti { font-size: var(--fs-3xl); flex: none; color: var(--success); }
.dbd-review p { margin: .15rem 0 0; }
.dbd-review-due { border-color: var(--warning); }
.dbd-review-due > .ti { color: var(--warning); }

/* ---- Charts ---- */
.dchart-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: var(--sp-4); margin: .4rem 0 1.2rem; }
.dchart { display: flex; flex-direction: column; gap: var(--sp-2); min-width: 0; }

.dchart-ring-wrap { position: relative; align-self: center; width: 132px; height: 132px; }
/* Same geometry as the router gauges (r=52, circumference 326.726), rotated so
   the first wedge starts at twelve o'clock. */
.dchart-ring { width: 132px; height: 132px; transform: rotate(-90deg); }
.dchart-track { fill: none; stroke: var(--track); stroke-width: 13; }
.dchart-arc { fill: none; stroke-width: 13; }
.dchart-center { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; pointer-events: none; }
.dchart-total { font-size: var(--fs-2xl); font-weight: 700; }

.dchart-legend { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--sp-1); }
.dchart-legend li { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-sm); }
.dchart-legend b { margin-left: auto; font-variant-numeric: tabular-nums; }
.dchart-dot { width: 9px; height: 9px; border-radius: 50%; flex: none; }
.dchart-legend-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dchart-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--sp-1);
  min-height: 132px; color: var(--ink-faint); font-size: var(--fs-sm); text-align: center; }
.dchart-empty .ti { font-size: var(--fs-4xl); opacity: .6; }

.dbars { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--sp-2); }
.dbars li { display: grid; grid-template-columns: minmax(0, 8rem) 1fr auto; align-items: center; gap: var(--sp-2); }
.dbar-label { font-size: var(--fs-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dbar-track { height: 8px; border-radius: var(--r-xs); background: var(--track); overflow: hidden; }
.dbar-fill { display: block; height: 100%; border-radius: var(--r-xs); }
.dbar-val { font-size: var(--fs-sm); font-weight: 600; font-variant-numeric: tabular-nums; }

/* A centre figure that is not a count ("12.4 GB") is printed, not counted up,
   and has to fit inside the ring. */
.dchart-total-text { font-size: var(--fs-base); }
/* A time chart wants the width a donut does not: it takes the whole row.
   `1 / -1` rather than `span 2`, which on a one-track (narrow) grid would mint
   an implicit second column and push the panel wider than the screen. */
.dchart-wide { grid-column: 1 / -1; }
.dchart-legend-row { flex-direction: row; flex-wrap: wrap; gap: var(--sp-1) var(--sp-4); }
.dchart-legend-row li { flex: none; }
.dchart-legend-row b { margin-left: .25rem; }

/* ---- Area chart: server-solved SVG paths on a 300x100 viewBox ---- */
.dchart-area { display: block; width: 100%; height: 150px; overflow: visible; }
.dchart-area-grid { stroke: var(--track); stroke-width: 1; vector-effect: non-scaling-stroke; }
.dchart-area-fill { opacity: .16; stroke: none; }
.dchart-area-line { fill: none; stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; vector-effect: non-scaling-stroke; }
.dchart-axis { display: flex; justify-content: space-between; gap: var(--sp-2); font-size: var(--fs-xs); color: var(--ink-faint);
  font-variant-numeric: tabular-nums; }

/* ---- Columns: daily / weekly counts, stacked or grouped, drawn with flex ---- */
.dcols { display: flex; align-items: stretch; gap: 3px; height: 150px; }
.dcol { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; gap: var(--sp-1); }
/* column-reverse: the first series sits on the floor and the rest pile up. */
.dcol-bar { flex: 1; display: flex; flex-direction: column-reverse; border-radius: var(--r-xs) var(--r-xs) 0 0; overflow: hidden;
  background: linear-gradient(to top, var(--track) 1px, transparent 1px); }
.dcol-bar.is-grouped { flex-direction: row; align-items: flex-end; gap: 2px; }
.dcol-seg { display: block; flex: none; min-height: 0; }
.dcol-bar.is-grouped .dcol-seg { flex: 1 1 0; border-radius: var(--r-xs) var(--r-xs) 0 0; }
.dcol:hover .dcol-seg { filter: brightness(1.12); }
.dcol-label { height: 1rem; font-size: var(--fs-xs); color: var(--ink-faint); text-align: center; white-space: nowrap;
  overflow: visible; font-variant-numeric: tabular-nums; }

/* Small multiline cells in the "needs a look" tables. */
.dbd-reasons { margin: 0; padding: 0; list-style: none; display: flex; flex-wrap: wrap; gap: var(--sp-1); }

@media (max-width: 700px) {
  .dbd-pick-panel { width: min(22rem, 92vw); }
  .dbars li { grid-template-columns: minmax(0, 6rem) 1fr auto; }
  .dcols { gap: 2px; }
}

/* Detail panel for one fragment. Positioned in the viewport by timeline.js and
   torn down on scroll/resize/htmx swap rather than re-measured. */
.hist-pop {
  position: fixed;
  z-index: 1200;
  min-width: 210px;
  padding: .7rem .85rem;
  border: 1px solid var(--glass-border);
  border-radius: var(--r-md);
  background: var(--pop-bg);
  box-shadow: var(--shadow-2);
  font-size: var(--fs-sm);
  color: var(--ink);
}
.hist-pop-head { display: flex; align-items: center; gap: var(--sp-1); margin-bottom: .45rem; padding-right: 1rem; }
.hist-pop-dot { width: 10px; height: 10px; border-radius: var(--r-xs); flex: 0 0 auto; }
.hist-pop-row { display: flex; justify-content: space-between; gap: var(--sp-4); padding: .12rem 0; }
.hist-pop-row span:first-child { color: var(--ink-muted); }
.hist-pop-close {
  position: absolute;
  top: .3rem;
  right: .45rem;
  border: 0;
  background: none;
  color: var(--ink-faint);
  font-size: var(--fs-lg);
  line-height: 1;
  cursor: pointer;
}
.hist-pop-close:hover { color: var(--ink); }

/* The interactive rollup bar: equal-width sections rather than proportional
   runs, so every section is a click target of the same size. The wider gap is
   what makes them read as separate sections instead of one continuous bar. */
.hist-bar-slots { gap: 2px; height: 26px; }
.hist-bar-slots .hist-seg { flex: 1 1 0; min-width: 0; border-radius: 2px; }
.hist-bar-drill .hist-seg:hover { filter: brightness(1.4); transform: scaleY(1.12); }
.hist-bar-drill .hist-seg { transition: filter .12s ease, transform .12s ease; }

/* Time-travel banner: the table below is a reconstruction, and it says so. */
.hist-asof {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex-wrap: wrap;
  padding: .55rem .8rem;
  margin-bottom: .8rem;
  border: 1px solid var(--warning);
  border-radius: var(--r-md);
  background: color-mix(in srgb, var(--warning) 12%, transparent);
  font-size: var(--fs-sm);
}
.hist-asof b { font-weight: 700; }

/* Only ever seen in a past snapshot: the recorded history does not reach that
   moment. Dashed, so it can never be mistaken for a real "offline". */
.pill-unknown {
  background: transparent;
  color: var(--ink-faint);
  border: 1px dashed var(--glass-border);
}
.hist-pop-detail { margin-top: .4rem; padding-top: .4rem; border-top: 1px solid var(--glass-border); color: var(--ink-muted); font-size: var(--fs-xs); }

/* Remediation ("How to fix") — the command an operator pastes. Monospace on a
   recessed surface so it reads as something to copy, not prose. */
.fix-block { margin-top: 1rem; padding-top: .9rem; border-top: 1px solid var(--glass-border); }
.fix-block h3 { margin: 0 0 .5rem; }
.fix-cmd { margin: .35rem 0 .75rem; padding: .65rem .8rem; border-radius: var(--r-sm);
           background: var(--glass-strong); border: 1px solid var(--glass-border);
           overflow-x: auto; }
/* The global `code` chip (own background, padding, radius) must not draw a
   second box inside the recessed block. */
.fix-cmd code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
                font-size: var(--fs-sm); line-height: var(--lh-body); white-space: pre;
                background: none; padding: 0; border-radius: 0; }
.fix-cmd-quiet { opacity: .78; }

/* =========================================================================
   Chartroom components — instrument cluster, command band, setup checklist,
   activity log, collapsible rail.

   Two ideas do most of the work here:

   1. Five figures are ONE instrument, not five cards. `.cluster` is a single
      slab divided by hairlines; five floating cards read as five unrelated
      alerts and the eye stops finding the one that matters.
   2. The activity feed is a LOG. A fixed monospaced time column against a rule
      is what makes eight events scannable — and the section it links to is
      called the Logbook, so it is the shape the name already implies.
   ========================================================================= */

/* ---- indicator lamp ----------------------------------------------------- */
/* A rounded square, not a dot: it reads as equipment rather than a bullet. */
.led { width: 7px; height: 7px; flex: none; border-radius: 2px; background: var(--ink-faint); }
.led-ok { background: var(--success); }
.led-warn { background: var(--warning); }
.led-crit { background: var(--danger); }

/* ---- instrument cluster ------------------------------------------------- */
.cluster {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(192px, 1fr));
  border: 1px solid var(--glass-border);
  border-radius: var(--r-md);
  overflow: hidden;
  background: var(--glass);
  box-shadow: inset 0 1px 0 var(--card-sheen), var(--shadow-1);
  margin-bottom: 1.2rem;
}
.cell {
  display: flex; flex-direction: column; gap: var(--sp-2);
  padding: 1.1rem 1.2rem 1.15rem;
  border-right: 1px solid var(--line-soft);
  color: inherit;
  transition: background .16s ease;
}
.cell:last-child { border-right: 0; }
a.cell:hover { background: var(--hover-bg); text-decoration: none; }
.cell-top { display: flex; align-items: center; gap: var(--sp-2); }
.cell-top .lbl { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cell-val { display: flex; align-items: flex-end; gap: var(--sp-2); }
.cell-num {
  font-family: var(--f-data); font-variant-numeric: tabular-nums;
  font-size: var(--fs-figure); font-weight: 500; line-height: .95; letter-spacing: -.035em;
}
.cell-crit .cell-num { color: var(--danger); }
.cell-warn .cell-num { color: var(--warning); }
.cell-of { font-family: var(--f-data); font-size: var(--fs-xs); color: var(--ink-faint); padding-bottom: .25rem; }
.cell-foot { display: flex; align-items: center; gap: var(--sp-1); min-height: 1.1rem; }
/* State words are language, so they stay in the text face. */
.state { font-family: var(--f-text); font-size: var(--fs-xs); color: var(--ink-faint); }
.spark { display: block; width: 100%; height: 27px; overflow: visible; }

/* Movement pill. Direction is not sentiment: a rising count of open findings is
   bad and a rising count of protected machines is good, so the caller states
   the tone and the arrow only states which way it moved. */
.delta {
  display: inline-flex; align-items: center; gap: .16rem;
  padding: .2rem .42rem; border-radius: var(--r-sm);
  font-family: var(--f-data); font-size: var(--fs-xs); font-weight: 500;
  font-variant-numeric: tabular-nums; white-space: nowrap;
  background: var(--chip-neutral-bg); color: var(--chip-neutral-fg);
}
.delta .ti { font-size: var(--fs-sm); }
.delta.is-good { background: var(--chip-ok-bg); color: var(--chip-ok-fg); }
.delta.is-bad { background: var(--chip-crit-bg); color: var(--chip-crit-fg); }

/* ---- command band ------------------------------------------------------- */
/* A ruled band, not a card: the page opens with structure rather than a box. */
.band {
  display: flex; align-items: flex-end; gap: var(--sp-5); flex-wrap: wrap;
  padding-bottom: 1.3rem; margin-bottom: 1.3rem;
  border-bottom: 1px solid var(--glass-border);
}
.band-id { flex: 1 1 22rem; min-width: 0; display: flex; flex-direction: column; gap: var(--sp-2); }
.band-id h1 { margin: 0; font-size: clamp(var(--fs-2xl), 2.6vw, var(--fs-4xl)); font-weight: 600; line-height: var(--lh-tight); }
.band-sub { margin: 0; color: var(--ink-muted); font-size: var(--fs-md); max-width: 60ch; }
.band-sub b { font-family: var(--f-data); font-weight: 500; font-variant-numeric: tabular-nums; color: var(--ink); }
.band-actions { display: flex; gap: var(--sp-2); flex-wrap: wrap; }
.tags { display: flex; flex-wrap: wrap; gap: var(--sp-1); margin-top: .15rem; }
.tag {
  display: inline-flex; align-items: center; gap: var(--sp-1);
  padding: .28rem .6rem; border: 1px solid var(--glass-border); border-radius: var(--r-sm);
  background: var(--glass); color: var(--ink-muted);
  font-size: var(--fs-xs); font-weight: 500; line-height: var(--lh-snug);
}
.tag .ti { font-size: var(--fs-md); color: var(--chip-brand-fg); }

/* ---- panel head + body -------------------------------------------------- */
/* `:where(.glass-card)` sets a default padding at zero specificity, so a panel
   that lays itself out in a head and a body just clears it. */
.dbd-panel.has-head { padding: 0; }
.card-head {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--line-soft);
}
.card-head h2, .card-head h3 { margin: 0; font-size: var(--fs-md); font-weight: 600; }
.card-head .lbl { margin-left: auto; }
.card-body { padding: var(--sp-4) var(--sp-4) var(--sp-4); }

/* ---- setup checklist ---------------------------------------------------- */
/* One segment per step, so the meter states the count rather than
   approximating it — the bar IS the tally. */
.steps-meter { display: flex; gap: var(--sp-1); margin-bottom: 1.15rem; }
.steps-meter span { flex: 1; height: 5px; border-radius: 2px; background: var(--track); }
.steps-meter span.on { background: var(--primary); }
.step-group { margin: 1.1rem 0 .48rem; }
.steps-meter + .step-group { margin-top: 0; }
.steps { list-style: none; margin: 0; padding: 0; }
/* Scoped to .steps: the Art. 23 stepper above also uses .step. */
.steps .step {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: .52rem .55rem .52rem .15rem; border-radius: var(--r-sm);
  color: inherit;
}
.steps a.step:hover { background: var(--hover-bg); text-decoration: none; }
.step-box {
  display: grid; place-items: center; width: 19px; height: 19px; flex: none;
  border: 1px solid var(--glass-border); border-radius: var(--r-xs);
  background: var(--input-bg); color: transparent; font-size: var(--fs-sm);
}
.steps .step.is-done .step-box { background: var(--chip-ok-bg); border-color: transparent; color: var(--chip-ok-fg); }
.step-text { flex: 1; min-width: 0; }
.step-name { display: block; font-family: var(--f-display); font-size: var(--fs-sm); font-weight: 600; letter-spacing: -.012em; }
.steps .step.is-done .step-name { color: var(--ink-muted); font-weight: 500; }
.step-hint { display: block; font-size: var(--fs-xs); color: var(--ink-faint); }
.step-go { flex: none; font-size: var(--fs-xs); font-weight: 500; color: var(--primary); white-space: nowrap; }
/* The checklist leads the dashboard at full width while setup is open, so a
   group's steps sit side by side rather than in one long column. */
.setup-card .steps { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr)); column-gap: var(--sp-4); }
/* Finished steps fold into one "N done · Show" line under the open ones. */
.steps-done { margin-top: .9rem; padding-top: .55rem; border-top: 1px solid var(--glass-border); }
.steps-done > summary { cursor: pointer; list-style: none; padding: .3rem 0; font-size: var(--fs-sm); color: var(--ink-muted); user-select: none; }
.steps-done > summary::-webkit-details-marker { display: none; }
.steps-done > summary:hover { color: var(--ink); }
.steps-done-show, .steps-done-hide { color: var(--link); font-weight: 500; }
.steps-done:not([open]) .steps-done-hide,
.steps-done[open] .steps-done-show { display: none; }
.steps-done[open] > summary { margin-bottom: .35rem; }

/* ---- activity log ------------------------------------------------------- */
.log { list-style: none; margin: 0; padding: 0; }
.log-row { display: grid; grid-template-columns: 3.3rem 1fr; gap: var(--sp-3); }
.log-time {
  padding: .58rem .58rem .58rem 0; text-align: right;
  border-right: 1px solid var(--line-soft);
  font-family: var(--f-data); font-size: var(--fs-xs); line-height: var(--lh-body);
  color: var(--ink-faint); white-space: nowrap;
}
.log-entry { display: flex; align-items: flex-start; gap: var(--sp-2); padding: .58rem 0; min-width: 0; }
.log-ico {
  display: grid; place-items: center; width: 21px; height: 21px; flex: none;
  border-radius: var(--r-sm); font-size: var(--fs-sm);
  background: var(--chip-neutral-bg); color: var(--chip-neutral-fg);
}
.log-audit .log-ico { background: var(--chip-brand-bg); color: var(--chip-brand-fg); }
.log-ok .log-ico { background: var(--chip-ok-bg); color: var(--chip-ok-fg); }
.log-warn .log-ico { background: var(--chip-warn-bg); color: var(--chip-warn-fg); }
.log-crit .log-ico { background: var(--chip-crit-bg); color: var(--chip-crit-fg); }
.log-text { font-size: var(--fs-sm); line-height: var(--lh-body); min-width: 0; overflow-wrap: break-word; color: var(--ink-muted); }
/* The actor and the object are names, so they take the display face; the verb
   between them is language and stays in the text face. */
.log-text b { font-family: var(--f-display); font-weight: 600; color: var(--ink); letter-spacing: -.012em; }
.log-text em { font-style: normal; }

/* Checklist and log share a row on a wide screen and stack on a narrow one. */
.dash-split { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 6fr); gap: var(--sp-4); margin-bottom: 1.3rem; }
@media (max-width: 1080px) { .dash-split { grid-template-columns: 1fr; } }

/* ---- rail: collapse + theme control ------------------------------------- */
.sidebar-foot { margin-top: auto; padding-top: .95rem; border-top: 1px solid var(--line-soft); }
.theme-seg {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 2px;
  padding: 3px; border: 1px solid var(--input-border); border-radius: var(--r-sm);
  background: var(--input-bg);
}
.theme-seg button {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-1);
  padding: .36rem .2rem; border: 0; border-radius: var(--r-sm);
  background: transparent; color: var(--ink-faint); cursor: pointer;
  font-family: var(--f-text); font-size: var(--fs-xs); font-weight: 500; line-height: 1;
}
.theme-seg button .ti { font-size: var(--fs-md); }
.theme-seg button:hover { color: var(--ink); }
.theme-seg button[aria-pressed="true"] { background: var(--glass); color: var(--ink); box-shadow: var(--shadow-1); }

.nav-collapse {
  display: none; align-items: center; justify-content: center;
  width: 25px; height: 25px; margin-left: auto;
  border: 1px solid var(--glass-border); border-radius: var(--r-sm);
  background: transparent; color: var(--ink-faint); cursor: pointer;
}
.nav-collapse:hover { color: var(--ink); background: var(--hover-bg); }
@media (min-width: 821px) { .nav-collapse { display: inline-flex; } }

/* Collapsed state is scoped to the desktop layout: below 820px the sidebar is a
   fixed-width slide-over with its own width, and a 64px icon rail there would
   be a worse version of the menu button that already exists. */
@media (min-width: 821px) {
  body.nav-collapsed .app-shell { grid-template-columns: 64px 1fr; }
  body.nav-collapsed .sidebar { padding-left: .55rem; padding-right: .55rem; overflow-x: hidden; }
  body.nav-collapsed .nav-link span,
  body.nav-collapsed .nav-group-title span,
  body.nav-collapsed .nav-group-title .ngt-caret,
  body.nav-collapsed .theme-seg button span,
  body.nav-collapsed .step-go { display: none; }
  body.nav-collapsed .nav-link { justify-content: center; padding-left: 0; padding-right: 0; }
  body.nav-collapsed .nav-link:hover { padding-left: 0; }
  body.nav-collapsed .nav-link.active::before { left: -.55rem; }
  /* On the rail a group heading is its icon alone (the `title` names it) and
     still opens and shuts the group; a rule sets an open group's pages apart. */
  body.nav-collapsed .nav-group-title { justify-content: center; padding-left: 0; padding-right: 0; }
  body.nav-collapsed .nav-group[open] > ul { border-top: 1px solid var(--line-soft); padding-top: .2rem; }
  body.nav-collapsed .theme-seg { grid-template-columns: 1fr; }
  body.nav-collapsed .nav-collapse { margin: 0 auto; }
  /* The full lockup does not fit a 64px rail: show the bare tile instead, and
     stack it above the button (34px + 25px side by side overflows too). */
  body.nav-collapsed .sidebar .brand-mark .brand-lockup { display: none; }
  body.nav-collapsed .sidebar .brand-mark .sd-logo-tile { display: block; }
  body.nav-collapsed .brand-mark { flex-direction: column; gap: var(--sp-2); }
  body.nav-collapsed .nav-collapse .ti { transform: rotate(180deg); }
}

/* ---- icon centring ------------------------------------------------------ */
/* An icon-font glyph is drawn from the BASELINE, not from the middle of its
   box, so centring the <i> centres its line box and leaves the drawn icon low.
   Measured from the vendored TTF (assets/fonts/tabler/tabler-icons.ttf):
   unitsPerEm 1000, hhea ascender 986, descender -100. The content area is
   therefore 1.086em and, in a 1em line box, the baseline sits 0.943em from the
   top. Tabler's 24x24 design box maps to roughly -101..891 font units, whose
   centre lands 0.048em BELOW the centre of the chip we are centring it in —
   about 1px on a 20px icon, which is exactly enough to look wrong in a small
   square.

   Applied ONLY where an icon is centred alone in a chip. An icon sitting beside
   text (nav links, buttons, table cells) must keep its baseline, because there
   it is aligning with the words next to it.

   `ti-check` needs more: its ink is 0..643 rather than a full icon's ~-18..808,
   because a tick is drawn in the lower-middle of its box. Centring the box
   still leaves the tick reading low in a 19px square, so the tick alone gets
   the extra nudge that puts its INK on centre. */
.icon-btn > .ti,
.step-box > .ti,
.log-ico > .ti,
.cell-top > .ti,
.theme-seg button > .ti,
.nav-collapse > .ti,
.dchart-empty > .ti,
.dbd-empty-icon {
  display: block;
  line-height: 1;
  transform: translateY(-.048em);
}
.step-box > .ti-check { transform: translateY(-.121em); }

/* ---- Bridge → Registers overview (/app/bridge) ----
   The standard page head, then a 3-column tile grid + three explainer cards. Tiles are `.glass-card`s;
   the 6px bar is the share of records in order (green ≥80, amber 40–79, red
   <40, empty track for a register with no records or one we cannot measure).
   `.reg-icon .ti` is forced to a 1em box: Tabler 2.4.0 glyphs carry a 1.411em
   advance with the ink in the left 1em, so an unconstrained icon sits 0.2em
   left of centre in a square tile. */
.reg-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--sp-4); margin-bottom: 1.6rem; }
@media (max-width: 1100px) { .reg-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 700px) { .reg-grid { grid-template-columns: minmax(0, 1fr); } }

.reg-tile {
  display: flex; flex-direction: column; gap: var(--sp-2); padding: var(--sp-4) var(--sp-4); min-width: 0;
  color: var(--ink); transition: transform .12s ease, border-color .15s ease, box-shadow .15s ease;
}
a.reg-tile:hover { text-decoration: none; transform: translateY(-2px); border-color: var(--primary-2); box-shadow: var(--shadow-2); }
.reg-tile-static { cursor: default; }
.reg-tile-head { display: flex; align-items: center; gap: var(--sp-3); min-width: 0; }
.reg-icon {
  flex: none; width: 2.1rem; height: 2.1rem; border-radius: var(--r-sm);
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(61, 157, 255, .12); color: var(--primary-2); font-size: var(--fs-lg);
}
.reg-icon .ti { display: inline-block; width: 1em; }
.reg-tile-name { flex: 1; min-width: 0; font-weight: 600; font-size: var(--fs-base); letter-spacing: -.01em; }
.reg-count { font-family: var(--f-data); font-variant-numeric: tabular-nums; font-size: var(--fs-sm); color: var(--ink-muted); }

.reg-bar { height: 6px; border-radius: var(--r-xs); background: var(--track); overflow: hidden; }
.reg-bar-na { background: repeating-linear-gradient(90deg, var(--track) 0 6px, transparent 6px 10px); }
.reg-bar-fill { display: block; height: 100%; border-radius: var(--r-xs); transition: width .35s ease; }
.reg-bar-ok { background: var(--success); }
.reg-bar-warn { background: var(--warning); }
.reg-bar-bad { background: var(--danger); }
.reg-bar-empty { background: transparent; }

.reg-foot { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-2); flex-wrap: wrap; }
.reg-note { font-size: var(--fs-xs); color: var(--ink-faint); }
.reg-flag { font-size: var(--fs-xs); font-weight: 500; color: var(--warning); white-space: nowrap; }
.reg-sub { font-size: var(--fs-sm); color: var(--ink-muted); }
.reg-keeper { display: inline-flex; align-items: center; gap: var(--sp-1); font-size: var(--fs-xs); color: var(--ink-faint); }
.reg-keeper .ti { display: inline-block; width: 1em; }

.reg-cards { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--sp-4); }
@media (max-width: 900px) { .reg-cards { grid-template-columns: minmax(0, 1fr); } }
.reg-card {
  display: flex; flex-direction: column; gap: var(--sp-2); padding: var(--sp-4) var(--sp-4); min-width: 0;
  color: var(--ink); transition: transform .12s ease, border-color .15s ease;
}
a.reg-card:hover { text-decoration: none; transform: translateY(-2px); border-color: var(--primary-2); }
.reg-card h3 { margin: .2rem 0 0; font-size: var(--fs-base); }
.reg-card p { margin: 0; color: var(--ink-muted); font-size: var(--fs-md); }
.reg-card-foot { margin-top: auto; padding-top: .4rem; font-size: var(--fs-xs); color: var(--ink-faint); }

/* ---- Helm · Identity & access ------------------------------------------ */
/* Helm pages use the standard page head and the app/helm/_chips.html chip
   row (#27); these are the parts of the landing below them. */
.helm-section-head { display: flex; flex-direction: column; gap: .15rem; margin: .4rem 0 .7rem; }

/* The role × area matrix: first column is the role, every other cell is one
   centred glyph. */
.helm-matrix th, .helm-matrix td { text-align: center; }
.helm-matrix th:first-child, .helm-matrix td:first-child { text-align: left; }
.helm-matrix td.mx-role { white-space: nowrap; }
.mx {
  display: inline-grid; place-items: center; width: 1.7rem; height: 1.7rem;
  border-radius: var(--r-sm); font-size: var(--fs-base); line-height: 1; position: relative;
}
.mx > .ti { display: block; line-height: 1; transform: translateY(-.048em); }
.mx > .ti-check { transform: translateY(-.121em); }
.mx-full, .mx-approve, .mx-partial { color: var(--success); background: rgba(46, 207, 154, .13); }
.mx-view { color: var(--link); background: var(--chip-brand-bg); }
.mx-hidden { color: var(--ink-faint); background: transparent; }
.mx-partial::after {
  content: ""; position: absolute; right: 3px; bottom: 3px; width: 5px; height: 5px;
  border-radius: 50%; background: var(--warning);
}
.mx-approve::after {
  content: ""; position: absolute; right: 3px; bottom: 3px; width: 5px; height: 5px;
  border-radius: 50%; background: var(--primary-2);
}
.helm-legend { display: flex; flex-wrap: wrap; gap: var(--sp-4); margin: .7rem 0 1.4rem; font-size: var(--fs-sm); color: var(--ink-muted); }
.helm-legend > span { display: inline-flex; align-items: center; gap: var(--sp-1); }
.helm-legend .mx { width: 1.4rem; height: 1.4rem; font-size: var(--fs-md); }

/* The three promise cards and the "not there" banner. */
.helm-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); gap: var(--sp-4); margin-bottom: 1.2rem; }
.helm-card { padding: var(--sp-4) var(--sp-4); display: flex; flex-direction: column; gap: var(--sp-2); }
.helm-card-icon {
  display: inline-grid; place-items: center; width: 2.2rem; height: 2.2rem;
  border-radius: var(--r-sm); background: var(--chip-brand-bg); color: var(--primary-2); font-size: var(--fs-lg);
}
.helm-card-icon > .ti { display: block; line-height: 1; transform: translateY(-.048em); }
.helm-card h3 { margin: .2rem 0 0; font-size: var(--fs-base); }
.helm-card p { margin: 0; color: var(--ink-muted); font-size: var(--fs-md); line-height: var(--lh-body); }
.helm-card-foot { margin-top: auto; padding-top: .4rem; }
.helm-banner { display: flex; gap: var(--sp-4); align-items: flex-start; padding: 1.2rem 1.4rem; margin-bottom: 1.2rem; border-left: 3px solid var(--primary); }
.helm-banner > .ti { font-size: var(--fs-2xl); color: var(--primary-2); flex: none; margin-top: .1rem; }
.helm-banner h3 { margin: 0 0 .3rem; font-size: var(--fs-base); }
.helm-banner p { margin: 0; color: var(--ink-muted); font-size: var(--fs-md); line-height: var(--lh-body); }
/* A sentence with an icon in front, so a block with the icon in a gutter, not a
   flex row: as flex items, every text run and every <strong>/<em> became its
   own column, and on a phone the note read as five narrow strips. */
.helm-note { position: relative; margin: 0 0 .8rem; padding-left: 1.45rem; color: var(--ink-muted); font-size: var(--fs-sm); line-height: var(--lh-snug); }
.helm-note .ti { position: absolute; left: 0; top: .15rem; color: var(--primary-2); }
.helm-note strong { color: var(--ink); }

/* Filter-chip counts. */
.chip-count { opacity: .72; margin-left: .2rem; font-family: var(--f-data); font-variant-numeric: tabular-nums; font-size: var(--fs-xs); }

/* ==========================================================================
   Phones
   --------------------------------------------------------------------------
   Measured on the rendered shell at 320-414px before this block existed: the
   header's right-hand cluster was a rigid 242px, so every page came out 474-
   509px wide and the phone either zoomed out or scrolled sideways; its buttons
   were 23px tall; and the popovers hung from triggers somewhere along the bar,
   so the dashboard picker opened at -150px and the status panel at -20px, cut
   off rather than scrollable.
   ========================================================================== */

/* The plan is carried in the caption above the organisation's name at every
   width (base_app.html `.header-tenant`). The header used to repeat it as a
   coloured badge on the right, and the dashboard as a third tag. */

/* iOS Safari zooms the whole page into any field whose text is under 16px and
   leaves it zoomed after the keyboard closes. The portal's fields inherit
   13-15px, and several components set theirs smaller still with more specific
   selectors, so on a touch screen or a narrow window this is a floor with
   `!important`: the point is that no field anywhere can go under it. */
@media (max-width: 820px), (pointer: coarse) {
  input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=file]):not([type=color]),
  select,
  textarea { font-size: max(var(--fs-lg), 1em) !important; }

  /* Touch targets: the small controls measured under 24px on a phone (theme
     switch 79x23, copy-IP 23x15, router id copy 29x20) get a 44px square. Named
     one by one: a blanket min-width on .btn-sm would widen pagers and action
     rows. .lq-copy and .rd-icon-btn are styled in lookout_queue.css and
     radar_detail.css, which set no size, so these rules still apply. */
  .theme-seg button { min-height: 44px; }
  .lq-copy,
  .rd-icon-btn { display: inline-grid; place-items: center; min-width: 44px; min-height: 44px; }
  /* Disclosure rows (19-22px tall) grow in height only; they stay list items so
     the open/closed marker keeps showing. */
  .lq-section > summary,
  .lq-ev-more > summary,
  details.raw-status > summary { padding-block: 12px; }
}

@media (max-width: 700px) {
  /* The picker's panel hangs from the band's right edge, not the picker's: the
     band wraps on a phone and puts the picker on the left, where a panel
     anchored to its own right edge opened off the screen. The band's edge is
     on the screen whether it wraps or not. */
  .band { position: relative; }
  .dbd-pick { position: static; }
}

@media (max-width: 560px) {
  /* The controls become touch squares (2.4rem is 35px on this 14.5px root). */
  .header-right { gap: var(--sp-1); }
  /* The organisation's name gets what the touch-square controls leave, rather
     than 22vw (86px on a 390px phone, "Nordic Bulk Ca…"). */
  .header-tenant strong { max-width: calc(100vw - 230px); }
  .search-trigger,
  .status-pill,
  .app-header .btn-sm,
  .app-header .icon-btn { width: 2.4rem; height: 2.4rem; padding: 0; justify-content: center; flex: none; }
  .status-pill { display: inline-grid; place-items: center; }

  /* The status and notification panels span the width under the header. With
     their wrappers static, the header (sticky, so positioned) is what they are
     placed against. The notification rules sit in base_app.html's own <style>,
     which loads after this file, hence the extra class on each selector. */
  .header-right .status-wrap,
  .header-right .notif-wrap { position: static; }
  .header-right .status-panel,
  .header-right .notif-panel { left: .6rem; right: .6rem; top: calc(100% + .35rem); width: auto; max-width: none; }

  /* Five figures in one column ran to 560px with nothing between them, because
     the dividers are drawn on each cell's right. Two abreast, with the gap
     itself drawn as the hairline, keeps them one instrument. */
  .cluster { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1px; background: var(--line-soft); }
  .cell { border-right: 0; background: var(--glass); padding: .95rem 1rem 1rem; }
  .cell:last-child:nth-child(odd) { grid-column: 1 / -1; }
  .cell-top .lbl { white-space: normal; }
  .cell-num { font-size: var(--fs-4xl); }

  /* Router gauges: a 150px floor just missed two columns inside a phone's card,
     so three rings stacked to 540px. Two abreast still fits the 116px ring. */
  .gauge-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-3); }
}

/* ---- Risk hub: link pickers, linked lists, risk log --------------------- */
/* The picker is a <details>: opening and closing is native, so it needs no
   script beyond htmx's search. Its panel sits in the flow of the card rather
   than floating, so a card with overflow clipping can never cut it off. */
.pick { margin: .4rem 0 .6rem; }
.pick > summary { list-style: none; cursor: pointer; display: inline-flex; }
.pick > summary::-webkit-details-marker { display: none; }
.pick-panel { margin-top: .5rem; padding: var(--sp-2); border: 1px solid var(--glass-border); border-radius: var(--r-md); background: var(--glass); }
.pick-inline .pick-panel { margin-top: 0; }
.pick-panel input[type=search] { width: 100%; }
.pick-results { max-height: 17rem; overflow-y: auto; margin-top: .45rem; }
.pick-option { margin: 0; }
.pick-btn { display: flex; width: 100%; align-items: center; justify-content: space-between; gap: var(--sp-2);
  text-align: left; background: none; border: 0; border-radius: var(--r-sm); padding: .45rem .5rem; color: var(--ink); font: inherit; cursor: pointer; }
.pick-btn:hover, .pick-btn:focus-visible { background: var(--hover-bg); outline: none; }
.pick-main { display: flex; flex-direction: column; min-width: 0; }
.pick-label { font-size: var(--fs-md); font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pick-sub { font-size: var(--fs-xs); color: var(--ink-faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pick-empty { font-size: var(--fs-sm); color: var(--ink-faint); margin: .4rem .5rem; }
.link-list { list-style: none; margin: .4rem 0 0; padding: 0; }
.link-list > li { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); padding: .55rem 0; border-bottom: 1px solid var(--glass-border); }
.link-list > li:last-child { border-bottom: 0; }
.link-list > li > div:first-child { min-width: 0; }
.link-meta { display: block; font-size: var(--fs-xs); color: var(--ink-faint); font-weight: 400; }
.unlink-btn { background: none; border: 0; padding: .15rem; color: var(--ink-faint); cursor: pointer; font: inherit; line-height: 1; }
.unlink-btn:hover { color: var(--danger); }
.risk-log { list-style: none; margin: .6rem 0 0; padding: 0; max-height: 32rem; overflow-y: auto; }
.log-item { display: flex; gap: var(--sp-3); padding: .6rem 0; border-bottom: 1px solid var(--glass-border); }
.log-item:last-child { border-bottom: 0; }
.log-icon { flex: none; width: 1.9rem; height: 1.9rem; border-radius: 50%; display: grid; place-items: center; border: 1px solid var(--glass-border); }
.log-body { flex: 1; min-width: 0; }
.log-lines { margin: .3rem 0 0; padding-left: 1rem; font-size: var(--fs-sm); }
.log-note { margin: .35rem 0 0; font-size: var(--fs-sm); white-space: pre-wrap; border-left: 3px solid var(--glass-border); padding-left: .55rem; }
.choice-list { border: 0; padding: 0; margin: 0; display: flex; flex-direction: column; gap: var(--sp-2); }
.choice-list .link-meta { margin-top: .1rem; }
.signoff-banner p { margin: 0 0 .5rem; }
.signoff-link { font-family: var(--f-data, monospace); font-size: var(--fs-sm); }
/* Category and similar: a fixed list plus "Custom…", which reveals the text box. */
.select-custom { display: flex; flex-direction: column; gap: var(--sp-2); }
.select-custom-input { display: none; }
.select-custom:has(option[value="__custom"]:checked) .select-custom-input { display: flex; }
/* Sign-off share page: the risk under review, and only the fields for the chosen decision. */
.signoff-risk { margin: 1rem 0 1.25rem; padding: 1rem; border: 1px solid var(--glass-border); border-radius: var(--r-md); }
.signoff-form:has(#soAccept:checked) .signoff-decline { display: none; }
.signoff-form:has(#soDecline:checked) .signoff-accept { display: none; }

/* ==========================================================================
   2026-09-18 — depth, rhythm and motion
   --------------------------------------------------------------------------
   Two things brought this pass on. The portal had one animation — a fade of the
   whole page on load — while the marketing site and the brand films it is sold
   with move constantly, so the product felt older than its own advertising. And
   the spacing was uniform: a page's title, its sections and its rows all sat
   the same distance apart, which is what makes a dense screen read as a wall.

   What is here, in order:

     1. the accent          every page is tinted by the feature it belongs to
     2. rhythm              titles, sections and rows on three different scales
     3. surfaces            panels with a light edge and a real hover
     4. motion              blocks arrive on scroll, titles assemble, the
                            aurora drifts, buttons take a sweep of light

   The motion half is gated behind `html.js-motion`, which `assets/js/motion.js`
   adds. Without the script nothing is ever hidden, so a blocked or slow script
   costs the animation and nothing else. `prefers-reduced-motion` turns the lot
   off at the bottom of this block — it is an accessibility setting, not taste.
   ========================================================================== */

/* ---- 1. the accent ------------------------------------------------------ */

/* base_app.html puts the active nav item's colour on `.app-content` as
   `--accent`, so anything inside a page can tint to the feature it is part of
   without a second lookup. */

.page-head { position: relative; }

/* A rule under the page title, cut short and fading out: the page has a
   masthead now instead of a heading that runs straight into its first card. */
.page-head::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -.85rem;
  height: 1px;
  background: linear-gradient(90deg,
              color-mix(in srgb, var(--accent, var(--primary)) 55%, transparent),
              var(--line-soft) 22%,
              transparent 70%);
}
.page-head.band::after { display: none; }

/* ---- 2. rhythm ---------------------------------------------------------- */

/* Three scales, not one: the masthead is set apart from the page, sections are
   set apart from each other, and rows inside a section are merely separated.
   The old file used ~1.25rem for all three. */
.app-content { max-width: 1320px; padding: 2.6rem 3rem 4.5rem; }
.page-head { margin-bottom: 2.2rem; }
.page-head h1 { font-size: var(--fs-3xl); }
.page-head .muted { margin-top: .15rem; font-size: var(--fs-base); }
/* The group a page belongs to ("Seawall"), a small label above its title
   (#27). `.lbl` gives the font, size and tracking. */
.page-kicker { display: block; margin: 0 0 .35rem; color: var(--ink-muted); text-transform: uppercase; }
/* NIS2 / IMO clause line (partials/framework_ref.html), inside a register page
   head under the lead line: one quiet line, no card. It stays above the
   `.page-head::after` rule, which keeps its gap to the first card. */
.fw-ref { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-1) var(--sp-3); margin-top: .55rem; font-size: var(--fs-sm); color: var(--ink-muted); }
.fw-ref-k { font-weight: 600; color: var(--ink); margin-right: .15rem; }
.fw-ref-imo > summary { cursor: pointer; user-select: none; }
.fw-ref-imo > summary:hover { color: var(--ink); }
.fw-ref-imo[open] { flex-basis: 100%; }
.fw-ref-imo p { margin: .3rem 0 0; max-width: 80ch; line-height: var(--lh-body); }
/* A compact search in a page head's action area (Lookout's IP check), so a
   lookup form does not push the page's status and queue below the fold. */
.head-search { display: flex; align-items: center; gap: var(--sp-1); margin: 0; }
.head-search .input { width: 15rem; max-width: 100%; }
@media (max-width: 560px) {
  .head-search { flex: 1 1 100%; }
  .head-search .input { flex: 1; width: auto; min-width: 0; }
}

.page-content-anim > .glass-card,
.page-content-anim > .cap-list,
.page-content-anim > .capability-grid,
.page-content-anim > .card-grid,
.page-content-anim > .table-wrap,
.page-content-anim > [hx-get] { margin-bottom: 1.6rem; }
.page-content-anim > .chip-row { margin-bottom: 1.35rem; }
.grid-2 { gap: var(--sp-5); margin-bottom: 1.6rem; }
/* Figures are ONE slab, not floating tiles (the dashboard `.cluster` lesson:
   separate cards read as unrelated alerts). Floating 150-250px tiles left a
   ragged right edge, an orphan on a second row and 300px of nothing beside
   them on wide pages. Equal cells in one bordered panel; each cell draws the
   hairline to its right and below, and the slab's `overflow: hidden` clips the
   ones on the outer edge, so a short last row ends in plain panel instead of
   the tinted hole a `gap: 1px` grid would leave. Declared here, after the gap
   rules above, or their 1.1rem gap wins. */
.stat-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 0;
  margin-bottom: 1.8rem;
  background: var(--glass);
  border: 1px solid var(--glass-border);
  border-radius: var(--r-md);
  box-shadow: inset 0 1px 0 var(--card-sheen), var(--shadow-1);
  overflow: hidden;
}
.stat-row > .stat {
  max-width: none; min-width: 0;
  background: transparent; border: 0; border-radius: 0;
  box-shadow: 1px 0 0 var(--line-soft), 0 1px 0 var(--line-soft);
}
.stat-row > a.stat:hover { transform: none; background: var(--hover-bg); }
/* The slab's `overflow: hidden` clips an outset focus ring: draw it inside. */
.stat-row > a.stat:focus-visible { outline-offset: -3px; background: var(--hover-bg); }
/* Inside a card the slab already sits on a panel: no second drop shadow. */
.glass-card .stat-row { box-shadow: none; }
/* Every tile behind a false condition: no empty bordered strip. */
.stat-row:not(:has(> *)) { display: none; }
.cap-list { gap: var(--sp-3); }

/* Cells are a table's rhythm; .7rem of padding on a 14.5px root was tight
   enough that a ten-column table read as a spreadsheet. The air goes in the
   VERTICAL only — the widest tables in the portal (portal users, the software
   inventory) run seven columns with buttons in the last, and horizontal padding
   is the one thing they have none of to give. */
.data-table th { padding: .78rem .92rem; }
.data-table td { padding: .82rem .92rem; }

@media (max-width: 900px) { .app-content { padding: 2rem 1.8rem 3rem; } }
@media (max-width: 700px) {
  .app-content { padding: 1.5rem 1.15rem 2.4rem; }
  .page-head { margin-bottom: 1.5rem; }
  .page-head h1 { font-size: var(--fs-2xl); }
}

/* ---- 3. surfaces -------------------------------------------------------- */

/* `position: relative` so a panel can hold its own pointer highlight. The
   existing positioned variant (.cap-card) already did this; making it the rule
   is what lets the highlight below be declared once. */
.glass-card { position: relative; }

/* The highlight that follows the pointer. Coordinates come from motion.js as
   --mx/--my, the look is entirely here, and the whole thing only exists on a
   device with a real pointer — on a touchscreen it would stick wherever the
   last tap landed. Painted as a background layer under the content rather than
   an ::after veil, so it can never intercept a click. */
/* `isolation` so the highlight has a stacking context of its own to be negative
   INSIDE. A negative z-index child paints above its element's background and
   below its in-flow content, which is exactly where a highlight belongs — and it
   gets there without `position: relative` on every child, which would have
   changed the containing block of anything absolutely positioned in a card. */
.glass-card[data-tracked] { isolation: isolate; }
.glass-card[data-tracked]::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  pointer-events: none;
  background: radial-gradient(340px circle at var(--mx, 50%) var(--my, 0%),
              color-mix(in srgb, var(--accent, var(--primary)) 13%, transparent),
              transparent 64%);
  opacity: 0;
  transition: opacity .28s ease;
}
.glass-card[data-tracked]:hover::before { opacity: 1; }
/* `.cap-card` already paints an oversized watermark glyph behind its own text
   and lights its whole border on hover; a second wash on top is noise. */
.cap-card[data-tracked]::before { content: none; }

/* A panel lifts a little and finds its accent when you are on it. Only the
   plain panels: rows, tiles and cards already have their own hover, and a
   second one on top of those reads as a twitch. */
/* `:where()` again, for the same reason as the padding rule above: ~15 variants
   declare a transition of their own earlier in this file, and a plain
   `.glass-card` rule down here would replace every one of them. */
:where(.glass-card) { transition: border-color .22s ease, box-shadow .22s ease; }
/* Only the plain panels take the accent on hover — the variants that already
   have a hover (rows, tiles, capability cards) keep theirs. */
.glass-card[data-tracked]:not(.cap-row):not(.cap-card):not(.stat):not(.hx-modal):not(.cmdk):hover {
  border-color: color-mix(in srgb, var(--accent, var(--primary)) 26%, var(--glass-border));
}

/* Rows: the accent arrives as a bar on the leading edge rather than the whole
   border changing colour, which at twenty rows was a christmas tree. */
/* Inset from the corners and rounded rather than clipped by `overflow: hidden`:
   a row can hold a "?" help popover, and clipping the row would cut it off. */
.cap-row { position: relative; }
.cap-row::before {
  content: "";
  position: absolute;
  left: 0;
  top: .55rem;
  bottom: .55rem;
  width: 2px;
  border-radius: 0 2px 2px 0;
  background: var(--accent, var(--primary));
  transform: scaleY(0);
  transform-origin: center;
  transition: transform .24s cubic-bezier(.22, 1, .36, 1);
}
.cap-row:hover::before, a.cap-row:hover::before { transform: scaleY(1); }

/* Selection carries the section colour too, so the page is coherent down to
   the smallest interaction. Focus does not: a light accent such as #2f97ff is
   2.75:1 on white, so the ring keeps `--focus-ring` in every section. */
::selection { background: color-mix(in srgb, var(--primary) 34%, transparent); color: var(--ink); }
:focus-visible { outline-color: var(--focus-ring); }
/* `--accent-ink` is computed where it is declared, so an element that sets
   its own `--accent` inline re-resolves it against that accent. */
[style*="--accent"] { --accent-ink: color-mix(in srgb, var(--accent, var(--primary)) var(--accent-ink-pct), var(--accent-ink-mix)); }

/* ---- 4. motion ---------------------------------------------------------- */

/* Blocks arrive as they come into view. This REPLACES the load-time stagger
   above (`animation: none` cancels it): animating everything on load meant the
   bottom of a long page had already finished before it was ever seen, and the
   page still cost a dozen simultaneous animations at the worst possible moment.
   `--reveal-step` is written per element by motion.js so a row of cards ripples
   across and the next row starts again from zero. */
html.js-motion .page-content-anim > *,
html.js-motion .cap-list > .cap-row,
html.js-motion .card-grid > .glass-card,
html.js-motion .helm-cards > .helm-card,
html.js-motion .stat-row > .stat,
html.js-motion .capability-grid > * {
  animation: none;
  opacity: 0;
  transform: translateY(18px);
  transition:
    opacity .58s cubic-bezier(.22, .68, .3, 1),
    transform .58s cubic-bezier(.22, .68, .3, 1);
  transition-delay: calc(var(--reveal-step, 0) * 55ms);
}
/* Paired selector for selector, NOT a bare `html.js-motion .is-in`: that is
   (0,2,1), and `.cap-list > .cap-row` / `.card-grid > .glass-card` /
   `.stat-row > .stat` / `.helm-cards > .helm-card` hide at (0,3,1), so the
   bare rule lost and every row, tile and card stayed at opacity 0 for good
   while the page's own sections (0,2,1, earlier) came through. */
html.js-motion .page-content-anim > .is-in,
html.js-motion .cap-list > .cap-row.is-in,
html.js-motion .card-grid > .glass-card.is-in,
html.js-motion .helm-cards > .helm-card.is-in,
html.js-motion .stat-row > .stat.is-in,
html.js-motion .capability-grid > .is-in { opacity: 1; transform: none; }

/* The masthead is never revealed on scroll — it is at the top of every page, so
   it would only ever animate at the same moment as the load. Its title carries
   the page's entrance instead, a word at a time. */
html.js-motion .page-content-anim > .page-head {
  opacity: 1;
  transform: none;
  transition: none;
}
.h1-words { display: inline; }
/* `backwards`, never `both`, and the ink fill inside the keyframes: the words
   inherit `text-fill-color: transparent` from `.h1-words` and are only seen
   through ITS `background-clip: text`. A word with a transform/opacity animation
   in effect is its own paint layer, and Chromium leaves a descendant layer's
   text out of the parent's text clip, so a `both` fill (which keeps the end
   state applied for good) left every split title permanently invisible. Now a
   word paints itself solid while it rises, and once the animation ends it has no
   layer and the gradient takes over. */
.page-head h1 .word {
  display: inline-block;
  animation: word-in .55s cubic-bezier(.22, 1, .36, 1) backwards;
  animation-delay: calc(var(--word-step, 0) * 55ms);
}
@keyframes word-in {
  from { opacity: 0; transform: translateY(.42em) rotate(1.2deg); -webkit-text-fill-color: var(--ink); }
  to { opacity: 1; transform: none; -webkit-text-fill-color: var(--ink); }
}
html.js-motion .page-head > .muted {
  animation: fadeInUp .5s cubic-bezier(.22, 1, .36, 1) .2s both;
}

/* The title takes the section's colour across its own last third. A gradient
   headline is the landing site's signature; at this weight it reads as the page
   being stamped rather than as decoration.

   Only `.h1-words` — the wrapper motion.js builds — and never the <h1> itself.
   `-webkit-text-fill-color: transparent` INHERITS, so painting the heading would
   also empty out any pill, count or link a template put inside it, and several
   do. The wrapper only exists when the title was plain text to begin with. */
.page-head h1 .h1-words {
  background: linear-gradient(100deg,
              var(--ink) 34%,
              color-mix(in srgb, var(--accent, var(--primary)) 62%, var(--ink)));
  background-size: 200% auto;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
  animation: title-sheen 2.4s ease-in-out 2 alternate both; /* one sweep and back, under 5s */
}
@keyframes title-sheen { to { background-position: 100% center; } }

/* The aurora drifts. Slowly enough that nobody watching consciously notices,
   enough that the page behind the panels is not a still image. The grid and the
   page ground stay put; only the brand light moves, and it is tinted by the
   section you are in. */
.aurora::before {
  content: "";
  position: absolute;
  left: 8%;
  top: -22%;
  width: 62vw;
  height: 62vh;
  border-radius: 50%;
  background: radial-gradient(closest-side,
              color-mix(in srgb, var(--accent, var(--primary)) 13%, transparent),
              transparent);
  filter: blur(18px);
  will-change: transform;
  animation: aurora-drift 34s ease-in-out 1 both; /* drifts once, then holds */
}
@keyframes aurora-drift {
  to { transform: translate3d(9vw, 7vh, 0) scale(1.14); }
}

/* A sweep of light crosses the primary action once, on hover. One button on a
   page, once per pointer visit — it draws the eye to the thing the page is for
   and then stops. */
.btn-primary { position: relative; overflow: hidden; }
.btn-primary::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: -60%;
  width: 45%;
  background: linear-gradient(100deg, transparent, rgba(255, 255, 255, .3), transparent);
  transform: skewX(-18deg);
  pointer-events: none;
}
.btn-primary:hover::after { animation: btn-sheen .72s ease; }
@keyframes btn-sheen { to { left: 130%; } }

/* The header casts a shadow only once content is passing under it. The border
   is left alone deliberately: taking it away by default would leave the header
   floating for anyone whose script never ran, and this is the enhancement, not
   the separation. */
.app-header { transition: background .35s ease, box-shadow .3s ease; }
.app-header.is-scrolled { box-shadow: var(--shadow-1); }

/* Sidebar links glide toward their page instead of just changing colour. The
   item you are already on does not move: it carries the accent bar, and sliding
   that away from the sidebar edge on hover looked like a mistake. */
.nav-link { transition: color .16s ease, background .16s ease, transform .16s cubic-bezier(.22, 1, .36, 1); }
.nav-link:hover:not(.active) { transform: translateX(2px); }

/* The accent bar grows to length rather than appearing, so moving between
   sections reads as one mark travelling down the sidebar. */
.nav-link.active::before {
  animation: nav-mark .34s cubic-bezier(.22, 1, .36, 1) both;
}
@keyframes nav-mark {
  from { transform: scaleY(0); }
  to { transform: scaleY(1); }
}

/* ---- 5. the details -------------------------------------------------------
   Small things, all of them things you only notice when they are missing.     */

/* A long table scrolled its own headings away, so by row forty every column was
   a mystery. The well the table sits in is the scroll container, so the head
   sticks to the top of THAT rather than to the viewport, and the background is
   opaque or the rows would show through it. */
.table-wrap { max-height: min(72vh, 900px); overflow: auto; }
/* On a phone that inner scroller trapped the swipe: a thumb landing on a table
   scrolled a 606px box instead of the page. Below 700px the well grows to the
   table's height and the page scrolls; the sticky head is given up there on
   purpose. The well still scrolls sideways. */
@media (max-width: 700px) { .table-wrap { max-height: none; } }
/* A table wider than its well fades out at the right edge while there is more
   to scroll to (portal.js sets .tw-more and drops it at the end), so a cut-off
   column reads as "swipe" rather than as the last column. It is a shade in the
   well's background, not a mask: about 37 wells are the card itself
   (`glass-card table-wrap`), and a mask clipped the card's drop shadow and
   faded its border. background-attachment: scroll pins the shade to the
   well's visible right edge; the well's own background colour stays under it. */
.table-wrap.tw-more {
  background-image: linear-gradient(to left, color-mix(in srgb, var(--ink-muted) 22%, transparent), transparent 1.5rem);
  background-repeat: no-repeat;
  background-attachment: scroll;
}
/* A sticky head pins to the scroll container's padding edge, not its border:
   any top padding left a strip above the head where scrolled rows showed
   through. The well has no top padding, so the head sits flush at the top. */
.table-wrap, .glass-card .table-wrap { padding-top: 0; }
.table-wrap > .data-table thead th:first-child { border-top-left-radius: 11px; }
.table-wrap > .data-table thead th:last-child { border-top-right-radius: 11px; }
.data-table thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--input-bg);
  box-shadow: inset 0 -1px 0 var(--glass-border);
  border-bottom: 0;
}
/* Fields answer to the section colour on focus, and lift a hair. The outline
   is no longer cleared here: the `input:focus-visible` ring near the form
   rules supplies the visible `--focus-ring`, which the 16% glow alone did
   not (1.18:1). */
input[type=text]:focus, input[type=email]:focus, input[type=password]:focus,
input[type=number]:focus, input[type=date]:focus, input[type=datetime-local]:focus,
input[type=time]:focus, input[type=tel]:focus, input[type=url]:focus,
input[type=search]:focus, textarea:focus, select:focus {
  border-color: color-mix(in srgb, var(--accent, var(--primary)) 62%, var(--glass-border));
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent, var(--primary)) 16%, transparent);
}

/* An empty state is a page in its own right, not a shrug. */
.empty { padding: 3rem 2rem; }
.empty p { margin: 0 auto; max-width: 42ch; }
/* Empty state with a next step (partials/empty.html): icon, title, one
   sentence, up to two buttons, stacked and centred. */
.empty-next { display: flex; flex-direction: column; align-items: center; gap: var(--sp-2); }
.empty-next-icon { font-size: var(--fs-4xl); line-height: 1; color: var(--ink-faint); }
.empty-next-title { margin: 0; font-family: var(--f-display); font-size: var(--fs-lg); font-weight: 600; color: var(--ink); }
.empty-next p { text-wrap: balance; } /* no one-word last line ("on.") under a centred title */
.empty-next-actions { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--sp-2); margin-top: .4rem; }

/* Status pills read as one family: the same weight, the same tracking, and a
   dot of their own colour rather than four different saturations of text. */
.pill { letter-spacing: .01em; padding: .16rem .55rem; }
.meta-chip { font-variant-numeric: tabular-nums; }

/* A stat tile is a figure with its name under it — and about forty of the ~96
   in the portal are written with <span> children, which flow INLINE. Those have
   been rendering "2Users", with no space at all, on the portal users page, the
   people register and everywhere else that markup was copied to. Making the tile
   itself a column fixes every one of them at once; the tiles written with <div>
   children already stacked and are unchanged.

   This is the `.check-row` lesson again (see the 2026-08-18 note): a class that
   IS defined can still leave a layout unsaid, and nothing in the build notices. */
.stat { display: flex; flex-direction: column; justify-content: center; gap: .12rem; }

/* ---- the one switch ----------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  html.js-motion .page-content-anim > *,
  html.js-motion .cap-list > .cap-row,
  html.js-motion .card-grid > .glass-card,
  html.js-motion .helm-cards > .helm-card,
  html.js-motion .stat-row > .stat,
  html.js-motion .capability-grid > * {
    opacity: 1;
    transform: none;
    transition: none;
  }
  .page-head h1 .h1-words, .page-head h1 .word, .aurora::before,
  .btn-primary:hover::after, html.js-motion .page-head > .muted { animation: none; }
  .cap-row::before, .glass-card[data-tracked]::before, .nav-link,
  .nav-group-title .ngt-caret { transition: none; }
}

/* ---- printing ----------------------------------------------------------- */

/* The gradient title is painted into the text itself, so on paper — where the
   print block forces ink on white — it would come out as nothing at all. */
@media print {
  .page-head h1 .h1-words {
    background: none !important;
    -webkit-text-fill-color: #000 !important;
    color: #000 !important;
    animation: none !important;
  }
  .page-head::after, .win-dock, .win-layer { display: none !important; }
  /* A table on paper has no scroll well: print every row, not the first screen. */
  .table-wrap { max-height: none !important; overflow: visible !important; background-image: none !important; }
  .data-table thead th { position: static !important; }
  html.js-motion .page-content-anim > *,
  html.js-motion .cap-list > .cap-row,
  html.js-motion .card-grid > .glass-card,
  html.js-motion .helm-cards > .helm-card,
  html.js-motion .stat-row > .stat,
  html.js-motion .capability-grid > * { opacity: 1 !important; transform: none !important; }
}

/* ---- Bridge > Regulatory clock (2026-09-18) ----
   The list is compact cards that open the entry's own page; the entry page
   carries the full text and the tickable checklist (`.check-row` boxes, which
   sit beside their text). Filter controls sit at one fixed height so a select
   and a search box never stretch to the bar's tallest child. */
.filter-bar .input, .filter-bar select.input, .filter-bar input.input { height: 2.7rem; padding: 0 .8rem; flex: none; }
.filter-bar > .btn { height: 2.7rem; display: inline-flex; align-items: center; }
.rc-back { display: inline-flex; align-items: center; gap: var(--sp-1); font-size: var(--fs-md); color: var(--ink-muted); text-decoration: none; margin-bottom: .4rem; }
.rc-back:hover { color: var(--ink); }
.rc-list { display: flex; flex-direction: column; gap: var(--sp-3); }
.rc-card { display: grid; grid-template-columns: 9rem 1fr auto; gap: var(--sp-4); padding: var(--sp-4) var(--sp-4); align-items: start; color: inherit; text-decoration: none; }
.rc-card:hover { text-decoration: none; }
.rc-card-date, .rc-date { display: flex; flex-direction: column; gap: var(--sp-1); }
.rc-date-main { font-weight: 700; font-size: var(--fs-base); line-height: var(--lh-tight); }
.rc-date-when { color: var(--ink-muted); font-size: var(--fs-sm); }
.rc-card-body, .rc-body { min-width: 0; }
.rc-card-title { font-weight: 600; font-size: var(--fs-base); line-height: var(--lh-snug); }
.rc-card-meta, .rc-meta { color: var(--ink-muted); font-size: var(--fs-sm); margin: .15rem 0 .45rem; }
.rc-card .badge-row { margin: 0 0 .5rem; }
.rc-card-summary { margin: 0; color: var(--ink-muted); font-size: var(--fs-md); line-height: var(--lh-body); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.rc-card-side { display: flex; flex-direction: column; align-items: flex-end; gap: var(--sp-2); white-space: nowrap; }
.rc-progress { font-size: var(--fs-sm); color: var(--ink-muted); display: inline-flex; align-items: center; gap: var(--sp-1); font-variant-numeric: tabular-nums; }
.rc-progress.is-complete { color: #0a8a5f; font-weight: 600; }
.rc-open-hint { font-size: var(--fs-sm); color: var(--link); display: inline-flex; align-items: center; gap: .15rem; }
.rc-entry { display: grid; grid-template-columns: 10rem 1fr; gap: var(--sp-4); padding: 1.3rem 1.5rem; align-items: start; }
.rc-entry .rc-date-main { font-size: var(--fs-xl); }
.rc-summary { margin: 0 0 .8rem; line-height: var(--lh-body); font-size: var(--fs-base); }
.rc-sources { display: flex; flex-wrap: wrap; gap: var(--sp-1) var(--sp-4); font-size: var(--fs-md); margin: 0 0 1rem; }
.rc-sources a { text-decoration: none; }
.rc-sources a:hover { text-decoration: underline; }
.rc-check { border-top: 1px solid var(--row-border); padding-top: .8rem; }
.rc-check-head { display: flex; justify-content: space-between; align-items: center; gap: var(--sp-4); margin-bottom: .55rem; }
.rc-check-title { font-weight: 600; font-size: var(--fs-md); display: inline-flex; align-items: center; gap: var(--sp-1); }
.rc-check-count { font-size: var(--fs-sm); color: var(--ink-muted); font-variant-numeric: tabular-nums; }
.rc-check-done { color: #0a8a5f; font-weight: 600; }
.rc-item { font-size: var(--fs-md); line-height: var(--lh-snug); }
.rc-item.is-done span { color: var(--ink-muted); text-decoration: line-through; }
.rc-note { font-size: var(--fs-sm); color: var(--ink-muted); margin-top: 1rem; }
@media (max-width: 760px) {
  .rc-card { grid-template-columns: 1fr; gap: var(--sp-2); }
  .rc-card-date { flex-direction: row; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
  .rc-card-side { flex-direction: row; justify-content: space-between; width: 100%; }
  .rc-entry { grid-template-columns: 1fr; gap: var(--sp-2); }
  .rc-date { flex-direction: row; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
}

/* ---- Shareable forms: create builder + personal sign-off link -------------
   The type tiles are radios. Which target panel shows follows the checked tile
   through :has(), so the form works with no script; share_builder.js adds
   `is-active` as well, for browsers without :has(), plus the extras (filter,
   generated password, live title and summary). */
.share-builder { padding: 1.2rem 1.5rem; margin-bottom: 1.25rem; }
.share-builder > summary { cursor: pointer; display: flex; flex-wrap: wrap; gap: var(--sp-1) var(--sp-3); align-items: baseline; }
.share-form { display: flex; flex-direction: column; gap: var(--sp-3); margin-top: 1rem; }
.share-form fieldset { display: flex; flex-direction: column; gap: var(--sp-3); }
.sf-step { display: flex; align-items: center; gap: var(--sp-2); margin-top: .6rem; }
.sf-step h3 { margin: 0; font-size: var(--fs-base); font-weight: 600; color: var(--ink); }
.sf-num { flex: none; width: 1.55rem; height: 1.55rem; border-radius: 50%; display: grid; place-items: center;
  font-family: var(--f-data); font-size: var(--fs-xs); color: var(--primary); border: 1px solid var(--primary); }
.sf-kinds { display: grid; grid-template-columns: repeat(auto-fill, minmax(11.5rem, 1fr)); gap: var(--sp-2); }
.sf-kind { display: block; position: relative; cursor: pointer; }
.sf-kind input, .sf-chip input, .sf-mode input { position: absolute; opacity: 0; pointer-events: none; }
.sf-kind-body { display: flex; flex-direction: column; gap: var(--sp-1); height: 100%; padding: var(--sp-3) var(--sp-3); border-radius: var(--r-md);
  border: 1px solid var(--glass-border); background: var(--glass); color: var(--ink-muted); font-weight: 400; font-size: var(--fs-sm); line-height: var(--lh-snug);
  transition: border-color .15s ease, background .15s ease; }
.sf-kind-body i { font-size: var(--fs-2xl); color: var(--primary-2); }
.sf-kind-body strong { color: var(--ink); font-size: var(--fs-md); }
.sf-kind:hover .sf-kind-body { background: var(--hover-bg); }
.sf-kind input:checked + .sf-kind-body { border-color: var(--primary); box-shadow: 0 0 0 1px var(--primary) inset; }
.sf-kind input:focus-visible + .sf-kind-body, .sf-mode input:focus-visible + span, .sf-chip input:focus-visible + span { outline: 2px solid var(--primary-2); outline-offset: 2px; }
.sf-panel { display: none; }
.sf-panel.is-active,
.share-form:has(input[name=kind][value="1"]:checked) .sf-panel[data-kind="1"],
.share-form:has(input[name=kind][value="2"]:checked) .sf-panel[data-kind="2"],
.share-form:has(input[name=kind][value="3"]:checked) .sf-panel[data-kind="3"],
.share-form:has(input[name=kind][value="5"]:checked) .sf-panel[data-kind="5"],
.share-form:has(input[name=kind][value="10"]:checked) .sf-panel[data-kind="10"] { display: block; }
.sf-panel label { gap: var(--sp-2); }
.sf-filter { margin-bottom: .1rem; }
.sf-empty { margin: 0; padding: .8rem 1rem; border: 1px dashed var(--glass-border); border-radius: var(--r-sm); color: var(--ink-muted); font-size: var(--fs-md); }
.sf-wording { display: flex; flex-direction: column; gap: var(--sp-3); }
.sf-wording .chip { align-self: flex-start; }
.sf-personal-note { display: none; margin: 0; }
/* A personal link is titled and identified by the system: hide what it ignores. */
.share-form:has(input[name=kind][value="10"]:checked) .sf-wording,
.share-form:has(input[name=kind][value="10"]:checked) .sf-identify { display: none; }
.share-form:has(input[name=kind][value="10"]:checked) .sf-personal-note { display: block; }
.share-form.is-personal .sf-wording, .share-form.is-personal .sf-identify { display: none; }
.share-form.is-personal .sf-personal-note { display: block; }
.sf-label { font-weight: 500; font-size: var(--fs-md); color: var(--ink-muted); display: block; margin-bottom: .35rem; }
.sf-expiry .chip-row { margin: 0; }
.sf-chip { display: inline-block; position: relative; }
.sf-chip span { display: inline-block; padding: .3rem .8rem; border-radius: var(--r-pill); font-size: var(--fs-sm); font-weight: 400;
  border: 1px solid var(--glass-border); background: var(--glass); color: var(--ink-muted); cursor: pointer; }
.sf-chip:hover span { background: var(--hover-bg); color: var(--ink); }
.sf-chip input:checked + span { background: var(--btn-primary-bg); border-color: var(--btn-primary-bg); color: var(--on-primary); }
.sf-identify { display: flex; flex-direction: column; gap: var(--sp-3); }
.sf-modes { display: grid; grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr)); gap: var(--sp-2); }
.sf-mode { display: block; position: relative; cursor: pointer; }
.sf-mode > span { display: flex; flex-direction: column; gap: .2rem; height: 100%; padding: .65rem .8rem; border-radius: var(--r-sm);
  border: 1px solid var(--glass-border); background: var(--glass); font-weight: 400; }
.sf-mode strong { color: var(--ink); font-size: var(--fs-md); }
.sf-mode:hover > span { background: var(--hover-bg); }
.sf-mode input:checked + span { border-color: var(--primary); box-shadow: 0 0 0 1px var(--primary) inset; }
.share-form:has(input[name=identify_mode][value="0"]:checked) .sf-anon { display: none; }
.sf-summary { margin: .2rem 0 0; min-height: 1.2em; }
.sf-summary:empty { display: none; }
.copy-field { display: flex; gap: var(--sp-1); align-items: center; }
.copy-field input { flex: 1; min-width: 0; }
.copy-label { gap: var(--sp-1); }
/* Person page / "link ready" card */
.signoff-card { padding: var(--sp-4) var(--sp-5); margin: 1.25rem 0; }
.signoff-head { display: flex; gap: var(--sp-3); align-items: flex-start; }
.signoff-head h2 { margin: 0 0 .2rem; font-size: var(--fs-base); }
.signoff-head p { margin: 0; }
.signoff-icon { flex: none; width: 2.2rem; height: 2.2rem; border-radius: 50%; display: grid; place-items: center;
  color: var(--primary-2); border: 1px solid var(--glass-border); font-size: var(--fs-lg); }
.signoff-issued, .signoff-status { display: flex; flex-direction: column; gap: var(--sp-3); margin-top: 1rem; }
.signoff-issued { padding: 1rem; border-radius: var(--r-sm); border: 1px solid var(--primary); }
.signoff-issued p, .signoff-status p { margin: 0; }
.signoff-create { display: flex; flex-wrap: wrap; gap: var(--sp-3) var(--sp-4); align-items: flex-end; margin: 1rem 0 .5rem; }
.signoff-create label { min-width: 9rem; }
@media (max-width: 760px) {
  .share-builder, .signoff-card { padding: var(--sp-4); }
  .sf-kinds { grid-template-columns: 1fr 1fr; }
}
/* Public personal sign-off page: one <details> per outstanding item. */
.pack-item { border: 1px solid var(--glass-border); border-radius: var(--r-sm); margin: .7rem 0; }
.pack-item > summary { cursor: pointer; padding: .75rem .9rem; display: flex; gap: var(--sp-2); align-items: center; flex-wrap: wrap; }
.pack-item[open] > summary { border-bottom: 1px solid var(--glass-border); }
.pack-item-body { padding: var(--sp-3); }
.pack-scroll { max-height: 46vh; overflow-y: auto; padding-right: .4rem; }
.pack-done { margin: 1rem 0; padding: 1rem; border: 1px solid var(--success); border-radius: var(--r-sm); }
.pack-done h2 { margin: 0 0 .3rem; font-size: var(--fs-base); }
.pack-done p { margin: 0; }
.pack-done-list { list-style: none; margin: 0; padding: 0; }
.pack-done-list li { padding: .45rem 0; border-bottom: 1px solid var(--line-soft); }

/* ---- Register entry pages (app/register/view.html) -------------------------
   Lede, "where this stands" findings, then a main column of facts beside an
   aside that explains the register and keeps the entry's history. */
.reg-subline { margin: .45rem 0 0; }
.reg-lede { font-size: var(--fs-base); line-height: var(--lh-body); color: var(--ink); max-width: 72ch; margin: 0 0 1.1rem; }
.reg-insights { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: var(--sp-3); margin: 0 0 1.25rem; }
.reg-insight { display: flex; gap: var(--sp-3); align-items: flex-start; padding: var(--sp-3) var(--sp-4); border-radius: var(--r-sm); border: 1px solid var(--glass-border); background: var(--chip-neutral-bg); }
.reg-insight > .ti { font-size: var(--fs-xl); line-height: var(--lh-tight); flex: none; color: var(--chip-neutral-fg); }
.reg-insight-body { min-width: 0; }
.reg-insight-body strong { display: block; font-family: var(--f-display); font-weight: 600; color: var(--ink); }
.reg-insight-body p { margin: .2rem 0 0; font-size: var(--fs-md); line-height: var(--lh-body); color: var(--ink-muted); }
.reg-insight-act { display: inline-block; margin-top: .45rem; font-size: var(--fs-sm); font-weight: 600; }
.reg-insight-ok { background: var(--chip-ok-bg); border-color: color-mix(in srgb, var(--chip-ok-fg) 35%, transparent); }
.reg-insight-ok > .ti { color: var(--chip-ok-fg); }
.reg-insight-warn { background: var(--chip-warn-bg); border-color: color-mix(in srgb, var(--chip-warn-fg) 35%, transparent); }
.reg-insight-warn > .ti { color: var(--chip-warn-fg); }
.reg-insight-bad { background: var(--chip-crit-bg); border-color: color-mix(in srgb, var(--chip-crit-fg) 40%, transparent); }
.reg-insight-bad > .ti { color: var(--chip-crit-fg); }
.reg-insight-info { background: var(--chip-brand-bg); border-color: color-mix(in srgb, var(--chip-brand-fg) 30%, transparent); }
.reg-insight-info > .ti { color: var(--chip-brand-fg); }
.reg-layout { display: grid; grid-template-columns: minmax(0, 1fr) minmax(260px, 340px); gap: var(--sp-4); align-items: start; }
.reg-main, .reg-aside { display: flex; flex-direction: column; gap: var(--sp-4); min-width: 0; }
.reg-main > .glass-card, .reg-aside > .glass-card { margin: 0; }
.reg-group-title { margin: 1.4rem 0 0; padding-top: 1rem; border-top: 1px solid var(--line-soft); }
.reg-group-title + .meta-grid { margin-top: .8rem; }
.reg-note-text { margin: .6rem 0 0; line-height: var(--lh-body); overflow-wrap: anywhere; }
.reg-about p { font-size: var(--fs-md); line-height: var(--lh-body); margin: .6rem 0 0; }
.reg-about > a { display: inline-block; margin-top: .8rem; }
.reg-about-why { color: var(--ink-muted); }
.reg-record { display: grid; gap: var(--sp-2); margin: .7rem 0 0; }
.reg-record dd { margin: .1rem 0 0; font-size: var(--fs-md); }
.reg-history { list-style: none; margin: .8rem 0 .6rem; padding: 0; }
.reg-history li { display: flex; flex-direction: column; gap: .1rem; padding: .45rem 0; border-bottom: 1px solid var(--line-soft); font-size: var(--fs-sm); }
.reg-history li:last-child { border-bottom: 0; }
.reg-history-at { font-family: var(--f-data); font-size: var(--fs-xs); color: var(--ink-faint); }
@media (max-width: 980px) {
  .reg-layout { grid-template-columns: minmax(0, 1fr); }
}

/* Effectiveness KPI entry (app/effectiveness/view.html): reading vs target. */
.kpi-hero { margin: 0 0 1.25rem; padding: 1.25rem 1.4rem; }
.kpi-hero-figures { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: var(--sp-4); }
.kpi-hero-figures > div { display: flex; flex-direction: column; gap: .15rem; }
.kpi-hero-num { font-family: var(--f-data); font-size: var(--fs-3xl); font-weight: 600; color: var(--ink); line-height: var(--lh-tight); }
.kpi-hero-num.kpi-good { color: var(--chip-ok-fg); }
.kpi-hero-num.kpi-bad { color: var(--chip-crit-fg); }
.kpi-hero-sub { font-size: var(--fs-sm); color: var(--ink-muted); }
.kpi-change-ok { color: var(--chip-ok-fg); }
.kpi-change-crit { color: var(--chip-crit-fg); }
.kpi-change-neutral { color: var(--ink-muted); }
.kpi-trend { margin: 1.1rem 0 0; }
.kpi-trend svg { display: block; width: 100%; height: 150px; overflow: visible; }
.kpi-trend-target { stroke: var(--chip-ok-fg); stroke-width: 1.5; stroke-dasharray: 6 5; vector-effect: non-scaling-stroke; opacity: .8; }
.kpi-trend-line { fill: none; stroke: var(--primary); stroke-width: 2; vector-effect: non-scaling-stroke; stroke-linejoin: round; }
.kpi-trend-dot { stroke: var(--primary); stroke-width: 9; stroke-linecap: round; vector-effect: non-scaling-stroke; }
.kpi-trend-axis { display: flex; justify-content: space-between; gap: var(--sp-3); flex-wrap: wrap; margin-top: .45rem; font-family: var(--f-data); font-size: var(--fs-xs); color: var(--ink-faint); }
.kpi-trend-key { display: inline-flex; align-items: center; gap: var(--sp-1); }
.kpi-key-target { display: inline-block; width: 18px; border-top: 2px dashed var(--chip-ok-fg); }

/* ---- Table action cells ------------------------------------------------------
   `.actions` is a flex row, and twelve tables put it on the <td> itself. A td
   that is `display: flex` stops being a table cell: it falls out of the row's
   height, so its buttons sat low and its bottom border no longer met the
   others'. Keep the cell a cell and lay the buttons out inline inside it; the
   small negative margin centres a button on the first line of its row's text.
   `.ta-right` also lost to `.data-table th { text-align: left }`, so an
   "Actions" heading sat left while its buttons sat right. */
.data-table td.actions { display: table-cell; white-space: nowrap; text-align: right; vertical-align: top; }
.data-table td.actions > * { display: inline-flex; vertical-align: top; margin: -.3rem 0 -.3rem .35rem; }
.data-table td.actions > *:first-child { margin-left: 0; }
.data-table th.ta-right, .data-table td.ta-right { text-align: right; }

/* ---- Row overflow menu --------------------------------------------------------
   Rare and destructive row actions (deactivate, make owner, remove) behind one
   "..." button, so the actions column stops squeezing the data beside it. A
   native <details>: it opens without script; portal.js closes it on an outside
   click or Escape. The well (.table-wrap) scrolls, so it clipped a panel that
   ran past it: on a one-user IAM list Deactivate and Revoke owner sat behind
   an inner scrollbar. portal.js lets a well that is not scrolling overflow
   while its menu is open (.tw-menu-open, raised above the cards after it), and
   in a well that does scroll it opens the panel upwards (.row-menu-up) when it
   does not fit below. Without script the last row of a longer table still
   opens upwards. */
.row-menu { position: relative; }
.row-menu > summary { list-style: none; cursor: pointer; }
.row-menu > summary::-webkit-details-marker { display: none; }
.row-menu-panel {
  position: absolute; right: 0; top: calc(100% + .25rem); z-index: 30;
  display: flex; flex-direction: column; min-width: 11rem; padding: .3rem;
  background: var(--pop-bg); border: 1px solid var(--glass-border); border-radius: var(--r-md);
  box-shadow: var(--shadow-2); text-align: left;
}
.data-table tbody tr:last-child:not(:first-child) .row-menu-panel,
.row-menu.row-menu-up .row-menu-panel { top: auto; bottom: calc(100% + .25rem); }
.table-wrap.tw-menu-open { overflow: visible; position: relative; z-index: 31; }
.row-menu-panel form { margin: 0; }
.row-menu-item {
  display: flex; align-items: center; gap: var(--sp-2); width: 100%; padding: .42rem .6rem;
  background: none; border: 0; border-radius: var(--r-sm); color: var(--ink); font: inherit; font-size: var(--fs-sm);
  text-align: left; cursor: pointer;
}
.row-menu-item:hover, .row-menu-item:focus-visible { background: var(--hover-bg); }
.row-menu-item.row-menu-danger { color: var(--danger); }
/* IAM users: the role select keeps its whole name ("Compliance officer", not "Compl"). */
.iam-role-select { min-width: 11rem; }
/* A select in a cell (a user's role, a row's state) kept shrinking with its
   column until only the chevron was left. `:where()` keeps this floor at one
   type selector of specificity, so a select's own rule (.iam-role-select) wins. */
:where(.data-table td) select { min-width: 8rem; max-width: 100%; }

/* ---- Stacked tables on phones (.data-table.stack-sm) -------------------------
   A seven-column list in a 355px column either scrolls sideways, with Actions
   off screen, or crushes every cell to one word. Below 700px a `.stack-sm`
   table drops its head and lays each row out as a card: one cell per line,
   with the column name (the td's `data-label`) to its left. A cell without a
   label (the row's title, its actions) spans the card. */
@media (max-width: 700px) {
  .data-table.stack-sm thead { display: none; }
  .data-table.stack-sm, .data-table.stack-sm tbody, .data-table.stack-sm tr, .data-table.stack-sm td { display: block; }
  .data-table.stack-sm tbody tr { padding: .5rem 0; border-bottom: 1px solid var(--row-border); }
  .data-table.stack-sm tbody tr:last-child { border-bottom: none; }
  .data-table.stack-sm td { border-bottom: none; padding: .3rem .7rem; }
  .data-table.stack-sm td:empty { display: none; }
  .data-table.stack-sm td[data-label] { position: relative; padding-left: calc(36% + .7rem); min-height: 1.9rem; }
  .data-table.stack-sm td[data-label]::before {
    content: attr(data-label);
    position: absolute; left: .7rem; top: .4rem; width: calc(36% - .7rem);
    font-family: var(--f-display); font-size: var(--fs-xs); font-weight: 500; letter-spacing: .045em;
    color: var(--ink-muted); overflow-wrap: break-word;
  }
  .data-table.stack-sm td.ta-right, .data-table.stack-sm td.num { text-align: left; }
  .data-table.stack-sm td.actions { display: block; white-space: normal; text-align: left; padding-top: .45rem; }
  .data-table.stack-sm td.actions > * { margin: 0 .35rem .35rem 0; }
  /* The "..." button now sits at the card's left edge, so its menu opens
     rightwards into the card instead of past the well's clipped left side. */
  .data-table.stack-sm .row-menu-panel { right: auto; left: 0; }
}

/* ---- Table tools (assets/js/table_tools.js) ----------------------------------
   Search, option filters and paging added to every list table the server sends
   whole. The bar reuses the filter-bar look of the server-filtered lists. */
.tt-bar { margin: 0 0 .8rem; }
.tt-bar .filter-field { flex: 0 1 11rem; min-width: 8rem; }
.tt-bar .filter-grow { flex: 1 1 16rem; max-width: 28rem; }
.tt-bar .filter-field .input { width: 100%; min-width: 0; box-sizing: border-box; flex: none; }
.tt-bar .tt-clear { align-self: flex-end; }
.tt-foot { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); flex-wrap: wrap; margin: .8rem 0 1.25rem; }
.tt-foot .pager { margin-top: 0; }
.tt-foot .pager .btn[disabled] { opacity: .45; cursor: default; }
/* The count reads the same in the server pager (partials/pager.html). */
.tt-foot .tt-count { font-variant-numeric: tabular-nums; }
/* The global `label` rule stacks its children; this one reads "Rows [25]". */
.tt-foot .tt-size { display: inline-flex; flex-direction: row; align-items: center; gap: var(--sp-2); }
.tt-foot .tt-size select.input { height: 2.1rem; padding: 0 .5rem; width: auto; min-width: 4.5rem; }
.tt-empty { padding: 1rem; text-align: center; }
.data-table tr.tt-hide { display: none !important; }

/* ---- Pages that left the standalone deploy shell -----------------------------
   `.fp-section` marks a section card on those pages (deploy, remote access);
   it can sit inside a grid or a wrapper, so it must not overflow its track.
   `.fp-action-group` > `.fp-action` is the Safe / Disruptive action list
   (Deploy actions, a router's own Actions block): a button beside one plain
   line saying what it does. */
.fp-section { min-width: 0; }
.fp-action-group { display: flex; flex-direction: column; gap: var(--sp-2); }
.fp-action-group > h3 {
  margin: 0 0 var(--sp-1); font-family: var(--f-display); font-size: var(--fs-xs);
  font-weight: 600; letter-spacing: .045em; color: var(--ink-faint);
}
.fp-action { display: grid; grid-template-columns: 13rem minmax(0, 1fr); align-items: center; gap: var(--sp-3); }
.fp-action > .btn { justify-self: start; }
.fp-action-desc { margin: 0; font-size: var(--fs-sm); color: var(--ink-muted); }
@media (max-width: 560px) {
  .fp-action { grid-template-columns: 1fr; gap: var(--sp-1); }
}
