/*
 * Copyright (c) 2026 HelloHive (hellohive.ai). All Rights Reserved.
 *
 * This file is part of HelloHive and contains confidential and proprietary
 * trade secrets of HelloHive. Unauthorized copying,
 * distribution, modification, or use is strictly prohibited.
 */

/* ═══════════════════════════════════════════════════════════════════════════
   xClarus COMPONENTS AND PAGES. Authority: docs/DESIGN.md.

   THIS FILE CONTAINS NO LITERAL COLOUR, NO FONT FACE AND NO RAW px FONT SIZE.
   Every value it paints with is a name declared in tokens.css, which base.html
   loads first. tests/central/test_design_tokens.py fails the build on a hex,
   an rgba() or a px font-size anywhere in this file.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ═══════════════════════════════════════════════════════════════════════════
   SYSTEM PRIMITIVES — density, focus, motion, and the shared components.
   These are the pieces every page composes from. A page that needs a variant
   adds a MODIFIER here; it does not open a <style> block.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── DENSITY MODES ────────────────────────────────────────────────────────
   Declared once per page on <main>. Components below read the variables, so
   the same markup is correctly tight on an inventory and correctly roomy on
   a detail page. A page that declares neither gets DATA, because the app is
   an operations tool first. */
.density-data {
  --row-y: var(--sp-2);
  --row-x: var(--sp-3);
  --panel-pad: var(--sp-4);
  --stack-gap: var(--sp-3);
}
.density-narrative {
  --row-y: var(--sp-3);
  --row-x: var(--sp-4);
  --panel-pad: var(--sp-5);
  --stack-gap: var(--sp-5);
}

/* ── EYEBROW ──────────────────────────────────────────────────────────────
   THE uppercase label treatment — section labels, stat captions, field
   labels, card kickers. One definition so tracking never drifts. */
.eyebrow {
  font-family: var(--font-mono);
  font-size: var(--fs-1);
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--text-tertiary);
  font-weight: var(--fw-normal);
}

/* ── VISUALLY HIDDEN ──────────────────────────────────────────────────────
   Text present for a screen reader and absent from the layout. The ONLY
   sanctioned way to render something invisibly: `display:none` and
   `visibility:hidden` remove it from the accessibility tree too, which is the
   opposite of what a caller reaching for this wants.

   It exists because §11 ("status is never colour alone") has a shape the chip
   rule does not cover: a dense strip of one-pixel-wide marks — a capture
   timeline, a sparkline — cannot carry a visible word per mark. The mark gets
   its word here, plus a visible legend, so the meaning survives greyscale,
   a screenshot, and a screen reader. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ── THE FLOATING SURFACE ──────────────────────────────────────────────────
   ONE tooltip element for the whole application, moved and refilled rather
   than one per trigger, because there is only ever one open (see
   ui/static/js/floating-surface.js — the slot is the whole idea).

   It replaces the BROWSER'S native `<title>` tooltip on the surfaces that
   register for it, and it exists because a native tooltip cannot be dismissed
   by anything on the page: on the topology map it rendered on top of the
   endpoint badge's own popover, two surfaces saying overlapping things in the
   same place. The `<title>` stays in the markup and is borrowed for the length
   of one hover, so a reader with scripting off still gets the native one and a
   screen reader still reads the description from the element itself.

   `pre-line` because the content it carries is authored as LINES — the map's
   hovers are three of them, fixed shape, and that shape is the point. */
.floating-tip {
  position: fixed;
  z-index: 60;
  display: none;
  max-width: 44ch;
  padding: var(--sp-2) var(--sp-3);
  background: var(--surface-overlay);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  box-shadow: var(--shadow-md);
  color: var(--text-secondary);
  font-family: var(--font-mono);
  font-size: var(--fs-2);
  line-height: 1.5;
  white-space: pre-line;
  /* Never a hit target. A tooltip that can be hovered is a tooltip that can
     keep itself open over the thing it is describing. */
  pointer-events: none;
}
.floating-tip[data-open] { display: block; }

/* ── FOCUS ────────────────────────────────────────────────────────────────
   Visible, always. :focus-visible so a mouse click does not ring a button,
   but every keyboard path does. Accessibility floor, not a preference. */
:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

/* ── MOTION ───────────────────────────────────────────────────────────────
   Respect the OS setting. The topology map's flow IS a live signal, not
   decoration — but a reader who has asked for stillness gets stillness, and
   every signal it animates is ALSO carried by colour, stroke width, pattern
   or a glyph, so freezing it loses no meaning (DESIGN_SYSTEM §16). */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}

/* ── COMPACT ABSENCE (the empty-state rule) ───────────────────────────────
   NOTHING THAT REPORTS ABSENCE MAY OCCUPY A FULL PANEL. Screen real estate
   is proportional to information content, and "there is nothing here" is one
   line of information. This is the single biggest visual win in the app: at
   lab scale the old full-panel empty states filled half the landing page
   with the news that nothing had happened.

   .absence      — one muted line, in flow. THE default for "no rows".
   .absence-cta  — same line plus one action, for a genuinely empty resource
                   the reader is expected to populate ("no sites yet").

   A full-bleed illustrated empty state is not available on purpose. If a
   surface truly has nothing and never will, it should not be on screen. */
.absence {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--row-x);
  color: var(--text-tertiary);
  font-size: var(--fs-3);
  line-height: var(--lh-normal);
}
/* SHELL-2: no leading dash. An absence is a sentence ("No open incidents"),
   and a "—" before it read as a placeholder value on every empty panel. */
.absence-cta {
  margin-left: auto;
  font-size: var(--fs-2);
}

/* ── DELIVERY ALARM (DELIVERY-ALARM, PLAN 2.21) ─────────────────────────────
   A broken notification channel is the one absence this product must NOT
   render quietly: it means alerts are not reaching anybody. So it is the
   critical edge, in flow at the top of every page an org's alerting is
   visible on, never a dismissible toast. Rendered only from
   ``_delivery_alarm.html``; the measurement is ``services/channel_health``'s. */
.delivery-alarm {
  border: 1px solid var(--state-critical-edge);
  background: var(--state-critical-bg);
  border-radius: var(--r-sm);
  padding: var(--sp-3) var(--sp-4);
  margin: 0 0 var(--sp-4);
  font-size: var(--fs-4);
  line-height: var(--lh-normal);
}
.delivery-alarm-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--sp-2);
}
.delivery-alarm-head .btn-link { margin-left: auto; }
.delivery-alarm-list {
  margin: var(--sp-2) 0 0;
  padding-left: var(--sp-5);
  font-size: var(--fs-3);
}

/* ── EVIDENCE DISCLOSURE ──────────────────────────────────────────────────
   THE SIGNATURE PATTERN OF THIS PRODUCT, unified.

   The incident page ("why do we think these alerts are one event?") and the
   vendor catalog ("how far has this pack actually been proven?") each
   invented a collapsible "here is what we know and how we know it" block.
   They are the same idea: xClarus states a conclusion, and the reader can
   always open the reasoning. That willingness IS the differentiator — no
   competitor shows its work — so it gets one component and one look.

   Closed by default: the conclusion is the product, the evidence is the
   proof on demand. Summary states what is inside and how much of it. */
.evidence {
  border: 1px solid var(--border-subtle);
  border-radius: var(--r-sm);
  background: var(--surface-card);
  margin: var(--sp-2) 0;
}
.evidence > summary {
  cursor: pointer;
  padding: var(--sp-2) var(--sp-3);
  font-family: var(--font-mono);
  font-size: var(--fs-2);
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--text-tertiary);
  list-style: none;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  transition: color var(--motion-fast) var(--ease-out);
}
.evidence > summary::-webkit-details-marker { display: none; }
.evidence > summary::before {
  content: "▸";
  color: var(--text-disabled);
  transition: transform var(--motion-fast) var(--ease-out);
}
.evidence[open] > summary::before { transform: rotate(90deg); }
.evidence > summary:hover { color: var(--text-secondary); }
.evidence-body {
  padding: 0 var(--sp-3) var(--sp-3);
  font-size: var(--fs-3);
  line-height: var(--lh-relaxed);
  color: var(--text-secondary);
  max-width: var(--prose-measure);
}
/* A disclosure whose argument is a SET of cases rather than a paragraph. Two
   sides of a trade read as two items; running them together as prose is how a
   reader comes away with only the half that flattered us. */
.evidence-body ul {
  margin: var(--sp-2) 0 0;
  padding-left: var(--sp-4);
}
.evidence-body li + li { margin-top: var(--sp-2); }

/* The count that tells a reader whether opening is worth it. */
.evidence-count {
  margin-left: auto;
  color: var(--text-disabled);
  letter-spacing: 0;
  text-transform: none;
}

* { box-sizing: border-box; }

/* HTML hidden attribute must win over component display (flex/grid). */
[hidden] { display: none !important; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--ink-0);
  color: var(--fg-0);
  font-family: var(--font-sans);
  font-size: var(--fs-4);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

body {
  /* Grain only on navy app pages — auth shell clears this below. */
  min-height: 100vh;
}

/* ── THE MONO/SANS SPLIT (DESIGN_SYSTEM.md §6) ───────────────────────────
   MONO IS A SIGNAL THAT A VALUE CAME FROM A DEVICE. Spending it on a
   sentence spends the signal for nothing, and spending it everywhere is why
   the app read as a terminal rather than as a product.

   These selectors are machine values by definition: chips (states and
   counts), code blocks, tokens, readouts, the brand mark.

   `.info-list dt` LEFT this list. It is a definition LABEL — "Recognises",
   "Collects", "Config backup" — words a person wrote, and the system already
   has a treatment for a label: .eyebrow. */
.chip,
.bulk-count,
.code-block,
.token-value,
code,
.readout-row,
.brand-mark {
  font-family: var(--font-mono);
}

/* Definition-list terms are labels, not values. */
.info-list dt {
  font-family: var(--font-sans);
  font-size: var(--fs-2);
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--text-tertiary);
}

/* ── TABLE CELLS — the one call that needs eyes on it ────────────────────
   A data table is mostly machine values (hostnames, IPs, counts, timestamps)
   with some prose mixed in (site names, notes, descriptions). Blanket mono is
   wrong for the prose; blanket sans is wrong for the columns that are meant
   to align digit-under-digit.

   The correct end state is per-cell opt-in, and the app is already most of
   the way there: `mono` appears 252 times in the templates and
   module_page.Cell has a `mono` flag that the shared table macro honours.

   THIS IS A ONE-VARIABLE SWITCH, on purpose. Neither the author of this
   system nor the engineer who applied it could see the rendered result, so
   the change that most affects "does this read as a terminal or as a product"
   was made cheap to reverse instead of guessed at:

       --table-cell-font: var(--font-mono);  → the original look
       --table-cell-font: var(--font-sans);  → prose-first tables; cells that
                                               ARE data keep mono via .mono

   IT WAS FLIPPED TO SANS and kept. Mono still carries every machine value —
   IPs, OIDs, metric names, hostnames, MACs, serials, config text, timestamps
   read as data — because those cells declare `.mono` and the rule below gives
   them precedence over the switch. Numeric alignment is preserved either way
   by the tabular-nums below. Reverting is one line, in the token layer. */
.data-table th,
.data-table td {
  font-family: var(--table-cell-font);
  font-variant-numeric: tabular-nums;
}
/* An explicit .mono cell always wins, whichever way the switch is set. */
.data-table td.mono,
.data-table td .mono {
  font-family: var(--font-mono);
}

button, input, select, textarea {
  font: inherit;
  color: inherit;
}

a { color: inherit; text-decoration: none; }

/* ═════════════════════════ AUTH SHELL (login) ═════════════════════════ */

.auth-shell {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr);
  min-height: 100vh;
  background: var(--xp-cream);
  background-image: none;
}

.auth-side {
  position: relative;
  background: var(--deep);
  padding: 48px 56px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  overflow: hidden;
}

.grid-overlay {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.brand-block { position: relative; }

.brand-mark {
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: var(--fs-6);
  letter-spacing: 0.04em;
  color: var(--fg-0);
  display: flex;
  align-items: center;
  gap: 8px;
}

.bracket {
  color: var(--brand);
  font-weight: 400;
}

.brand-name { color: var(--fg-0); }
.brand-mid { color: var(--brand); }
.brand-x-mark {
  height: 1em;
  width: auto;
  color: var(--brand);
  vertical-align: -0.12em;
  margin-right: 0.04em;
}
.brand-tld { color: var(--hellohive-tld); } /* HelloHive.ai accent — matches site --indigo-light */

.customer-brand-logo {
  display: block;
  max-height: 28px;
  width: auto;
  max-width: 100%;
  object-fit: contain;
}

.auth-side .customer-brand-logo {
  max-height: 32px;
}

.site-tagline {
  text-align: center;
  font-size: var(--fs-3);
  letter-spacing: 0.04em;
  color: var(--fg-2);
  padding: 20px 0 16px;
  user-select: text;
}

.hellohive-site-link {
  color: inherit;
  text-decoration: none;
}

/* The derived build identifier. Diagnostic, so it is the quietest text on the
   page — present for whoever needs it, invisible to whoever does not.

   NOT `.site-build`, which is what this was called for about an hour: the
   tenant-isolation suite proves org A's pages never contain org B's markers by
   grepping the WHOLE page for literals like "site-b", and "site-build"
   contains one. The coarseness is the point of that check — it is what would
   catch a real leak in markup nobody thought to assert on — so the class moved
   rather than the guard. */
.build-stamp {
  margin-left: var(--sp-3);
  font-size: var(--fs-2);
  color: var(--text-disabled);
  letter-spacing: 0;
}

.hellohive-site-link:hover {
  color: var(--fg-0);
  text-decoration: underline;
  text-underline-offset: 2px;
}

.brand-sub {
  margin-top: 14px;
  font-size: var(--fs-2);
  letter-spacing: 0.22em;
  color: var(--fg-2);
}

.brand-parent {
  margin-top: 4px;
  font-size: var(--fs-2);
  letter-spacing: 0.08em;
  color: var(--fg-3);
}

.readout {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 20px 22px;
  border: 1px solid var(--line);
  background: var(--scrim-page);
  backdrop-filter: blur(8px);
  width: fit-content;
  min-width: 320px;
}

.readout-row {
  display: grid;
  grid-template-columns: 36px 1fr auto;
  align-items: center;
  font-size: var(--fs-3);
  gap: 12px;
}

.readout-key {
  color: var(--fg-3);
  font-weight: 600;
  letter-spacing: 0.1em;
}

.readout-val { color: var(--fg-1); }

.readout-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--fg-3);
  box-shadow: 0 0 0 3px var(--scrim-strong);
}

.readout-dot.ok {
  background: var(--ok);
  box-shadow: 0 0 0 3px var(--status-success-glow), 0 0 12px var(--status-success-halo);
  animation: pulse 2.4s ease-in-out infinite;
}

@keyframes pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.5; }
}

.quote { position: relative; max-width: 520px; }

.quote-text {
  font-family: var(--font-sans);
  font-style: italic;
  font-size: var(--fs-8);
  line-height: 1.32;
  color: var(--fg-0);
  margin: 0;
  font-weight: 500;
}

.quote-mark {
  color: var(--brand);
  font-style: normal;
  font-weight: 400;
}

.auth-side-bottom {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 8px;
}

.auth-side-site {
  display: block;
  text-align: center;
  font-size: var(--fs-1);
  letter-spacing: 0.14em;
  color: var(--fg-3);
  user-select: text;
}

.auth-side-footer {
  position: relative;
  display: flex;
  justify-content: space-between;
  font-size: var(--fs-1);
  letter-spacing: 0.2em;
  color: var(--fg-3);
}

/* ── Right panel ── */

.auth-main {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 48px;
  background: var(--xp-cream);
}

.auth-card {
  width: 100%;
  max-width: 420px;
  display: flex;
  flex-direction: column;
  gap: 28px;
  background: var(--xp-cream);
  color: var(--xp-text);
  border: var(--xp-border-soft);
  border-radius: var(--xp-radius-lg);
  box-shadow: var(--xp-shadow-card);
  padding: 40px 36px;
}

.auth-eyebrow {
  display: inline-block;
  font-size: var(--fs-2);
  letter-spacing: 0.28em;
  color: var(--brand);
  padding: 4px 8px;
  border: 1px solid var(--brand);
  background: var(--brand-glow);
}

/* The auth pages' own page title ("Sign in", "Reset your password") — the
   ONE light surface, and the first type a stranger sees. Same treatment as
   .page-title. */
.auth-title {
  font-family: var(--font-sans);
  font-weight: 700;
  font-size: var(--fs-9);
  line-height: 1.05;
  margin: 14px 0 8px;
  color: var(--xp-text);
  letter-spacing: -0.02em;
}

.auth-lede {
  margin: 0;
  color: var(--xp-text-muted);
  font-size: var(--fs-4);
}

.auth-form {
  display: flex;
  flex-direction: column;
  gap: 18px;
}

.field { display: flex; flex-direction: column; gap: 6px; }

.field-label {
  font-size: var(--fs-1);
  letter-spacing: 0.22em;
  color: var(--fg-2);
  font-weight: 600;
  font-family: var(--font-sans);
}

.field input {
  background: var(--ink-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 12px 14px;
  color: var(--fg-0);
  font-size: var(--fs-4);
  font-family: var(--font-sans);
  transition: border-color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
}

.field input::placeholder { color: var(--fg-3); }

.field input:hover { border-color: var(--line-2); }

.field input:focus:not(:focus-visible) {
  outline: none;
  border-color: var(--brand);
  background: var(--ink-1);
  box-shadow: 0 0 0 3px var(--brand-glow);
}

.btn-primary {
  margin-top: 8px;
  background: var(--brand);
  color: var(--xp-text-on-violet);
  border: none;
  padding: 12px 18px;
  font-family: var(--font-sans);
  font-weight: 600;
  font-size: var(--fs-4);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  transition: background 0.15s ease, transform 0.05s ease, box-shadow 0.15s ease;
  border-radius: var(--radius);
}

.btn-primary:hover {
  background: var(--brand-hover);
  box-shadow: 0 4px 14px var(--brand-glow);
}

.btn-primary:active { transform: translateY(1px); }

.btn-arrow { font-weight: 400; }

.alert {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 14px;
  font-size: var(--fs-4);
  border-radius: var(--radius);
  border: 1px solid var(--line);
}

.alert-error {
  background: var(--status-danger-bg);
  border-color: var(--status-danger-border);
  color: var(--error);
}

.alert-icon {
  font-weight: 700;
  width: 18px;
  height: 18px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--error);
  color: var(--xp-white);
  border-radius: var(--radius);
  font-size: var(--fs-2);
}

.auth-card-footer {
  border-top: 1px dashed var(--accent-loud);
  padding-top: 16px;
  font-size: var(--fs-3);
  color: var(--xp-text-muted);
}

/* ── Cream panel inputs & controls (auth, form cards, modals) ── */

.auth-card .field-label,
.form-card .field-label,
.bulk-modal-card .field-label {
  color: var(--xp-text-muted);
}

.auth-card .field input,
.auth-card .field select,
.auth-card .field textarea,
.form-card .field input,
.form-card .field select,
.form-card .field textarea,
.bulk-modal-card .field input,
.bulk-modal-card .field select,
.bulk-modal-card .field textarea {
  background: var(--xp-white);
  border: var(--xp-border-soft);
  border-radius: var(--xp-radius-md);
  color: var(--xp-text);
  font-family: var(--font-sans);
}

.auth-card .field input::placeholder,
.form-card .field input::placeholder,
.form-card .field textarea::placeholder,
.bulk-modal-card .field input::placeholder {
  color: var(--xp-text-muted);
}

.auth-card .field input:hover,
.form-card .field input:hover,
.form-card .field select:hover,
.form-card .field textarea:hover,
.bulk-modal-card .field input:hover {
  border-color: var(--xp-violet-border);
}

.auth-card .field input:focus:not(:focus-visible),
.form-card .field input:focus:not(:focus-visible),
.form-card .field select:focus:not(:focus-visible),
.form-card .field textarea:focus:not(:focus-visible),
.bulk-modal-card .field input:focus:not(:focus-visible) {
  outline: none;
  border-color: var(--brand);
  background: var(--xp-white);
  box-shadow: 0 0 0 3px var(--brand-glow);
}

.auth-card .field input[disabled],
.auth-card .field input.field-readonly,
.form-card .field input[disabled] {
  background: var(--surface-light);
  color: var(--xp-text-muted);
  opacity: 1;
  cursor: not-allowed;
}

.auth-card .btn-primary,
.form-card .btn-primary,
.bulk-modal-card .btn-primary {
  border: none;
  border-radius: var(--xp-radius-md);
}

.auth-card .btn-link,
.form-card .btn-link,
.bulk-modal-card .btn-link {
  color: var(--xp-text-muted);
}

.auth-card .btn-link:hover,
.form-card .btn-link:hover,
.bulk-modal-card .btn-link:hover {
  color: var(--brand);
}

.auth-card .muted,
.form-card .muted,
.bulk-modal-card .muted {
  color: var(--xp-text-muted);
}

.auth-card .alert,
.form-card .alert,
.bulk-modal-card .alert {
  border-radius: var(--xp-radius-md);
}

.auth-card .alert-warn .alert-icon,
.form-card .alert-warn .alert-icon {
  color: var(--xp-white);
}

.muted { color: var(--fg-2); }

/* ═════════════════════════ LOGIN (direction B, split — report 00055) ══════════
   50/50 at >=1024px, one column below. Left: --deep, the mark, two glows, a
   decorative illustration drawn only in tokens, one claim. Right: the form.
   The glows are the auth exception to "no gradients" (docs/DESIGN.md §7). */
.login-b {
  min-height: 100vh;
  display: grid;
  grid-template-columns: 1fr;
  background: var(--ground);
  color: var(--body);
}
@media (min-width: 1024px) {
  .login-b { grid-template-columns: 1fr 1fr; }
}
.login-b-left {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: var(--sp-6);
  padding: var(--sp-6) var(--sp-7);
  background:
    radial-gradient(closest-side at 12% 88%, var(--accent-2-glow), transparent),
    radial-gradient(closest-side at 88% 12%, var(--accent-glow), transparent),
    var(--deep);
  overflow: hidden;
}
.login-b-brand { display: inline-flex; align-items: center; gap: var(--sp-2); }
.login-b-wordmark {
  font-size: var(--fs-6);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--tracking-title);
  color: var(--text);
}
.login-b-art { width: 100%; max-width: 480px; height: auto; align-self: center; }
.login-b-art .art-grid line { stroke: var(--grid); stroke-width: 1; }
.login-b-art .art-links line { stroke: var(--hairline-strong); stroke-width: 1.5; }
.login-b-art .art-primary { stroke: var(--accent); stroke-width: 2; }
.login-b-art .art-secondary { stroke: var(--accent-2); stroke-width: 2; }
.login-b-art .art-node { fill: var(--surface); stroke: var(--hairline-strong); stroke-width: 1.5; }
.login-b-art .art-node-primary { stroke: var(--accent); stroke-width: 2; }
.login-b-art .art-node-secondary { stroke: var(--accent-2); stroke-width: 2; }
.login-b-art .art-dot-nominal { fill: var(--nominal); }
.login-b-art .art-dot-warning { fill: var(--warning); }
.login-b-claim { max-width: 44ch; display: flex; flex-direction: column; gap: var(--sp-2); }
.login-b-headline {
  margin: 0;
  font-size: var(--fs-9);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--tracking-title);
  line-height: var(--lh-tight);
  color: var(--text);
}
.login-b-lede { margin: 0; font-size: var(--fs-4); line-height: var(--lh-relaxed); color: var(--label); }
@media (max-width: 1023px) {
  .login-b-art { display: none; }
  .login-b-left { padding: var(--sp-5) var(--sp-5); gap: var(--sp-4); }
}

.login-b-right {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--sp-6) var(--sp-5);
}
.login-b-panel { width: 100%; max-width: 380px; display: flex; flex-direction: column; gap: var(--sp-4); }
.login-b-title {
  margin: 0;
  font-size: var(--fs-8);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--tracking-title);
  color: var(--text);
}
.login-b-help { margin: 0; color: var(--label); font-size: var(--fs-4); }
.login-b-notice { margin: 0; }
.login-b-form { display: flex; flex-direction: column; gap: var(--sp-4); }
.login-b-field { display: flex; flex-direction: column; }
.login-b-form .input { min-height: var(--control-h-auth); width: 100%; }
.login-b-label-row { display: flex; align-items: baseline; justify-content: space-between; }
.login-b-forgot { font-size: var(--fs-3); color: var(--accent-2); }
.login-b-forgot:hover { color: var(--accent-2-hover); }
.login-b-remember {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--fs-3);
  color: var(--label);
  cursor: pointer;
}
.login-b-remember input { width: 16px; height: 16px; accent-color: var(--accent); }
.login-b-submit { width: 100%; min-height: var(--control-h-auth); }
.login-b-alt { margin: 0; font-size: var(--fs-3); color: var(--muted); }

/* Hive ID sign-in, below the password form. A rule with a word in it, drawn
   with borders rather than a background image so it follows the theme. */
.login-b-sso-sep {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin: var(--sp-4) 0;
  color: var(--muted);
  font-size: var(--fs-3);
}
.login-b-sso-sep::before,
.login-b-sso-sep::after {
  content: "";
  flex: 1;
  border-top: 1px solid var(--line);
}
.login-b-sso { display: block; width: 100%; text-align: center; }

/* The return visit (00056): who the remembered device says is here. */
.login-b-who {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3);
  background: var(--surface);
  border-radius: var(--r-control);
}
.login-b-avatar {
  flex: none;
  width: 40px;
  height: 40px;
  border-radius: var(--r-full);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--accent-tint);
  color: var(--accent);
  font-weight: var(--fw-semibold);
  font-size: var(--fs-5);
}
.login-b-who-text { display: flex; flex-direction: column; min-width: 0; }
.login-b-who-email { color: var(--text); font-size: var(--fs-4); overflow-wrap: anywhere; }
.login-b-who-org { color: var(--muted); font-size: var(--fs-3); }
.login-b-notyou { margin: 0; }
.login-b-notyou .btn-quiet { width: 100%; }
.login-b-until {
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-1);
  font-size: var(--fs-2);
  color: var(--faint);
}
.login-b-linkbtn {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: var(--accent-2);
  cursor: pointer;
}
.login-b-linkbtn:hover { color: var(--accent-2-hover); }

/* ═════════════════════════ APP SHELL (authenticated layout) ═════════════════ */

.app-shell {
  min-height: 100vh;
  display: flex;
  flex-direction: row;
  align-items: stretch;
}

/* Fixed left rail — logo top, nav middle, account bottom */
.app-sidebar {
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  z-index: 100;
  width: var(--sidebar-width);
  display: flex;
  flex-direction: column;
  background: var(--ink-1);
  border-right: 1px solid var(--line);
}

.app-sidebar-top {
  flex-shrink: 0;
  /* SHELL-2: trimmed so the MSP and customer rails fit a 900 px viewport. */
  padding: 12px 20px 10px;
  border-bottom: 1px solid var(--line);
}

.brand-header__link {
  display: block;
  text-decoration: none;
  color: inherit;
}

.brand-header .brand-name {
  color: var(--fg-0);
  font-size: var(--fs-6);
  font-weight: 700;
  letter-spacing: 0.02em;
  line-height: 1.1;
}

.brand-header .brand-mid {
  color: var(--accent-soft);
}

.brand-header .customer-brand-logo {
  display: block;
  max-height: 32px;
  width: auto;
  max-width: 100%;
  object-fit: contain;
}

.brand-header__clock {
  margin-top: 4px;
}

.brand-header .site-clock-label {
  font-size: var(--fs-1);
  color: var(--fg-3);
}

.brand-header .site-clock,
.brand-header #site-clock {
  font-size: var(--fs-2);
  color: var(--fg-2);
}

.sidebar-parent-brand {
  margin: 0;
  font-size: var(--fs-1);
  letter-spacing: 0.08em;
  color: var(--fg-3);
  line-height: 1.3;
}

.app-brand {
  display: flex;
  align-items: center;
  gap: 6px;
  font-weight: 700;
  letter-spacing: 0.04em;
  font-size: var(--fs-4);
}

.app-brand-stack {
  flex-direction: column;
  align-items: flex-start;
  gap: 4px;
}

.app-brand-mark {
  display: flex;
  align-items: center;
  gap: 6px;
}

.app-sidebar-top .brand-parent {
  margin-top: 0;
  font-size: var(--fs-1);
  letter-spacing: 0.08em;
  line-height: 1.2;
  max-width: 100%;
}

.site-clock-block {
  margin-top: 10px;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.site-clock-label {
  font-family: var(--font-mono);
  font-size: var(--fs-1);
  letter-spacing: 0.16em;
  color: var(--fg-3);
  text-transform: uppercase;
}

.site-clock,
#site-clock {
  font-family: var(--font-mono);
  font-size: var(--fs-3);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.06em;
  color: var(--fg-1);
  line-height: 1.2;
}

.app-sidebar-nav {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 8px 10px;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.nav-section-label {
  margin: 0;
  padding: 10px 12px 4px;
  font-size: var(--fs-1);
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--fg-3);
}

.nav-section-label:first-child {
  padding-top: 4px;
}

.nav-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 7px 12px;
  font-size: var(--fs-4);
  font-weight: 500;
  letter-spacing: 0.02em;
  color: var(--fg-1);
  border-radius: var(--xp-radius-sm);
  border-left: 3px solid transparent;
  transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease;
}

.nav-item:hover {
  color: var(--fg-0);
  background: var(--sheen-3);
}

.nav-item.active {
  color: var(--brand-soft);
  background: var(--brand-glow);
  border-left-color: var(--brand);
}

.nav-icon {
  display: flex;
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  color: inherit;
  opacity: 0.85;
}

.nav-item.active .nav-icon { opacity: 1; }

/* The support banner (R1C) — what authority this visit carries, why, and when it
   ends. Existing tokens only: no new colour, no new type size, and the chip is the
   shared component. Muted by design — it states a fact about the session, it is not
   a health verdict, and DESIGN_SYSTEM §4 reserves colour for those. */
.nav-support {
  margin: 0 0 8px;
  padding: 8px 10px;
  background: var(--accent-2-tint);
  border-radius: var(--radius);
}
.nav-support-title {
  margin: 0 0 4px;
  font-size: var(--fs-2);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg-2);
}
.nav-support-line {
  margin: 2px 0 0;
  font-size: var(--fs-3);
  color: var(--fg-1);
}
.nav-support-expiry { color: var(--fg-2); }

.nav-label {
  flex: 1;
  min-width: 0;
  line-height: 1.3;
}

.nav-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  margin-left: 6px;
  font-size: var(--fs-1);
  font-weight: 700;
  letter-spacing: 0.04em;
  line-height: 1;
  color: var(--xp-text-on-violet);
  background: var(--brand);
  border-radius: 999px;
  vertical-align: middle;
}

.nav-item.disabled {
  color: var(--fg-3);
  cursor: not-allowed;
  pointer-events: none;
}

/* Muted variant for not-yet-released modules (license shown, page pending). */
.nav-badge.soon {
  color: var(--fg-2);
  background: var(--ink-4);
  font-weight: 600;
  text-transform: uppercase;
}

/* Built-but-role-gated: the surface EXISTS, the viewer just lacks permission. A
   bordered "owner only" badge — visually distinct from .soon so it never reads as
   "coming soon". Honesty principle applied to nav (see locked_item in _shell.html). */
.nav-badge.locked {
  color: var(--fg-2);
  background: transparent;
  border: 1px solid var(--line);
  font-weight: 600;
  text-transform: uppercase;
}

.app-sidebar-footer {
  flex-shrink: 0;
  margin-top: auto;
  padding: 8px 14px 10px;
  border-top: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.sidebar-user-meta {
  display: flex;
  flex-direction: column;
  gap: 8px;
  font-size: var(--fs-3);
}

.sidebar-user-actions {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 8px;
}

.user-email {
  color: var(--fg-1);
  word-break: break-word;
  font-size: var(--fs-3);
}

.user-role {
  align-self: flex-start;
  font-size: var(--fs-1);
  letter-spacing: 0.18em;
  color: var(--accent-soft);
  border: 1px solid var(--brand);
  padding: 3px 6px;
  background: var(--brand-glow);
}

.logout-form { margin: 0; }

.btn-link {
  background: none;
  border: none;
  color: var(--fg-2);
  padding: 0;
  cursor: pointer;
  font-size: var(--fs-3);
  letter-spacing: 0.04em;
  transition: color 0.15s ease;
}

.btn-link:hover { color: var(--accent-soft); }

.app-main {
  flex: 1;
  min-width: 0;
  margin-left: var(--sidebar-width);
  padding: 32px;
  max-width: 1400px;
  width: calc(100% - var(--sidebar-width));
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  gap: 32px;
}

/* ── Banner ── */

.banner {
  display: grid;
  grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
  gap: 32px;
  align-items: end;
  border-bottom: 1px solid var(--line);
  padding-bottom: 32px;
}

.page-title {
  font-family: var(--font-sans);
  font-weight: 700;
  font-size: var(--fs-8);
  line-height: 1;
  margin: 0;
  letter-spacing: -0.02em;
  color: var(--fg-0);
}

.page-lede {
  margin: 14px 0 0;
  color: var(--fg-1);
  font-size: var(--fs-4);
  max-width: 560px;
}

.banner-status {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 16px 18px;
  border: 1px solid var(--line);
  background: var(--ink-1);
}

.status-row {
  display: grid;
  grid-template-columns: 70px 1fr;
  font-size: var(--fs-3);
  gap: 12px;
  align-items: baseline;
}

.status-key {
  color: var(--fg-3);
  letter-spacing: 0.16em;
  font-weight: 600;
}

.status-val { color: var(--fg-0); }

/* ── Tenant-context bar (platform per-tenant pages) ──────────────────────────
   Sticky identity strip so WHICH tenant the operator is acting on stays visible
   even after scrolling into an action form. Dark-by-design; reuses chip tokens. */

.tenant-context {
  position: sticky;
  top: 0;
  z-index: 30;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 8px 20px;
  margin: 0 0 24px;
  padding: 12px 18px;
  border: 1px solid var(--line);
  /* A shell element, so no coloured edge (DESIGN.md §7, report 00059): the
     tint is laid over an OPAQUE base, because this bar is sticky and the page
     scrolls underneath it. The inset shadow is the tint layer. */
  background: var(--ink-1);
  box-shadow: inset 0 0 0 100vmax var(--accent-2-tint), 0 6px 18px var(--scrim-mid);
}

.tenant-context-id {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  min-width: 0;
}

.tenant-context-kicker {
  font-size: var(--fs-1);
  letter-spacing: 0.18em;
  font-weight: 700;
  color: var(--fg-3);
}

.tenant-context-name {
  font-family: var(--font-sans);
  font-weight: 700;
  font-size: var(--fs-5);
  color: var(--fg-0);
  letter-spacing: -0.01em;
}

.tenant-context-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  font-size: var(--fs-3);
  color: var(--fg-2);
}

.tenant-context-sep { color: var(--fg-3); }

/* ── Metric cards ── */

.card-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 16px;
}

@media (max-width: 1000px) {
  .card-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 600px) {
  .card-grid { grid-template-columns: 1fr; }
}

.metric-card {
  background: var(--ink-1);
  border: 1px solid var(--line);
  padding: 20px 22px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  min-height: 160px;
  position: relative;
  overflow: hidden;
  transition: border-color 0.15s ease;
}

.metric-card:hover { border-color: var(--line-2); }

.metric-card.accent { border-color: var(--brand-accent-border); }
.metric-card.accent::before {
  content: "";
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 2px;
  background: var(--brand);
}

.card-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.card-eyebrow {
  font-size: var(--fs-1);
  letter-spacing: 0.22em;
  color: var(--fg-2);
  font-weight: 600;
}

.card-meta {
  font-size: var(--fs-1);
  letter-spacing: 0.18em;
  color: var(--fg-3);
  border: 1px solid var(--line-2);
  padding: 2px 6px;
}

.card-figure {
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex: 1;
}

.figure-num {
  font-family: var(--font-sans);
  font-size: var(--fs-9);
  line-height: 1;
  color: var(--fg-0);
  letter-spacing: -0.03em;
  font-weight: 700;
}

.figure-unit {
  font-size: var(--fs-2);
  letter-spacing: 0.16em;
  color: var(--fg-2);
}

.card-foot {
  margin: 0;
  font-size: var(--fs-3);
  color: var(--fg-2);
  line-height: 1.4;
}

/* ── Roadmap section ── */

.section-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  border-bottom: 1px solid var(--line);
  padding-bottom: 12px;
}

/* THE SECTION LABEL — 65 templates. */
.section-title {
  font-family: var(--font-sans);
  font-size: var(--fs-3);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--fg-1);
  font-weight: 600;
  margin: 0;
}

.section-meta {
  font-size: var(--fs-2);
  letter-spacing: 0.16em;
  color: var(--fg-3);
}

.phase-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
}

.phase {
  display: grid;
  grid-template-columns: 60px 1fr auto;
  align-items: center;
  gap: 20px;
  padding: 14px 0;
  border-bottom: 1px dashed var(--line);
  transition: background 0.15s ease;
}

.phase:hover { background: var(--sheen-1); }

.phase-num {
  font-family: var(--font-mono);
  font-size: var(--fs-6);
  color: var(--fg-3);
  font-weight: 700;
  letter-spacing: 0.04em;
}

.phase-body { display: flex; flex-direction: column; gap: 2px; }

.phase-body strong {
  font-family: var(--font-mono);
  font-size: var(--fs-4);
  color: var(--fg-0);
  font-weight: 600;
}

.phase-sub {
  font-size: var(--fs-3);
  color: var(--fg-2);
}

.phase-state {
  font-size: var(--fs-1);
  letter-spacing: 0.2em;
  color: var(--fg-3);
  padding: 4px 8px;
  border: 1px solid var(--line);
}

.phase.done .phase-num { color: var(--ok); }
.phase.done .phase-state {
  color: var(--ok);
  border-color: var(--ok);
}

.phase.next .phase-num { color: var(--accent-soft); }
.phase.next .phase-state {
  color: var(--accent-soft);
  border-color: var(--brand);
  background: var(--brand-glow);
}

/* ── Small screens ── */

@media (max-width: 800px) {
  .auth-shell { grid-template-columns: 1fr; }
  .auth-side { display: none; }
  .banner { grid-template-columns: 1fr; gap: 24px; }
  .page-title { font-size: var(--fs-8); }

  .app-shell { flex-direction: column; }

  .app-sidebar {
    position: sticky;
    top: 0;
    left: auto;
    bottom: auto;
    width: 100%;
    height: auto;
    max-height: none;
    flex-direction: column;
    z-index: 100;
  }

  .app-sidebar-top {
    padding: 12px 16px;
  }

  .app-sidebar-nav {
    flex: none;
    flex-direction: row;
    flex-wrap: nowrap;
    overflow-x: auto;
    padding: 8px 12px 12px;
    gap: 4px;
    -webkit-overflow-scrolling: touch;
  }

  .nav-item {
    flex-shrink: 0;
    padding: 8px 12px;
    border-left: none;
    border-bottom: 2px solid transparent;
  }

  .nav-item.active {
    border-left: none;
    border-bottom-color: var(--brand);
  }

  .nav-label { white-space: nowrap; }

  .app-sidebar-footer {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 12px 16px;
  }

  .sidebar-user-meta {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px;
  }

  .sidebar-user-actions {
    flex-direction: row;
    flex-wrap: wrap;
    gap: 12px;
  }

  .app-main {
    margin-left: 0;
    width: 100%;
    max-width: none;
    padding: 24px 16px;
  }
}

/* Selection */
::selection {
  background: var(--brand);
  color: var(--xp-text-on-violet);
}

/* Scrollbar — keep it understated */
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: var(--ink-1); }
::-webkit-scrollbar-thumb { background: var(--ink-4); border: 2px solid var(--ink-1); }
::-webkit-scrollbar-thumb:hover { background: var(--ink-5); }
/* ═════════════════════════ WAVE A: shared components ═════════════════════════ */

/* Narrower main column for forms */
.app-main.narrow { max-width: 720px; }

/* ── Banner variants ── */

.banner.narrow {
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
}

.btn-primary.inline {
  display: inline-flex;
  width: auto;
  padding: 10px 16px;
  font-size: var(--fs-3);
  margin-top: 0;
}

/* ── Form heads, back links ── */

.form-head { margin-bottom: 24px; }

.back-link {
  font-size: var(--fs-2);
  letter-spacing: 0.12em;
  color: var(--fg-2);
  display: inline-block;
  margin-bottom: 14px;
  transition: color 0.15s ease;
}
.back-link:hover { color: var(--accent-soft); }

.detail-head {
  margin-bottom: 32px;
  padding-bottom: 24px;
  border-bottom: 1px solid var(--line);
}

.detail-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  margin-top: 10px;
  align-items: center;
}

.meta-item {
  font-size: var(--fs-3);
  color: var(--fg-2);
}

.meta-item .link, .link {
  color: var(--fg-1);
  border-bottom: 1px solid var(--line-2);
  transition: color 0.15s ease, border-color 0.15s ease;
}
.meta-item .link:hover, .link:hover {
  color: var(--accent-soft);
  border-bottom-color: var(--brand);
}

.maps-link {
  display: inline-block;
  text-decoration: none;
  color: var(--fg-1);
  border-bottom: 1px solid var(--line-2);
  transition: color 0.15s ease, border-color 0.15s ease;
}
.maps-link:hover {
  color: var(--accent-soft);
  border-bottom-color: var(--brand);
}
.maps-hint {
  margin-top: 8px;
  font-size: var(--fs-2);
}

/* ── Form cards ── */

.form-card {
  background: var(--xp-cream);
  color: var(--xp-text);
  border: var(--xp-border-soft);
  border-radius: var(--xp-radius-lg);
  box-shadow: var(--xp-shadow-card);
  padding: 28px;
  display: flex;
  flex-direction: column;
  gap: 18px;
}

.field-group {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
}
@media (max-width: 560px) { .field-group { grid-template-columns: 1fr; } }

.field select,
.field textarea {
  background: var(--ink-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 12px 14px;
  color: var(--fg-0);
  font-size: var(--fs-4);
  font-family: var(--font-sans);
  transition: border-color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
}
.field select:hover, .field textarea:hover { border-color: var(--line-2); }
/* A control whose prerequisite is missing renders DISABLED rather than
   vanishing, so it must read as deliberately unavailable and not as broken —
   a browser-default disabled select is a light chip on a dark form. The
   affordance beside it is what the reader acts on. */
.field select:disabled {
  background: var(--ink-1);
  color: var(--fg-3);
  border-style: dashed;
  cursor: not-allowed;
}
.field select:disabled:hover { border-color: var(--line); }
.field select:focus:not(:focus-visible), .field textarea:focus:not(:focus-visible) {
  outline: none;
  border-color: var(--brand);
  background: var(--ink-1);
  box-shadow: 0 0 0 3px var(--brand-glow);
}
.field textarea { resize: vertical; min-height: 80px; }

.form-actions {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 6px;
}

/* ── Data tables ── */

.data-table {
  background: var(--ink-1);
  border: 1px solid var(--line);
  overflow-x: auto;
}

.data-table table {
  width: 100%;
  border-collapse: collapse;
}

.data-table th {
  text-align: left;
  font-size: var(--fs-1);
  letter-spacing: 0.22em;
  color: var(--fg-3);
  font-weight: 600;
  padding: 14px 18px;
  border-bottom: 1px solid var(--line);
  background: var(--ink-2);
}

.data-table td {
  padding: 14px 18px;
  border-bottom: 1px solid var(--line);
  font-size: var(--fs-4);
  color: var(--fg-1);
}

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

.data-table tbody tr {
  transition: background 0.12s ease;
  cursor: pointer;
}

.data-table tbody tr:hover { background: var(--sheen-2); }

.data-table.compact th,
.data-table.compact td { padding: 10px 14px; font-size: var(--fs-3); }

.data-table .strong { color: var(--fg-0); font-weight: 600; }
.data-table .strong a { color: var(--fg-0); }
.data-table .strong a:hover { color: var(--accent-soft); }
.data-table .mono { font-family: var(--font-mono); }
.data-table .num { font-variant-numeric: tabular-nums; }
.data-table .muted { color: var(--fg-2); }

/* ── Per-row Manage menu (PLAN 4.7) ──────────────────────────────────────── */

.manage-menu { display: inline-block; position: relative; }
.manage-menu-trigger {
  cursor: pointer;
  list-style: none;
  font-size: var(--fs-2);
  color: var(--accent-soft);
  padding: 2px 6px;
  border: 1px solid var(--line-2);
  border-radius: var(--radius);
}
.manage-menu-trigger::-webkit-details-marker { display: none; }
.manage-menu-trigger::after { content: " \25BE"; color: var(--fg-3); }
.manage-menu[open] .manage-menu-trigger { border-color: var(--brand); }
.manage-menu-items {
  position: absolute;
  right: 0;
  z-index: 50;
  min-width: 160px;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 2px;
  padding: 6px;
  text-align: left;
  background: var(--ink-1);
  border: 1px solid var(--line-2);
  border-radius: var(--radius);
  box-shadow: var(--shadow-md);
}
.manage-menu-items > * { padding: 4px 6px; }
.manage-menu--locked { cursor: default; }

/* ── Tier context bar (PLAN 4.2) ─────────────────────────────────────────────

   Above the sidebar AND the content, which is why it is fixed rather than a
   flex child: `.app-shell` is a row shared by every authenticated page, and
   turning it into a column would move every one of them.

   THE TIER IS CARRIED BY THE WORD INSIDE IT.  `data-tier` only tints the left
   edge — remove the colour entirely and the bar still says "Platform". */

.tier-bar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 200;            /* over the sidebar's 100 */
  height: var(--tier-bar-h);
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 16px;
  background: var(--ink-1);
  border-bottom: 1px solid var(--line);
  font-size: var(--fs-2);
  color: var(--fg-1);
  /* SHELL-2: visible, not hidden — the customer switcher's popover hangs
     below the bar and a clipping bar swallowed it. Width is held by the
     flex rules below (names shrink, controls do not), not by clipping. */
  overflow: visible;
  white-space: nowrap;
  max-width: 100vw;
  box-sizing: border-box;
}

/* Reserve the bar's height only where a bar is actually rendered, so the
   forced-password-change shell (which renders none) is untouched. */
.app-shell:has(> .tier-bar) { padding-top: var(--tier-bar-h); }
.app-shell:has(> .tier-bar) .app-sidebar { top: var(--tier-bar-h); }

/* ── INSIDE SOMEBODY ELSE'S TENANT ────────────────────────────────────────
   Taller, readable, its own background, and the way out is a button. The
   first version was a thin line of faint grey with "Exit tenant" rendered as
   an 11px text link in the corner — the least visible thing on a page whose
   subject is that you are acting inside a customer's estate.

   COLOUR ONLY REINFORCES: every word here is identical to the quiet bar's,
   so a monochrome screen loses nothing but the emphasis. */
.tier-bar[data-entered] {
  height: var(--tier-bar-h-in);
  font-size: var(--fs-4);
  color: var(--fg-0);
  /* SHELL-2: OPAQUE. The tint is a translucent token, and this bar is fixed
     over a page that scrolls under it — so the tint rides an inset shadow on
     a solid base (the .tenant-context precedent) and nothing shows through. */
  background: var(--ink-1);
  box-shadow: inset 0 0 0 100vmax var(--tier-bar-in-bg);
  border-bottom: 1px solid var(--brand);
  /* SHELL-2: ONE LINE, NEVER WIDER THAN THE VIEWPORT. Wrapping inside a
     fixed height pushed Exit tenant out of the box at 1366 px; now the
     names shrink (ellipsis, full value in title=) and the controls do not. */
  white-space: nowrap;
  gap: 10px;
}
.app-shell:has(> .tier-bar[data-entered]) { padding-top: var(--tier-bar-h-in); }
.app-shell:has(> .tier-bar[data-entered]) .app-sidebar { top: var(--tier-bar-h-in); }
.tier-bar[data-entered] .tier-bar-label { color: var(--fg-2); }
.tier-bar[data-entered] .tier-bar-name { font-size: var(--fs-5); }
.tier-bar[data-entered] .tier-bar-expiry { color: var(--warn-link); font-weight: 600; }

.tier-bar-exit-btn {
  background: var(--brand);
  border: 1px solid var(--brand);
  color: var(--xp-cream);
  font-family: var(--font-mono);
  font-size: var(--fs-2);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 7px 14px;
  border-radius: var(--radius);
  cursor: pointer;
}
.tier-bar-exit-btn:hover { background: var(--brand-hover); border-color: var(--brand-hover); }

/* THE MSP BAR (MSP-OVERNIGHT Phase 4) — the in-customer additions: the way
   home and the customer switcher. Rendered only when ui/deps attached
   msp_bar (an MSP staffer inside a customer), so none of this exists for a
   customer user. */
.tier-bar-allcust { color: var(--accent); text-decoration: none; font-size: var(--fs-3); white-space: nowrap; }
.tier-bar-allcust:hover { text-decoration: underline; }
.tier-bar-switch { position: relative; font-size: var(--fs-3); }
.tier-bar-switch > summary { cursor: pointer; color: var(--accent); list-style: none; white-space: nowrap; }
.tier-bar-switch > summary::-webkit-details-marker { display: none; }
.tier-bar-switch[open] > summary { color: var(--text); }
.tier-bar-switch-pop { position: absolute; top: calc(100% + var(--sp-2)); left: 0; z-index: 40;
  min-width: 320px; max-height: 50vh; overflow-y: auto; padding: var(--sp-3);
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-card);
  box-shadow: var(--shadow-dialog); display: flex; flex-direction: column; gap: var(--sp-2); }
.tier-bar-switch-pop input { width: 100%; }
.tier-bar-switch-pop ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.tier-bar-switch-pop form { display: block; }
.tier-bar-switch-pop button { width: 100%; text-align: left; padding: var(--sp-1) var(--sp-2);
  border: 0; border-radius: var(--r-control); background: transparent; color: var(--body);
  font: inherit; font-size: var(--fs-3); cursor: pointer; }
.tier-bar-switch-pop button:hover { background: var(--raised); }
.tier-bar-switch-current { display: block; padding: var(--sp-1) var(--sp-2); color: var(--muted); font-size: var(--fs-3); }
/* SHELL-3: the trigger IS the MSP's name — "Source1 MSP ▾" — on every page
   an MSP identity sees. It may shrink (ellipsis, full name in title=); the
   chevron never leaves it. */
.tier-bar > .tier-bar-switch { flex: 0 1 auto; min-width: 0; }
.tier-bar-msp { display: inline-flex; align-items: center; gap: var(--sp-1); min-width: 0;
  color: var(--text); font-weight: 600; border-radius: var(--r-control); padding: 0 var(--sp-1); }
.tier-bar-msp:hover { background: var(--raised); }
.tier-bar-msp:focus-visible { outline: var(--focus-ring); outline-offset: var(--focus-offset); }
.tier-bar-msp-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tier-bar-chev { flex: 0 0 auto; color: var(--fg-3); transition: transform var(--motion-fast) var(--ease-out); }
.tier-bar-switch[open] .tier-bar-chev { transform: rotate(180deg); }
.tier-bar-switch-all { color: var(--accent); font-size: var(--fs-3); text-decoration: none; padding: 0 var(--sp-2); }
.tier-bar-switch-all:hover { text-decoration: underline; }
.tier-bar-custword { margin-right: 0; }

@media (max-width: 720px) {
  /* The tenant and the way out are what must survive the width. */
  .tier-bar[data-entered] .tier-bar-acting { display: none; }
}

.tier-bar-label { color: var(--fg-3); margin-right: 4px; }
.tier-bar-tier  { font-weight: 600; letter-spacing: 0.06em; }
.tier-bar-sep   { color: var(--fg-3); }
.tier-bar-name  { font-weight: 600; }
/* SHELL-2: every bar item keeps its natural width EXCEPT the names, which
   give way first; the exit button never shrinks and never leaves the box. */
.tier-bar > * { flex: 0 0 auto; }
.tier-bar > .tier-bar-name,
.tier-bar > .tier-bar-acting,
.tier-bar > .tier-bar-reason {
  flex: 0 1 auto; min-width: 0; max-width: 100%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tier-bar > .tier-bar-exit { flex: 0 0 auto; }
/* SHELL-2: content starts BELOW the fixed bar — anchors and sticky strips
   included, so no heading renders under it. */
html:has(.tier-bar) { scroll-padding-top: calc(var(--tier-bar-h-in) + var(--sp-3)); }
.app-shell:has(> .tier-bar) .tenant-context { top: var(--tier-bar-h); }
.app-shell:has(> .tier-bar[data-entered]) .tenant-context { top: var(--tier-bar-h-in); }
.tier-bar-reason .mono { color: var(--fg-2); }
.tier-bar-expiry { color: var(--status-warning); }
.tier-bar-exit  { margin-left: auto; }
.tier-bar-exit button { font-size: var(--fs-2); }

/* Colour REINFORCES the tier on its WORD; it never carries it, and the bar
   carries no coloured edge (a shell element — DESIGN.md §7, report 00059). */
.tier-bar[data-tier="platform"] .tier-bar-tier { color: var(--accent-2); }
.tier-bar[data-tier="msp"]      .tier-bar-tier { color: var(--accent-soft); }

@media (max-width: 880px) {
  /* The tier and the tenant are the two facts worth the width; the reason and
     the countdown are still in the audit trail and on the enter page. */
  .tier-bar-reason { display: none; }
}

/* ── Shared state components (PLAN 4.3) ──────────────────────────────────────

   SIX STATES, SIX MARKS, AND NOT ONE OF THEM IS A COLOUR ALONE.  The border
   STYLE carries the state on a monochrome screen, for a colour-blind reader
   and in a printed screenshot; the word inside carries it everywhere else.
   Colour is the third channel and never the only one (DESIGN_SYSTEM §11).

   Two classes are required — `.state` AND `.state-<kind>` — so these cannot
   collide with the topology map's own `.topo-shape.state-unknown`, which is a
   different vocabulary in an SVG.  `ui/state_vocab.state_class` is the only
   thing that writes the pair, and it RAISES on a kind nobody declared. */

.state {
  display: inline-block;
  font-size: var(--fs-1);
  letter-spacing: 0.14em;
  padding: 3px 8px;
  text-transform: lowercase;
  font-weight: 600;
  border: 1px solid var(--line-2);
  border-radius: var(--radius);
  color: var(--fg-1);
}

/* The measurement that goes with the state — "last seen 2h ago", "0 of 4
   signals".  Beside the chip, never inside it: a chip holds a state and a
   sentence holds an argument (DESIGN_SYSTEM §18). */
.state-detail {
  font-size: var(--fs-1);
  color: var(--fg-2);
}

/* SOLID — the only unbroken outline in the set, so "we know, and it is good"
   is the one mark that looks whole. */
.state.state-ok {
  border-style: solid;
  color: var(--state-healthy);
  border-color: var(--state-healthy);
  background: var(--state-healthy-bg);
}

/* DASHED — a broken outline for a broken-off reading. */
.state.state-stale {
  border-style: dashed;
  color: var(--state-watch);
  border-color: var(--state-watch);
  background: var(--state-watch-bg);
}

/* DOTTED — the faintest outline, for the state where there is no measurement
   yet and one is expected.  Deliberately NOT the unreachable colour: a device
   that never responded did not become anything (report 00025). */
.state.state-awaiting {
  border-style: dotted;
  color: var(--state-watch);
  border-color: var(--state-watch);
  background: transparent;
}

/* HATCHED — the only fill, and it is grey.  Unknown is never a shade that
   reads as fine; that is the false-green defect wearing a stylesheet. */
.state.state-unknown {
  border-style: solid;
  color: var(--state-unknown);
  border-color: var(--state-unknown-edge);
  background: repeating-linear-gradient(
    135deg,
    transparent 0 4px,
    var(--state-hatch) 4px 8px
  );
}

/* DOUBLE — the hardware does not publish it.  A distinct mark rather than a
   second dashed one, because "not measured yet" and "never measurable here"
   send an operator to two different places. */
.state.state-unsupported {
  border-style: double;
  border-width: 3px;
  color: var(--fg-2);
  border-color: var(--line-2);
  background: transparent;
}

/* PADLOCK — the reader may not use this control.  The glyph is the shape;
   the role inside is the word.  It never replaces the server's own check. */
.state.state-limited {
  border-style: solid;
  color: var(--fg-2);
  border-color: var(--state-limited-edge);
  background: transparent;
}
.state.state-limited::before {
  content: "\1F512";
  margin-right: 4px;
  /* The glyph is decoration over a label that already says "<role> role"; a
     screen reader announcing "locked padlock" before it adds nothing. */
  speak: never;
}

/* ── Chips ── */

.chip {
  display: inline-block;
  font-size: var(--fs-1);
  letter-spacing: 0.14em;
  padding: 3px 8px;
  text-transform: lowercase;
  font-weight: 600;
  border: 1px solid var(--line-2);
  color: var(--fg-1);
  border-radius: var(--radius);
}
.chip.ok {
  color: var(--ok);
  border-color: var(--ok);
  background: var(--status-success-bg);
}
/* Warning / stale / pending — amber status, NOT brand violet */
.chip.amber,
.chip.warn {
  color: var(--status-warning);
  border-color: var(--status-warning);
  background: var(--status-warning-bg);
}
.chip.brand {
  color: var(--accent-soft);
  border-color: var(--brand);
  background: var(--brand-glow);
}
.chip.muted    { color: var(--fg-2);     border-color: var(--line-2);   background: transparent; }
.chip.error    { color: var(--error);    border-color: var(--error);    background: var(--status-danger-bg); }
.chip.critical { color: var(--critical); border-color: var(--critical); background: var(--status-critical-bg); }
.chip.role-admin {
  color: var(--accent-soft);
  border-color: var(--brand);
  background: var(--brand-glow);
}
.chip.role-operator { color: var(--fg-0);     border-color: var(--line-2);   background: var(--sheen-4); }
.chip.role-viewer   { color: var(--fg-2);     border-color: var(--line-2);   background: transparent; }

.row-muted td { opacity: 0.55; }
.row-actions { text-align: right; white-space: nowrap; }
.page-foot { margin-top: 16px; font-size: var(--fs-4); }

.roles-matrix { margin-bottom: 32px; }
.roles-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 20px;
}
/* Discovered-inventory asset fields (device edit/detail) — read-only value
   with a "from device" chip; manual entry collapses behind <details>. */
.discovered-value {
  display: flex; align-items: center; gap: 8px;
  padding: 9px 12px; border: 1px dashed var(--line-2); border-radius: 6px;
  font-size: var(--fs-4); color: var(--fg-1); background: var(--ink-2);
}
.asset-mismatch { font-size: var(--fs-2); color: var(--warn); margin: 2px 0 0; display: inline-block; }
.asset-manual summary {
  cursor: pointer; font-size: var(--fs-2); color: var(--fg-2);
  letter-spacing: 0.08em; text-transform: uppercase; margin: 4px 0 8px;
}
.asset-manual[open] summary { color: var(--fg-1); }

.form-card-danger {
  margin-top: 32px;
  border-color: var(--error);
  background: var(--status-danger-panel);
  color: var(--xp-text);
}
.btn-link.danger { color: var(--error); }
.field-check {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-direction: row;
}
.field-check input[type="checkbox"] { width: auto; margin: 0; }
/* RUN-4 (I-54b): a ``.field`` label wrapping a checkbox reads as one line —
   the box beside its words, as .field-check does. ``.field`` is a column, so
   without this the box sat above its own label. */
.field:has(> input[type="checkbox"]) {
  flex-direction: row;
  align-items: center;
  gap: 10px;
}
.field:has(> input[type="checkbox"]) > input[type="checkbox"] {
  width: auto;
  margin: 0;
  padding: 0;
  flex: none;
}

.detail-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  margin-top: 12px;
  align-items: center;
}
.inline-form { display: inline; margin: 0; }
.inline-form button { background: none; border: none; padding: 0; cursor: pointer; font: inherit; }

.bulk-bar {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 12px 16px;
  margin-bottom: 12px;
  border: 1px solid var(--line);
  background: var(--sheen-3);
}
.bulk-count { font-family: var(--font-mono); font-size: var(--fs-4); color: var(--fg-1); }
.col-check { width: 36px; text-align: center; }
.col-check input { width: auto; margin: 0; }

/* ── List filter bar (inventory: Devices / Sites / Probes) ── */

.list-filter-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px 14px;
  width: 100%;
  padding: 14px 18px;
  margin: 20px 0 16px;
  background: var(--ink-2);
  border: 1px solid var(--line);
  border-radius: var(--xp-radius-sm);
  box-sizing: border-box;
}

.list-filter-search-label {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.list-filter-search {
  flex: 1 1 200px;
  min-width: 160px;
  max-width: 320px;
  padding: 9px 12px 9px 36px;
  border: 1px solid var(--line);
  border-radius: var(--xp-radius-sm);
  background-color: var(--ink-3);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236f6a5e' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='M20 20l-3-3'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: 11px center;
  background-size: 15px 15px;
  color: var(--fg-0);
  font-family: var(--font-mono);
  font-size: var(--fs-4);
  transition: border-color 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease;
}

.list-filter-search::placeholder { color: var(--fg-3); }

.list-filter-search:hover {
  border-color: var(--line-2);
  background-color: var(--ink-2);
}

.list-filter-search:focus:not(:focus-visible) {
  outline: none;
  border-color: var(--brand);
  background-color: var(--ink-2);
  box-shadow: 0 0 0 3px var(--brand-glow);
}

/* Status pills — scoped so Alerts page keeps its own filter styles */
.list-filter-bar .filter-group {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  border: none;
  border-radius: 0;
  overflow: visible;
  flex-shrink: 0;
}

.list-filter-bar .filter-btn {
  margin: 0;
  padding: 7px 14px;
  border: 1px solid var(--line);
  border-radius: var(--xp-radius-sm);
  background: var(--ink-3);
  color: var(--fg-2);
  font-family: var(--font-mono);
  font-size: var(--fs-1);
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease,
    box-shadow 0.15s ease;
}

.list-filter-bar .filter-btn:hover {
  background: var(--ink-4);
  border-color: var(--line-2);
  color: var(--fg-1);
}

/* Resting tints — echo .chip.* without full saturation */
.list-filter-bar .filter-btn[data-status="all"] {
  color: var(--fg-2);
  border-color: var(--line);
  background: var(--ink-3);
}

.list-filter-bar .filter-btn[data-status="ok"],
.list-filter-bar .filter-btn[data-status="active"],
.list-filter-bar .filter-btn[data-status="online"] {
  color: var(--ok);
  border-color: var(--ok);
  background: var(--status-success-bg);
}

.list-filter-bar .filter-btn[data-status="stale"],
.list-filter-bar .filter-btn[data-status="enrolling"],
.list-filter-bar .filter-btn[data-status="pending"] {
  color: var(--status-warning);
  border-color: var(--status-warning-border);
  background: var(--status-warning-bg);
}

.list-filter-bar .filter-btn[data-status="error"] {
  color: var(--error);
  border-color: var(--status-danger-border);
  background: var(--status-danger-bg);
}

.list-filter-bar .filter-btn[data-status="offline"] {
  color: var(--fg-2);
  border-color: var(--line-2);
  background: transparent;
}

.list-filter-bar .filter-btn[data-status="paused"] {
  color: var(--fg-2);
  border-color: var(--line-2);
  background: transparent;
}

/* Active — driven by aria-pressed (set by list-filter.js) */
.list-filter-bar .filter-btn[aria-pressed="true"] {
  font-weight: 700;
  box-shadow: 0 1px 0 var(--sheen-4) inset;
}

.list-filter-bar .filter-btn[data-status="all"][aria-pressed="true"] {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--xp-text-on-violet);
  box-shadow: 0 0 0 1px var(--brand-glow);
}

.list-filter-bar .filter-btn[data-status="ok"][aria-pressed="true"],
.list-filter-bar .filter-btn[data-status="active"][aria-pressed="true"],
.list-filter-bar .filter-btn[data-status="online"][aria-pressed="true"] {
  background: var(--status-success-bg);
  border-color: var(--ok);
  color: var(--fg-0);
  box-shadow: 0 0 0 1px var(--status-success-glow);
}

.list-filter-bar .filter-btn[data-status="stale"][aria-pressed="true"],
.list-filter-bar .filter-btn[data-status="enrolling"][aria-pressed="true"],
.list-filter-bar .filter-btn[data-status="pending"][aria-pressed="true"] {
  background: var(--status-warning-bg);
  border-color: var(--status-warning);
  color: var(--fg-0);
  box-shadow: 0 0 0 1px var(--status-warning-border);
}

.list-filter-bar .filter-btn[data-status="error"][aria-pressed="true"] {
  background: var(--status-danger-bg);
  border-color: var(--error);
  color: var(--fg-0);
  box-shadow: 0 0 0 1px var(--status-danger-border);
}

.list-filter-bar .filter-btn[data-status="offline"][aria-pressed="true"],
.list-filter-bar .filter-btn[data-status="paused"][aria-pressed="true"] {
  background: var(--ink-4);
  border-color: var(--fg-2);
  color: var(--fg-0);
  box-shadow: 0 0 0 1px var(--line-2);
}

.list-filter-bar .filter-btn[aria-pressed="true"]:hover {
  filter: brightness(1.06);
}

.list-filter-count {
  margin: 0 0 0 auto;
  padding-left: 8px;
  font-family: var(--font-mono);
  font-size: var(--fs-2);
  letter-spacing: 0.06em;
  color: var(--fg-2);
  line-height: 1.4;
  flex: 0 0 auto;
  align-self: center;
  white-space: nowrap;
}

.list-filter-empty {
  flex: 1 1 100%;
  order: 10;
  margin: 8px 0 0;
  padding: 20px 12px;
  text-align: center;
  font-family: var(--font-mono);
  font-size: var(--fs-3);
  letter-spacing: 0.04em;
  color: var(--fg-3);
  border-top: 1px solid var(--line);
  background: transparent;
}

.list-filter-empty[hidden] { display: none !important; }

.data-table tbody tr.list-filter-hidden { display: none !important; }

@media (max-width: 720px) {
  .list-filter-bar {
    padding: 12px 14px;
    gap: 10px 12px;
  }

  .list-filter-search {
    flex: 1 1 100%;
    max-width: none;
  }

  .list-filter-count {
    margin-left: 0;
    flex: 1 1 100%;
    text-align: right;
  }
}

.bulk-modal { position: fixed; inset: 0; z-index: 100; display: flex; align-items: center; justify-content: center; }
.bulk-modal[hidden] { display: none !important; }
.bulk-modal-backdrop { position: absolute; inset: 0; background: var(--scrim-soft); }
.bulk-modal-card {
  position: relative;
  max-width: 480px;
  width: 90%;
  z-index: 1;
  background: var(--xp-cream);
  color: var(--xp-text);
  border: var(--xp-border-soft);
  border-radius: var(--xp-radius-lg);
  box-shadow: var(--xp-shadow-overlay);
  padding: 28px;
}
.bulk-modal-body { margin: 12px 0 16px; }
.bulk-modal-body p { margin: 0 0 8px; color: var(--xp-text-muted); }
.danger-btn { border-color: var(--error) !important; }

/* ── Empty states ── */

.empty-state {
  text-align: center;
  padding: 80px 32px;
  border: 1px dashed var(--line);
  background: var(--sheen-1);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
}

.empty-mark {
  font-family: var(--font-mono);
  font-size: var(--fs-7);
  color: var(--accent-soft);
  letter-spacing: 0.1em;
  opacity: 0.5;
}

.empty-title {
  font-family: var(--font-sans);
  font-size: var(--fs-8);
  margin: 0;
  font-weight: 700;
  color: var(--fg-0);
}

.empty-text {
  max-width: 480px;
  color: var(--fg-1);
  font-size: var(--fs-4);
  margin: 0;
}

/* ── Alerts (warnings, info banners on forms) ── */

.alert-warn {
  background: var(--status-warning-bg);
  border-color: var(--status-warning-border);
  color: var(--warn);
}
.alert-warn .alert-icon { background: var(--warn); color: var(--xp-white); }

/* ── Link cards on dashboard ── */

.metric-card.link { cursor: pointer; text-decoration: none; }
.metric-card.link:hover { border-color: var(--line-2); transform: translateY(-1px); }

/* ── Section split (probes + devices on site detail) ── */

.split-section {
  margin-top: 32px;
}

.split-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  padding-bottom: 12px;
  margin-bottom: 12px;
  border-bottom: 1px solid var(--line);
}

/* ── Token reveal card ── */

.token-card {
  background: var(--ink-1);
  border: 1px solid var(--brand);
  padding: 24px;
  margin: 24px 0;
  position: relative;
}
.token-card::before {
  content: "";
  position: absolute;
  top: 0; left: 0; right: 0; height: 2px;
  background: var(--brand);
}

.token-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 14px;
}

.token-value {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--fs-4);
  color: var(--fg-0);
  background: var(--ink-0);
  padding: 14px 16px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  word-break: break-all;
  user-select: all;
}

.token-note {
  font-size: var(--fs-3);
  color: var(--fg-2);
  margin: 12px 0 0;
}

.token-instructions {
  margin: 24px 0;
}

.code-block {
  background: var(--ink-0);
  border: 1px solid var(--line);
  padding: 14px 16px;
  font-family: var(--font-mono);
  font-size: var(--fs-3);
  color: var(--fg-1);
  border-radius: var(--radius);
  overflow-x: auto;
}

code {
  font-family: var(--font-mono);
  font-size: 0.9em;
  background: var(--ink-2);
  padding: 1px 5px;
  border: 1px solid var(--line);
  border-radius: 2px;
  color: var(--fg-0);
}

/* ── Device detail page ── */

.device-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 2.2fr);
  gap: 20px;
}
@media (max-width: 900px) { .device-grid { grid-template-columns: 1fr; } }

.info-card,
.chart-card {
  background: var(--ink-1);
  border: 1px solid var(--line);
  padding: 20px 22px;
  display: flex;
  flex-direction: column;
  gap: 16px;
  min-height: 360px;
}

.info-list {
  display: grid;
  grid-template-columns: 100px 1fr;
  gap: 8px 16px;
  margin: 0;
  font-size: var(--fs-3);
}
.info-list dt { color: var(--fg-3); letter-spacing: 0.12em; font-weight: 600; text-transform: uppercase; }
.info-list dd { margin: 0; color: var(--fg-0); }

.error-text {
  font-size: var(--fs-3);
  color: var(--error);
  padding-top: 12px;
  border-top: 1px dashed var(--line);
  margin: 0;
  font-family: var(--font-mono);
}

.inline-select {
  background: var(--ink-2);
  border: 1px solid var(--line);
  padding: 4px 8px;
  font-size: var(--fs-2);
  font-family: var(--font-mono);
  color: var(--fg-1);
  letter-spacing: 0.06em;
  border-radius: var(--radius);
  cursor: pointer;
}
.inline-select:hover { border-color: var(--line-2); color: var(--fg-0); }

.chart {
  flex: 1;
  min-height: 320px;
}

.chart-empty {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  text-align: center;
  color: var(--fg-2);
}

/* ═════════════════════════ LIVE CONSOLE (dashboard) ═════════════════════════ */

.console-page { gap: 28px; }

.console-eyebrow {
  font-family: var(--font-mono);
  font-size: var(--fs-1);
  font-weight: 600;
  letter-spacing: 0.2em;
  color: var(--fg-3);
}

.console-loading,
.console-fetch-error {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 16px 18px;
  border: 1px solid var(--line);
  background: var(--ink-1);
  font-size: var(--fs-4);
  color: var(--fg-1);
}

.console-fetch-error {
  border-color: var(--status-warning-border);
  background: var(--status-warning-bg);
  color: var(--warn);
}

.console-pulse-dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--ok);
  box-shadow: 0 0 0 3px var(--status-success-glow), 0 0 10px var(--status-success-halo);
  animation: console-pulse 2.4s ease-in-out infinite;
  flex-shrink: 0;
}

@keyframes console-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.45; transform: scale(0.92); }
}

.console-live {
  display: flex;
  flex-direction: column;
  gap: 24px;
}

/* ── THE INVENTORY STRIP ───────────────────────────────────────────────────
   Four 172px-tall cards, each carrying one number, became ONE row.

   The Overview had three stacked rows of chrome above anything an operator
   could act on: a full-bleed hero whose content was the single word "Nominal",
   a four-card posture band, and a four-card inventory grid. The hero is gone
   (it duplicated the band with less information) and this is what the grid
   became. The console-summary health reading survives as the chip and sentence
   at the head of the row — it is a DIFFERENT signal from fleet health (an
   alert + poll-outcome rollup, no score), so deleting it would have lost a
   fact rather than a decoration.

   Numbers stay mono: they are machine values, and the eyebrow above each one
   is a word a person wrote. That is the §6 split, applied. */
.console-strip {
  display: flex;
  align-items: flex-start;
  flex-wrap: wrap;
  gap: var(--sp-5);
  padding: var(--sp-3) var(--sp-4);
  background: var(--surface-card);
  border: 1px solid var(--border-default);
}
.console-strip-status {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  flex: 1 1 260px;
  min-width: 0;
  font-size: var(--fs-3);
}
.console-strip-reason { color: var(--text-secondary); }
.console-stat {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  min-width: 104px;
  color: inherit;
}
a.console-stat:hover .console-stat-num { color: var(--accent-soft); }
.console-stat-num {
  font-size: var(--fs-6);
  line-height: var(--lh-tight);
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}
.console-stat-sub { font-size: var(--fs-2); color: var(--text-tertiary); }
.console-stat .console-bar { height: 3px; border-radius: var(--r-sm); }
.console-stat .console-severity-dots { gap: var(--sp-1); }
.console-strip-updated {
  margin-left: auto;
  align-self: center;
  font-size: var(--fs-2);
  color: var(--text-disabled);
}

.console-bar {
  display: flex;
  width: 100%;
  height: 8px;
  border-radius: 4px;
  overflow: hidden;
  background: var(--ink-0);
  border: 1px solid var(--line);
}

.console-bar--empty {
  opacity: 0.35;
}

.console-bar-seg {
  min-width: 2px;
  height: 100%;
}

.console-bar-seg--ok { background: var(--ok); }
.console-bar-seg--stale { background: var(--warn); }
.console-bar-seg--error { background: var(--critical); }
.console-bar-seg--no_data { background: var(--warn); }
.console-bar-seg--paused { background: var(--fg-3); }
.console-bar-seg--pending { background: var(--brand-soft); }
.console-bar-seg--online { background: var(--ok); }
.console-bar-seg--offline { background: var(--critical); }

.console-severity-dots {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}

.console-sev-dot {
  font-family: var(--font-mono);
  font-size: var(--fs-2);
  font-weight: 600;
  padding: 3px 8px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--ink-0);
}

.console-sev-dot--critical {
  color: var(--critical);
  border-color: var(--status-danger-border);
  background: var(--status-critical-bg);
}
.console-sev-dot--warning {
  color: var(--warn);
  border-color: var(--status-warning-border);
  background: var(--status-warning-bg);
}
.console-sev-dot--info {
  color: var(--brand-soft);
  border-color: var(--brand-accent-border);
  background: var(--brand-glow);
}

/* ── Lower zone placeholders ── */

.console-lower {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 16px;
}

@media (max-width: 900px) {
  .console-lower { grid-template-columns: 1fr; }
}

.console-panel {
  background: var(--ink-1);
  border: 1px solid var(--line);
  padding: 22px 24px;
  min-height: 200px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.console-panel-title {
  margin: 0;
  font-size: var(--fs-5);
  font-weight: 600;
  color: var(--fg-0);
}

.console-panel-empty {
  margin: 12px 0 0;
  font-size: var(--fs-4);
  color: var(--fg-2);
  line-height: 1.5;
}

.console-panel-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
}

.fleet-health-rollup {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 16px;
  font-family: var(--font-mono);
  font-size: var(--fs-2);
  color: var(--fg-2);
  margin-top: 4px;
}

.fleet-health-rollup .n-ok { color: var(--ok); }
.fleet-health-rollup .n-watch { color: var(--status-warning); }
.fleet-health-rollup .n-degrading { color: var(--status-warning); }
.fleet-health-rollup .n-critical { color: var(--critical); }
/* Unscored devices — grey, never folded into healthy (see the no-false-green rule). */
.fleet-health-rollup .n-unknown { color: var(--fg-3); }

.fleet-health-worst {
  list-style: none;
  margin: 12px 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.fleet-health-worst li {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  padding: 8px 10px;
  background: var(--ink-2);
  border: 1px solid var(--line);
  font-family: var(--font-mono);
  font-size: var(--fs-2);
}

.fleet-health-worst a {
  color: var(--fg-0);
  text-decoration: none;
}

.fleet-health-worst a:hover {
  color: var(--accent-soft);
}

.console-activity-list {
  list-style: none;
  margin: 12px 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
  max-height: 280px;
  overflow-y: auto;
}

.console-activity-item {
  display: flex;
  align-items: flex-start;
  gap: 12px;
}

.console-activity-sev {
  flex-shrink: 0;
  margin-top: 2px;
  width: 10px;
  height: 10px;
  min-width: 10px;
  padding: 0;
  font-size: 0;
  border-radius: 50%;
}

.console-activity-body {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 6px 10px;
  min-width: 0;
}

.console-activity-time {
  font-family: var(--font-mono);
  font-size: var(--fs-2);
  color: var(--fg-3);
  letter-spacing: 0.02em;
}

.console-activity-label {
  font-size: var(--fs-4);
  color: var(--fg-0);
  line-height: 1.4;
  flex: 1 1 12rem;
}

.console-activity-transition {
  font-size: var(--fs-1);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg-2);
  border: 1px solid var(--line-2);
  padding: 2px 6px;
  border-radius: var(--xp-radius-sm);
}

.console-timeline {
  display: flex;
  align-items: flex-end;
  gap: 3px;
  margin-top: 16px;
  min-height: 72px;
  padding: 8px 0 4px;
  border-top: 1px solid var(--line);
}

.console-timeline-cell {
  flex: 1 1 0;
  min-width: 4px;
  height: 12px;
  border-radius: 2px 2px 0 0;
  background: var(--ink-4);
  transition: height 0.15s ease;
}

.console-timeline-cell--info {
  height: 28px;
  background: var(--state-unknown-mid);
  border: 1px solid var(--state-unknown-loud);
}

.console-timeline-cell--warning {
  height: 44px;
  background: var(--status-warning-bg);
  border: 1px solid var(--status-warning-border);
}

.console-timeline-cell--critical {
  height: 60px;
  background: var(--status-danger-bg);
  border: 1px solid var(--status-danger-border);
}

/* ── Empty state ── */

.console-empty {
  display: flex;
  flex-direction: column;
  gap: 32px;
  padding: 8px 0 24px;
}

/* The other clamp the type guard could not see (see .login-v2-hero). Now the
   scale's secondary display step: 28→32px at the floor, 40→44px at the
   ceiling. */
.console-empty-title {
  font-family: var(--font-sans);
  font-size: var(--fs-9);
  font-weight: 700;
  margin: 10px 0 0;
  letter-spacing: -0.02em;
  color: var(--fg-0);
}

.console-empty-lede {
  margin: 12px 0 0;
  max-width: 56ch;
  font-size: var(--fs-4);
  color: var(--fg-1);
  line-height: 1.55;
}

.console-steps {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 16px;
}

@media (max-width: 900px) {
  .console-steps { grid-template-columns: 1fr; }
}

.console-step {
  background: var(--ink-1);
  border: 1px solid var(--line);
  padding: 20px 22px;
  display: flex;
  gap: 16px;
  align-items: flex-start;
}

.console-step-num {
  font-family: var(--font-mono);
  font-size: var(--fs-3);
  font-weight: 600;
  color: var(--accent-soft);
  letter-spacing: 0.1em;
}

.console-step-body {
  display: flex;
  flex-direction: column;
  gap: 6px;
  font-size: var(--fs-4);
}

.console-step-body strong {
  font-size: var(--fs-5);
  color: var(--fg-0);
}

.console-step-detail { color: var(--fg-2); font-size: var(--fs-3); line-height: 1.45; }

.console-step-link {
  margin-top: 6px;
  font-family: var(--font-mono);
  font-size: var(--fs-2);
  letter-spacing: 0.08em;
  color: var(--brand-soft);
  text-transform: uppercase;
}

.console-step-link:hover { color: var(--accent-soft); }

/* First-login wizard: Finish / Skip actions under the setup steps. */
.console-empty-actions {
  display: flex;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
}

.console-empty-capabilities { margin-top: 8px; }

/* ── License-driven onboarding module states (active / available / coming-soon) ── */

.module-states { display: flex; flex-direction: column; gap: 10px; }
.module-states .section-head { margin-top: 18px; }
.module-states .section-head:first-child { margin-top: 0; }

.module-card {
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 16px 18px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  background: var(--sheen-1);
}

.module-card-head {
  display: flex;
  align-items: center;
  gap: 10px;
}

.module-card-head strong {
  font-family: var(--font-mono);
  font-size: var(--fs-4);
  color: var(--fg-0);
  font-weight: 600;
}

.module-card-blurb {
  margin: 0;
  font-size: var(--fs-3);
  color: var(--fg-2);
  line-height: 1.5;
}

/* Active: lifted by shadow; the ACTIVE chip carries the word (report 00059). */
.module-card.active { box-shadow: var(--shadow-card); }

/* Available & coming-soon: demoted so the live modules read as primary. */
.module-card.muted { opacity: 0.78; }
.module-card.disabled { opacity: 0.5; }

.module-feature-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.module-feature {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding-left: 12px;
  border-left: 1px dashed var(--line);
}

.module-feature strong {
  font-family: var(--font-mono);
  font-size: var(--fs-3);
  color: var(--fg-1);
  font-weight: 600;
}

.module-request-form {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}

.module-request-meta {
  font-family: var(--font-mono);
  font-size: var(--fs-2);
  color: var(--fg-3);
}

.module-fineprint {
  font-size: var(--fs-2);
  letter-spacing: 0.04em;
  color: var(--fg-3);
  font-style: italic;
}

.btn-secondary {
  font-family: var(--font-sans);
  font-size: var(--fs-3);
  letter-spacing: 0.04em;
  color: var(--fg-1);
  background: transparent;
  border: 1px solid var(--line);
  border-radius: 6px;
  padding: 7px 14px;
  cursor: pointer;
  transition: border-color 0.15s ease, color 0.15s ease;
}

.btn-secondary:hover {
  color: var(--accent-soft);
  border-color: var(--brand);
}

/* ── Demoted capabilities footer ── */

.console-capabilities {
  margin-top: 16px;
  padding-top: 28px;
  border-top: 1px solid var(--line);
  opacity: 0.85;
}

.console-capabilities .section-title {
  font-size: var(--fs-6);
}

.section-sub {
  margin: 6px 0 0;
  font-size: var(--fs-3);
}

/* ──────────────────────────────────────────────────────────────────────────
   Platform-operator console (Tier-0) — dark-by-design panels.
   .form-card is a CREAM auth surface; the platform console uses .panel so every
   org-management surface matches the dark NOC language (no cream cards here).
   ────────────────────────────────────────────────────────────────────────── */
.panel {
  background: var(--ink-1);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 24px;
  margin: 18px 0;
  display: flex;
  flex-direction: column;
  gap: 16px;
}
.panel .field-label { color: var(--fg-2); }
.panel .field input,
.panel .field select,
.panel .field textarea {
  background: var(--ink-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  color: var(--fg-0);
  font-size: var(--fs-4);
  padding: 12px 14px;
  font-family: var(--font-sans);
}
.panel .field input:focus,
.panel .field select:focus,
.panel .field textarea:focus:not(:focus-visible) { outline: none; border-color: var(--brand); }

.stat-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: 14px;
  margin: 18px 0;
}
.stat-card {
  background: var(--ink-1);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 18px 20px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.stat-num { font-size: var(--fs-8); font-weight: 600; color: var(--fg-0); font-family: var(--font-mono); }
.stat-label { font-size: var(--fs-2); letter-spacing: 0.08em; color: var(--fg-2); }

.card-head-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.action-row { display: flex; flex-wrap: wrap; align-items: center; gap: 18px; }
.btn-link.warn { color: var(--warn-link); }

/* ── Plan 1.1 — Platform Command Center ──────────────────────────────────────── */
.kpi-card { text-decoration: none; transition: border-color .12s ease, transform .12s ease; }
.kpi-card:hover { border-color: var(--brand); transform: translateY(-1px); }
.kpi-card.kpi-critical .stat-num { color: var(--critical); }
.kpi-card.kpi-error .stat-num    { color: var(--error); }
.kpi-card.kpi-amber .stat-num    { color: var(--warn); }

.attention-panel .section-title { margin: 0; }
.attention-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.attention-link {
  display: flex; align-items: center; gap: 12px;
  text-decoration: none; color: var(--fg-1);
  padding: 8px 6px; border-radius: var(--radius);
}
.attention-link:hover { background: var(--sheen-3); color: var(--fg-0); }
.attention-label { flex: 1; }
.attention-arrow { color: var(--fg-2); }

.filter-bar { flex-direction: row; flex-wrap: wrap; align-items: flex-end; gap: 16px; }
.filter-bar .field { display: flex; flex-direction: column; gap: 6px; }
.filter-actions { display: flex; align-items: center; gap: 14px; margin-left: auto; }

/* Phase D — audit/search filter form + system-health cards */
.filter-grid {
  display: flex; flex-wrap: wrap; align-items: flex-end; gap: 14px 18px; width: 100%;
}
.filter-grid .field { display: flex; flex-direction: column; gap: 6px; }
.health-card { display: flex; flex-direction: column; gap: 10px; }
.health-card .card-head-row { margin: 0; align-items: center; }
.kv-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.kv-list li { color: var(--fg-2); font-size: var(--fs-4); }
.check-row { display: flex; align-items: center; gap: 8px; padding: 4px 0; color: var(--fg-1); }

.invite-url {
  word-break: break-all;
  background: var(--ink-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 12px 14px;
  display: block;
  color: var(--fg-0);
  font-family: var(--font-mono);
  font-size: var(--fs-4);
}

.btn-primary.danger {
  background: var(--error);
  border-color: var(--error);
}
.btn-primary.danger:hover { filter: brightness(1.08); }

/* ── Plan 1.1 Phase B — tabbed detail pages (Org & MSP command pages) ── */
/* Server-rendered, JS-toggled tabs (no roundtrip). All panels render; a small
   inline script shows one. Honors location.hash so a lifecycle redirect can land
   on its own tab. Dark-NOC language reusing --ink/--fg/--brand tokens. */
.detail-tabs {
  display: flex;
  gap: 2px;
  flex-wrap: wrap;
  border-bottom: 1px solid var(--line);
  margin: 18px 0 0;
}
.detail-tab {
  background: none;
  border: 1px solid transparent;
  border-bottom: none;
  color: var(--fg-2);
  font-family: var(--font-mono);
  font-size: var(--fs-2);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  padding: 9px 16px;
  cursor: pointer;
  border-radius: 6px 6px 0 0;
  margin-bottom: -1px;
}
.detail-tab:hover { color: var(--fg-0); background: var(--ink-2); }
.detail-tab.active {
  color: var(--fg-0);
  background: var(--ink-2);
  border-color: var(--line);
  border-bottom-color: var(--ink-2);
}
.detail-tab .tab-count {
  color: var(--fg-3);
  margin-left: 6px;
  font-variant-numeric: tabular-nums;
}
/* PLAN 4.57 (RUN-EIGHT): every panel is VISIBLE until detail-tabs.js marks the
   root ready, so a reader without the script sees all of them, and each tab is
   an anchor that jumps to its panel. The script alone hides the inactive ones. */
.tab-panel { padding-top: 18px; }
[data-detail-tabs].tabs-ready .tab-panel { display: none; }
[data-detail-tabs].tabs-ready .tab-panel.active { display: block; }
a.detail-tab { text-decoration: none; display: inline-block; }

/* Onboarding checklist (org overview) */
.checklist { list-style: none; margin: 0; padding: 0; }
.checklist-item {
  display: flex;
  align-items: baseline;
  gap: 12px;
  padding: 10px 0;
  border-bottom: 1px solid var(--line);
}
.checklist-item:last-child { border-bottom: none; }
.checklist-mark {
  font-family: var(--font-mono);
  font-size: var(--fs-4);
  width: 1.4em;
  flex: none;
  text-align: center;
}
.checklist-item.done .checklist-mark { color: var(--ok); }
.checklist-item.todo .checklist-mark { color: var(--fg-3); }
.checklist-label { color: var(--fg-0); font-weight: 600; }
.checklist-item.todo .checklist-label { color: var(--fg-1); font-weight: 500; }
.checklist-detail {
  margin-left: auto;
  color: var(--fg-2);
  font-family: var(--font-mono);
  font-size: var(--fs-2);
}
.checklist-progress {
  font-family: var(--font-mono);
  font-size: var(--fs-3);
  color: var(--fg-2);
}

/* License-envelope usage bars (MSP detail) */
.usage-bar-row { margin: 12px 0; }
.usage-bar-head {
  display: flex;
  justify-content: space-between;
  font-family: var(--font-mono);
  font-size: var(--fs-3);
  color: var(--fg-1);
  margin-bottom: 6px;
}
.usage-bar-track {
  height: 8px;
  background: var(--ink-2);
  border: 1px solid var(--line);
  border-radius: 999px;
  overflow: hidden;
}
.usage-bar-fill {
  height: 100%;
  background: var(--brand);
  border-radius: 999px;
}
.usage-bar-fill.warn { background: var(--warn); }
.usage-bar-fill.over { background: var(--error); }

/* ═══════════════════════════════════════════════════════════════════════════
   MODULE PAGE STANDARD (§4 universal layout) — reusable across every module.
   Dark-by-design, hand CSS, existing tokens only. See templates/modules/
   _module_page.html + services/module_page.py.
   ═════════════════════════════════════════════════════════════════════════ */
.module-page { display: flex; flex-direction: column; }

/* 1 · Header — scope indicator under the lede */
.module-header .banner-text { display: flex; flex-direction: column; gap: 8px; }
.module-scope { display: flex; align-items: center; gap: 8px; margin: 2px 0 0; }
.module-scope-label {
  font-size: var(--fs-1); letter-spacing: 0.18em; text-transform: uppercase; color: var(--fg-3);
}

/* 1b · Module sub-nav — data-driven tabs under the header (Overview · …).
   Module-agnostic like the rest of the standard; reserved items are inert. */
.module-subnav {
  display: flex; flex-wrap: wrap; gap: 2px; margin: 4px 0 14px;
  border-bottom: 1px solid var(--line);
}
.module-subnav-item {
  padding: 7px 14px 8px; font-size: var(--fs-3); letter-spacing: 0.05em;
  color: var(--fg-2); text-decoration: none; border-bottom: 2px solid transparent;
}
.module-subnav-item:hover { color: var(--fg-0); }
.module-subnav-item.active {
  color: var(--fg-0); border-bottom-color: var(--brand);
}
.module-subnav-item.disabled { color: var(--fg-3); cursor: default; }

/* Enumeration-page filter bar (a module's dedicated list pages) — quiet
   inline GET form on the same dark tokens. */
.module-filter-bar {
  display: flex; flex-wrap: wrap; align-items: center; gap: 14px;
  padding: 10px 14px; margin: 0 0 14px;
  background: var(--ink-2); border: 1px solid var(--line); border-radius: 8px;
}
.module-filter { display: flex; align-items: center; gap: 7px; }
.module-filter-label {
  font-size: var(--fs-1); letter-spacing: 0.14em; text-transform: uppercase; color: var(--fg-3);
}
.module-filter select {
  background: var(--ink-1); color: var(--fg-1); border: 1px solid var(--line);
  border-radius: 6px; padding: 4px 8px; font-size: var(--fs-3);
}
.module-filter-check { gap: 6px; }

/* 2 · KPI row — reuses .stat-row/.stat-card/.kpi-card; adds note + honest unknown */
.module-kpis .stat-card { position: relative; }
.kpi-note {
  font-size: var(--fs-1); letter-spacing: 0.04em; color: var(--fg-3);
  text-transform: uppercase; margin-top: 2px;
}
/* A3 — the empty-state sentence and its next step. SENTENCE CASE on purpose:
   `.kpi-note` above is the eyebrow voice and is uppercased, which is right for
   two words and a shout for a sentence. */
.kpi-empty {
  display: block;
  font-size: var(--fs-2);
  color: var(--fg-2);
  line-height: var(--lh-normal);
  margin-top: 4px;
  white-space: normal;
}
.kpi-next {
  display: inline-block;
  font-size: var(--fs-2);
  color: var(--accent-soft);
  margin-top: 4px;
}
.kpi-card.kpi-ok .stat-num       { color: var(--ok); }
.kpi-card.kpi-brand .stat-num    { color: var(--accent-soft); }
/* Honest unknown — grey, never a false-green zero. */
.kpi-card.kpi-unknown .stat-num  { color: var(--fg-3); }
.kpi-card.kpi-unknown .stat-label{ color: var(--fg-3); }

/* 3 · Attention queue */
.module-attention { margin: 18px 0; }
.attention-count {
  font-size: var(--fs-3); color: var(--fg-2); background: var(--ink-2);
  border: 1px solid var(--line); border-radius: 999px; padding: 1px 9px;
}
.attention-link.static { cursor: default; }
.attention-link.static:hover { background: transparent; color: var(--fg-1); }
.attention-meta { color: var(--fg-3); font-size: var(--fs-2); }
.attention-clear { display: flex; align-items: center; gap: 10px; margin: 6px 0 0; }

/* 4 · Main panel + generic inventory table */
.module-main-head { margin: 22px 0 10px; }
.module-main-sub { color: var(--fg-3); font-size: var(--fs-2); letter-spacing: 0.06em; }
.module-main { margin-bottom: 4px; }
.module-table tbody tr.row-link { cursor: pointer; }
.module-table tbody tr.row-critical { background: var(--status-danger-panel); }
.module-table tbody tr.row-warn { background: var(--status-warning-bg); }
.module-table tbody tr.row-critical:hover,
.module-table tbody tr.row-warn:hover { filter: brightness(1.25); }
.module-table td .cell-sub {
  display: block; color: var(--fg-3); font-size: var(--fs-1); letter-spacing: 0.04em; margin-top: 2px;
}
.module-empty {
  padding: 28px 16px; text-align: center; color: var(--fg-3);
  font-family: var(--font-mono); font-size: var(--fs-3);
}

/* 5 · Timeline / activity */
.module-timeline { margin: 4px 0 6px; }
.timeline-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.timeline-item {
  display: flex; align-items: center; gap: 12px;
  padding: 9px 4px; border-bottom: 1px solid var(--line);
}
.timeline-item:last-child { border-bottom: none; }
.timeline-dot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; background: var(--fg-3); }
.timeline-dot.sev-critical { background: var(--critical); }
.timeline-dot.sev-warning  { background: var(--warn); }
.timeline-dot.sev-info     { background: var(--brand); }
.timeline-dot.sev-ok       { background: var(--ok); }
.timeline-ts { color: var(--fg-3); font-size: var(--fs-2); flex: 0 0 auto; min-width: 150px; }
.timeline-label { flex: 1; color: var(--fg-1); font-size: var(--fs-4); }
.timeline-transition { flex: 0 0 auto; }

/* 6 · Action panel — grid of cards; unbuilt actions are inert "soon" */
.module-actions {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: 12px; margin: 4px 0 10px;
}
.module-action {
  display: flex; flex-direction: column; gap: 4px;
  background: var(--ink-1); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 14px 16px; text-decoration: none;
  transition: border-color .12s ease, transform .12s ease;
}
a.module-action:hover { border-color: var(--brand); transform: translateY(-1px); }
.module-action-label { color: var(--fg-0); font-weight: 600; font-size: var(--fs-4); display: flex; align-items: center; gap: 8px; }
.module-action-arrow { color: var(--fg-2); }
.module-action-desc { color: var(--fg-2); font-size: var(--fs-2); line-height: 1.35; }
.module-action.disabled {
  cursor: not-allowed; opacity: 0.6; border-style: dashed;
}
.module-action.disabled .module-action-label { color: var(--fg-2); }

/* 7 · AI helper slot — reserved, clearly-labelled placeholder (never fake output) */
.ai-slot {
  margin: 4px 0 10px; border: 1px dashed var(--brand-accent-border);
}
.ai-slot-head { display: flex; align-items: center; gap: 10px; }
.ai-slot-head .section-title { margin: 0; }
.ai-slot-mark { color: var(--accent-soft); font-size: var(--fs-5); }
.ai-slot-body { color: var(--fg-1); font-size: var(--fs-4); margin: 8px 0 6px; max-width: 68ch; }
.ai-slot-scope { color: var(--fg-3); font-size: var(--fs-2); letter-spacing: 0.06em; }

/* ═══════════════════════════════════════════════════════════════════════════
   DEVICE DETAIL STANDARD (§4, device-scoped) — reuses the module-page styles;
   only the identity header is detail-specific. See templates/modules/
   _device_detail.html + services/device_detail.py.
   ═════════════════════════════════════════════════════════════════════════ */
.device-detail-page { display: flex; flex-direction: column; }
.device-detail-head { display: flex; flex-direction: column; gap: 12px; margin-bottom: 2px; }

/* One header row: back · hostname · health · risk · spacer · actions right. */
.device-head-row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.device-back { color: var(--fg-2); font-size: var(--fs-3); }
.device-back:hover { color: var(--accent-soft); }
.device-title { margin: 0; font-size: var(--fs-6); font-weight: 500; color: var(--fg-0); }
.device-risk { font-size: var(--fs-1); letter-spacing: 0.04em; }
/* Signal-model coverage beside the health chip ("scored on 3 of 4 signals").
   Sits next to the score, not in a tooltip: a partial read that only shows on
   hover is a partial read the operator will miss. */
.device-health-note { font-size: var(--fs-1); letter-spacing: 0.04em; font-style: italic; }
.device-head-actions { margin-left: auto; display: flex; align-items: center; gap: 14px; }

/* Identity panel — ONE bordered card, equal label/value columns. */
.device-identity {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: 10px 18px; margin: 0;
  background: var(--ink-1); border: 1px solid var(--line); border-radius: 8px;
  padding: 11px 14px;
}
.device-id-field { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.device-id-field dt {
  font-size: var(--fs-1); letter-spacing: 0.14em; text-transform: uppercase; color: var(--fg-3);
}
.device-id-field dd {
  margin: 0; color: var(--fg-1); font-size: var(--fs-3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.device-id-field dd.mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }

/* Compact vitals — DeviceDetail.vitals_compact: ONE quiet mono line (no bar,
   no panel). Muted lowercase labels, light values, dot-separated, generous
   gaps. Honest unknowns stay grey. */
.vitals-strip {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 0;
  margin: 12px 2px 0; padding: 0;
  font-family: var(--font-mono); font-size: var(--fs-2);
}
.vitals-strip .vital { display: inline-flex; align-items: baseline; gap: 7px; text-decoration: none; }
.vitals-strip .vital:not(:first-child)::before {
  content: "·"; color: var(--fg-3); margin: 0 13px;
}
.vitals-strip .vital-label { text-transform: lowercase; color: var(--fg-2); }
.vitals-strip .vital-value { color: var(--fg-1); font-variant-numeric: tabular-nums; }
.vitals-strip .vital.ok .vital-value       { color: var(--ok); }
.vitals-strip .vital.amber .vital-value    { color: var(--warn); }
.vitals-strip .vital.error .vital-value    { color: var(--error); }
.vitals-strip .vital.critical .vital-value { color: var(--critical); }
.vitals-strip .vital.unknown .vital-value  { color: var(--fg-3); }
a.vital:hover .vital-value { text-decoration: underline; }
/* ═══════════════════════════════════════════════════════════════════════════
   NETWORK SWITCH HEAT DECK + INTERFACE INSPECTOR STRIP (network-owned viz for
   the device-detail standard's main slot — approved Option B faceplate, page
   order C). See templates/network/_switch_faceplate.html + _switch_plate.html
   + _switch_if_drawer.html + js/switch-detail.js.

   Color semantics (failed-recently rule): up = green with utilization heat
   (--heat custom property; idle floor keeps up vs down obvious; no util data
   → flat idle fill) · down = quiet dark grey · FAILED-RECENTLY = red ·
   errors = amber · admin-down = hatch · absent slot = outline · selected =
   violet border. Port numbers render INSIDE the squares (~11px mono).
   ═════════════════════════════════════════════════════════════════════════ */
.fp-toolbar { display: flex; justify-content: flex-end; margin: 0 0 10px; }
.fp-view-toggle {
  display: inline-flex; border: 1px solid var(--line); border-radius: var(--radius);
  overflow: hidden; background: var(--ink-1);
}
.fp-toggle-btn {
  appearance: none; background: transparent; border: none; cursor: pointer;
  color: var(--fg-2); font: inherit; font-size: var(--fs-3); padding: 5px 14px;
}
.fp-toggle-btn.active { background: var(--ink-3); color: var(--fg-0); }
.fp-toggle-btn:not(.active):hover { color: var(--fg-0); }

.heat-deck { display: flex; flex-direction: column; gap: 14px; }

/* Member tabs — one per stack member; active = violet border. */
.hd-tabs { display: flex; flex-wrap: wrap; gap: 8px; }
.hd-tab {
  appearance: none; cursor: pointer; font-size: var(--fs-2); padding: 6px 14px;
  background: var(--ink-1); color: var(--fg-2);
  border: 1px solid var(--line); border-radius: var(--radius);
}
.hd-tab:hover { color: var(--fg-0); border-color: var(--fg-3); }
.hd-tab.active {
  color: var(--fg-0); background: var(--ink-2);
  border: 1.5px solid var(--brand);
}

/* The plate — one member's faceplate. Cream FRAME (border accent only). */
.hd-plate {
  background: var(--ink-1); border: 2px solid var(--plate-frame); border-radius: 8px;
  padding: 14px 16px; display: flex; flex-direction: column; gap: 12px;
}

/* Member-swap loading state: htmx marks #plate-wrap (hx-indicator) with
   .htmx-request while the next member's plate is in flight — dim the OUTGOING
   plate so a stale member is never presented as current. The clicked tab
   carries .loading until the landed plate syncs the active state (afterSwap). */
#plate-wrap.htmx-request .hd-plate { opacity: 0.45; filter: saturate(0.5); }
.hd-plate { transition: opacity 0.12s ease; }
.hd-tab.loading { border-color: var(--brand); animation: hd-tab-loading 0.9s ease-in-out infinite; }
@keyframes hd-tab-loading {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.55; }
}
.hd-plate-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.hd-plate-label {
  font-size: var(--fs-1); letter-spacing: 0.14em; text-transform: uppercase; color: var(--fg-2);
}
.hd-plate-hint { font-size: var(--fs-1); color: var(--fg-3); }

/* Banks of odd-over-even paired columns, separator gap between banks. */
.hd-banks { display: flex; gap: 14px; flex-wrap: wrap; align-items: flex-start; }
.hd-bank-wrap { display: flex; flex-direction: column; gap: 4px; }
.hd-bank { display: flex; gap: 3px; }
.hd-col { display: flex; flex-direction: column; gap: 3px; }
/* mGig bank caption — quiet registry-driven label under the covered bank. */
.hd-bank-caption {
  align-self: center; font-size: var(--fs-1); letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--fg-3);
  border-top: 1px solid var(--line); padding: 2px 6px 0;
}

/* Port squares — SOLID, number INSIDE (~12px mono). Heat fill via --heat. */
.hp-port {
  appearance: none; cursor: pointer; box-sizing: border-box;
  position: relative;
  width: 32px; height: 27px; border-radius: 3px;
  padding: 0; border: 1px solid transparent;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: var(--fs-3); line-height: 1; color: var(--fg-1);
  background: var(--ink-3);
  transition: transform .08s ease, filter .08s ease;
}
/* Infrastructure-link marker — a CURRENT LLDP/CDP neighbor on this port:
   BRIGHT violet when the neighbor is one of OUR monitored devices, DIM for
   any other discovered neighbor. Stale edges (cable unplugged) lose it. */
.hp-port.linked-monitored::after, .hp-port.linked-foreign::after {
  content: ""; position: absolute; left: 6px; right: 6px; bottom: 3px;
  height: 2px; border-radius: 1px; background: var(--brand);
}
.hp-port.linked-foreign::after { opacity: 0.4; }
.hp-port:hover { transform: translateY(-1px); filter: brightness(1.3); }
.hp-port.up {
  background: color-mix(in srgb, var(--ok) calc(var(--heat, 0.15) * 100%), var(--ink-1));
}
.hp-port.up.dark { color: var(--on-dark-strong); }
.hp-port.down { background: var(--ink-3); color: var(--fg-3); }  /* quiet — not attention */
.hp-port.failed {
  background: color-mix(in srgb, var(--critical) 72%, var(--ink-1)); color: var(--fg-0);
}
.hp-port.err {
  background: color-mix(in srgb, var(--warn) 72%, var(--ink-1)); color: var(--on-dark-strong);
}
.hp-port.admin-down {
  background: repeating-linear-gradient(45deg, var(--ink-3) 0 3px, var(--ink-2) 3px 6px);
  color: var(--fg-3);
}
.hp-port.absent {
  background: transparent; border: 1px dashed var(--ink-4); color: var(--ink-4);
  cursor: default;
}
.hp-port.void { background: transparent; border: none; cursor: default; }
/* Uplink cells carry the full short interface name (Te1/1/1), never a bare
   trailing digit — auto width, slightly smaller mono. */
.hp-port.uplink {
  width: auto; min-width: 32px; padding: 0 6px;
  font-size: var(--fs-1); letter-spacing: 0.02em;
}
.hp-port.selected { border: 1.5px solid var(--brand); }
.fp-chip.selected { background: var(--ink-4); border: 1.5px solid var(--brand); }
.fp-swatch.linked { background: var(--ink-3); border-bottom: 2px solid var(--brand); }

/* Uplink bank — LEFT-aligned below the copper banks (label then cells), full short names. */
.hd-uplinks {
  display: flex; align-items: center; justify-content: flex-start; gap: 3px;
  flex-wrap: wrap; padding-top: 4px;
}
.hd-uplinks-label {
  font-size: var(--fs-1); letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--fg-3); margin-right: 8px;
}

/* Other-interfaces / logical strips — quiet chips, never on the plate. */
.fp-logical { display: flex; align-items: baseline; gap: 12px; border-top: 1px solid var(--line); padding-top: 12px; }
.fp-logical-label {
  font-size: var(--fs-1); letter-spacing: 0.16em; text-transform: uppercase; color: var(--fg-3); flex: none;
}
.fp-logical-strip { display: flex; flex-wrap: wrap; gap: 6px; }
.fp-chip {
  appearance: none; cursor: pointer; font-family: var(--font-mono); font-size: var(--fs-1);
  padding: 2px 9px; border-radius: 999px; border: 1px solid var(--line);
  background: var(--ink-2); color: var(--fg-2);
}
.fp-chip:hover { border-color: var(--fg-2); color: var(--fg-1); }
.fp-chip.up { border-color: color-mix(in srgb, var(--ok) 55%, var(--line)); color: var(--fg-1); }
.fp-chip.failed { border-color: var(--critical); color: var(--fg-1); }
.fp-chip.admin-down { border-style: dashed; }

/* Legend. */
.fp-legend { display: flex; flex-wrap: wrap; gap: 4px 16px; font-size: var(--fs-1); color: var(--fg-3); border-top: 1px solid var(--line); padding-top: 10px; }
.fp-key { display: inline-flex; align-items: center; gap: 6px; }
.fp-swatch { width: 15px; height: 12px; border-radius: 2px; display: inline-block; box-sizing: border-box; }
.fp-swatch.up.idle { background: color-mix(in srgb, var(--ok) 15%, var(--ink-1)); }
.fp-swatch.up.busy { background: color-mix(in srgb, var(--ok) 85%, var(--ink-1)); }
.fp-swatch.down { background: var(--ink-3); }
.fp-swatch.failed { background: color-mix(in srgb, var(--critical) 72%, var(--ink-1)); }
.fp-swatch.err { background: color-mix(in srgb, var(--warn) 72%, var(--ink-1)); }
.fp-swatch.admin-down { background: repeating-linear-gradient(45deg, var(--ink-3) 0 3px, var(--ink-2) 3px 6px); }
.fp-swatch.selected { background: var(--ink-4); border: 1.5px solid var(--brand); }

/* Interface inspector — the full-width strip below the plate
   (htmx partial → #if-drawer). The selection is marked on the faceplate cell
   (.fp-chip.selected), not by a coloured edge on this card (docs/DESIGN.md §7,
   report 00057). */
.if-strip:empty { display: none; }
.if-strip-card {
  background: var(--ink-2); border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 12px 14px 14px; display: flex; flex-direction: column; gap: 12px;
}
.if-strip-row { display: flex; gap: 18px; align-items: flex-start; flex-wrap: wrap; }
.if-strip-id { flex: 1 1 220px; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.if-strip-name-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.if-insp-name { color: var(--fg-0); font-size: var(--fs-5); }
.if-drawer-close {
  appearance: none; background: transparent; border: none; cursor: pointer;
  color: var(--fg-3); font-size: var(--fs-5); line-height: 1; margin-left: auto; padding: 2px 6px;
}
.if-drawer-close:hover { color: var(--fg-0); }
.if-insp-sub { margin: 0; font-size: var(--fs-1); color: var(--fg-2); }
.if-strip-connected { margin: 2px 0 0; font-size: var(--fs-1); color: var(--fg-1); }
.if-connected-label {
  font-size: var(--fs-1); letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--accent-soft); margin-right: 4px;
}
.if-connected-label.muted { color: var(--fg-3); }
.if-insp-empty-inline { color: var(--fg-3); }

/* Center: traffic sparkline (inline SVG from real counter samples). */
.if-strip-spark { flex: 1 1 240px; display: flex; flex-direction: column; gap: 3px; align-items: flex-start; }
.if-sparkline { max-width: 100%; }
.if-sparkline polyline { stroke: var(--ok); stroke-width: 1.5; }
.if-spark-caption { font-size: var(--fs-1); color: var(--fg-3); }

/* Right: IN / OUT / ERRORS cards. */
.if-strip-cards { display: flex; gap: 8px; flex: 0 1 auto; }
.if-card {
  background: var(--ink-1); border: 1px solid var(--line); border-radius: 3px;
  padding: 8px 12px; display: flex; flex-direction: column; gap: 3px; min-width: 84px;
}
.if-card-label {
  font-size: var(--fs-1); letter-spacing: 0.14em; text-transform: uppercase; color: var(--fg-3);
}
.if-card-value { font-size: var(--fs-4); color: var(--fg-0); font-variant-numeric: tabular-nums; }
.if-card.amber .if-card-value { color: var(--warn); }
.if-card.muted .if-card-value { color: var(--fg-3); }

/* Detail row: CONNECTED / RUNNING CONFIG / RECENT. */
.if-strip-detail {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 14px; border-top: 1px solid var(--line); padding-top: 12px;
}
.if-insp-section { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
.if-insp-label {
  font-size: var(--fs-1); letter-spacing: 0.16em; text-transform: uppercase; color: var(--fg-3);
}
.if-nbr-list, .if-mac-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.if-nbr { display: flex; align-items: baseline; gap: 6px; font-size: var(--fs-2); flex-wrap: wrap; }
.if-nbr-label { color: var(--fg-0); }
.if-nbr-port, .if-nbr-extra { color: var(--fg-2); font-size: var(--fs-1); }
.if-mac-count { margin: 0; font-size: var(--fs-1); color: var(--fg-2); }
.if-mac { display: flex; gap: 10px; font-size: var(--fs-1); align-items: baseline; }
.if-mac-addr { color: var(--fg-1); }
.if-mac-vendor { color: var(--fg-2); }
.if-mac-vlan { color: var(--fg-3); }
/* Reserved-but-honest code block — becomes the real running config (§8.10). */
.if-insp-config {
  margin: 0; background: var(--ink-0); border: 1px solid var(--line); border-radius: 3px;
  padding: 10px 12px; font-family: var(--font-mono); font-size: var(--fs-1); line-height: 1.5;
  color: var(--fg-2); white-space: pre-wrap; word-break: break-word;
}
.if-insp-recent { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.if-insp-event { display: flex; align-items: baseline; gap: 7px; font-size: var(--fs-2); }
.if-insp-event-ts { color: var(--fg-3); font-size: var(--fs-1); flex: none; }
.if-insp-event-label { color: var(--fg-1); min-width: 0; }
.if-insp-empty { margin: 0; font-size: var(--fs-2); color: var(--fg-3); }

@media (max-width: 980px) {
  .if-strip-row { flex-direction: column; }
  .hd-banks { gap: 10px; }
  /* Narrow screens: 24 paired columns of 32px don't fit — degrade to the
     previous cell size instead of overflowing the plate. */
  .hp-port { width: 26px; height: 22px; font-size: var(--fs-2); }
}

/* ── PORT SIGNIFICANCE (network-owned) ──────────────────────────────────────
   Inspector monitor-toggle, the review-monitored-ports surface, and the
   detail CONNECTIONS panel's "all connections" link. Same dark tokens. */
.if-monitor-toggle {
  display: flex; align-items: center; gap: 10px; margin: 6px 0 2px; flex-wrap: wrap;
}
.if-monitor-btn { font-size: var(--fs-1); padding: 4px 10px; }
.if-monitor-reason { font-size: var(--fs-1); color: var(--fg-3); }

.connections-more { margin: 8px 2px 0; font-size: var(--fs-3); }

.ports-review-panel { padding: 14px 16px; }
.ports-review-tools { display: flex; gap: 10px; margin-bottom: 10px; }
.ports-review-list {
  list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px;
}
/* ONE fixed grid for BOTH sections (monitored + suggested), so chips and
   text line up vertically across every row:
   checkbox | port | status | class | evidence | timestamp | action.
   The suggested-row <label> is display:contents — its children ARE the grid
   items, and the whole row stays click-to-toggle. */
.ports-review-row {
  display: grid;
  grid-template-columns: 18px 110px 88px 118px minmax(0, 1fr) 212px max-content;
  column-gap: 10px; row-gap: 2px; align-items: center;
  padding: 6px 8px; border-radius: 6px;
}
.ports-review-row label { display: contents; cursor: pointer; }
.ports-review-row:not(.monitored-row):hover { background: var(--ink-2); }
.ports-review-row .chip { justify-self: start; }
.pr-check { width: 18px; }
.pr-port {
  color: var(--fg-0); font-size: var(--fs-3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Evidence/provenance: one line, ellipsized — the full text rides the
   title-attr tooltip; rows never change height because a reason is long. */
.pr-reason {
  color: var(--fg-2); font-size: var(--fs-2); min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ports-review-save { margin: 16px 0 4px; }

/* Port-significance fixups: monitored-section rows, unmonitor control, VLANs
   panel, switch-detail tabs (tabs reuse .module-subnav). */
.switch-tabs { margin-top: 0; }
.pr-setby {
  font-size: var(--fs-1); color: var(--fg-3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pr-unmonitor-btn { font-size: var(--fs-1); padding: 4px 10px; }

/* Second grid line under a monitored row: the machine-observed ifAlias
   ("switch says: …") and the HUMAN-OWNED operator note — distinct sources,
   both quieter than the port identity. */
.pr-note-line {
  grid-column: 2 / -1; display: flex; align-items: center; gap: 14px;
  min-width: 0; font-size: var(--fs-2);
}
/* RUN-4 (I-53): the auto-uplink verdict line; a stale one is quieter — it
   is a memory of where a neighbour was, not a state. */
.pr-uplink { font-size: var(--fs-2); color: var(--fg-2); }
.pr-uplink.is-stale,
.if-uplink.is-stale { color: var(--fg-3); }
.pr-alias {
  color: var(--fg-3); flex-shrink: 1; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pr-note-cell { display: inline-flex; align-items: center; gap: 8px; min-width: 0; }
.pr-note {
  color: var(--fg-2); font-style: italic; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pr-note-edit {
  background: none; border: none; padding: 0; cursor: pointer;
  font-size: var(--fs-1); color: var(--fg-3); text-decoration: underline dotted;
}
.pr-note-edit:hover { color: var(--fg-1); }
.pr-note-add { text-decoration: none; }
.pr-note-form { display: inline-flex; align-items: center; gap: 6px; }
.pr-note-input {
  background: var(--ink-1); color: var(--fg-1);
  border: 1px solid var(--ink-3); border-radius: 5px;
  font-size: var(--fs-2); padding: 3px 8px; width: 260px; max-width: 45vw;
}
.pr-note-btn { font-size: var(--fs-1); padding: 3px 8px; }

/* Operator note inside shared module tables (connections surfaces) — a muted
   quoted line under the port identity, never louder than it. */
.cell-note { font-style: italic; }

/* VLANs panel: aligned rows — id | name | member count | usage bar scaled to
   the device's largest vlan. Reserved vlans (1, 1002-1005) muted; zero-port
   vlans live in the collapsed group below the signal rows. */
.vlan-list {
  list-style: none; margin: 0; padding: 6px 4px;
  display: flex; flex-direction: column; gap: 4px;
}
.vlan-row {
  display: grid; grid-template-columns: 92px minmax(0, 1fr) 96px minmax(80px, 220px);
  column-gap: 14px; align-items: center; font-size: var(--fs-3); padding: 3px 8px;
}
.vlan-id { color: var(--fg-0); }
.vlan-name {
  color: var(--fg-1);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.vlan-count { color: var(--fg-3); font-size: var(--fs-2); }
.vlan-bar-track { height: 4px; border-radius: 2px; background: var(--ink-2); overflow: hidden; }
.vlan-bar { display: block; height: 100%; border-radius: 2px; background: var(--brand); opacity: 0.55; }
.vlan-reserved .vlan-id, .vlan-reserved .vlan-name, .vlan-reserved .vlan-count { color: var(--fg-3); }
.vlan-reserved .vlan-bar { opacity: 0.25; }
.vlan-empty-group { margin: 6px 4px 2px; }
.vlan-empty-summary {
  cursor: pointer; font-size: var(--fs-2); color: var(--fg-3); padding: 3px 8px;
  list-style-position: inside;
}
.vlan-empty-summary:hover { color: var(--fg-1); }
.vlan-list-empty .vlan-row { padding-left: 20px; }

/* ══════════════════════════════════════════════════════════════════════════
   THE LIVING TOPOLOGY — the network drawn as the hierarchy it is.

   ONE topology drawing in this app (services/topology_live.py + the
   network/_topology_map.html component, hosted by the Overview panel and by
   /network/topology). The two hosts differ by a POLICY the service applied,
   never by a second stylesheet.

   THE ONE LAW, in CSS: every property below is bound to a data-attribute the
   SERVICE measured or derived with evidence. There is no rule here that fires
   on "looks nice" — the selectors are [data-band], [data-media], [data-role],
   [data-tier], [data-stale], [data-down] and the per-edge --flow custom
   property, which is the measured rate expressed as a duration. Nothing
   animates without a number behind it.

   RESTRAINT IS THE COMPOSITION. A node is a CIRCLE — one repeated form, so
   the field reads as one network — and the role is a small glyph inside it.
   The inhabitants are one badge on the node rather than a row of pills under
   it; the legend is behind one affordance; the caption is one sentence. What
   that space was spent on instead is the only thing on this screen a
   competitor cannot draw: a line moving at the rate it measured.

   THREE INDEPENDENT CHANNELS, so no single one has to carry everything:
     GLYPH  = role (firewall, router, switch, AP, server, camera, …)
     FILL   = monitored (solid) vs discovered-only (outline)
     COLOUR = health, and only health — the one thing on a node that changes
              minute to minute, and the reason role is a glyph and not a hue.

   MOTION, AND WHY IT IS ALLOWED (DESIGN_SYSTEM §13 + §16). The system bans
   decorative animation. This is not decoration: the dash speed IS the
   measurement, the same way a needle's position is a measurement, and it
   STOPS when the measurement goes stale. A frozen line here is information —
   it says "we stopped knowing" — which is exactly the test §13 sets for the
   only kind of motion this product permits.

   COLOUR-BLIND / REDUCED-MOTION SAFE BY CONSTRUCTION: band is carried by
   colour AND stroke width, media by stroke pattern and structure (a fiber
   line has a sheath, a stack line is doubled, unknown is dotted), down by a
   dash pattern, stale by a glyph, role by silhouette AND by the word in the
   node's title and hidden text. Freeze every animation and remove every hue
   and the map still answers — which is precisely what the reduced-motion
   block at the top of this file does to it.
   ══════════════════════════════════════════════════════════════════════════ */

.topo-live { display: flex; flex-direction: column; gap: var(--sp-2); min-width: 0; }

/* ── TYPE ON THE MAP: EXACTLY TWO SIZES, AND THEY ARE TOKENS ──────────────
   The audit that produced this found five different sizes doing four
   different jobs inside one drawing — a hostname at one size, a tier name at
   another, a bundle count at a third, a popover row at a fourth — which is the
   same "three density voices" problem the design system fixed everywhere else
   and never reached inside this SVG.

   There are now two, and the rule is what each one MEANS rather than where it
   happens to sit:

     --topo-label-fs   a NAME. Node hostnames, and the WAN anchor's label.
                       Nothing else, ever.
     --topo-meta-fs    everything that is not a name: tier labels and counts,
                       the endpoint badge, the popover, the expander, the
                       bundle count, the frozen glyph, the model sub-line.

   Neither varies by tier, by role, by health or by composition. A discovered
   node's name is quieter than a monitored one's, and it is quieter by COLOUR
   and OPACITY — never by size, because a size step reads as a different KIND
   of thing and a ghost is the same kind of thing as a device. */
.topo-map {
  --topo-label-fs: var(--fs-2);
  --topo-meta-fs: var(--fs-1);
}
.topo-map {
  display: block;
  width: 100%;
  background: var(--surface-card);
  border: 1px solid var(--border-default);
  border-radius: var(--r-lg);
  overflow: visible;
}
/* FIT TO CONTENT on the full page. `--topo-aspect` is the viewBox's own ratio,
   set inline by the renderer, so the frame takes the DRAWING's shape instead
   of letterboxing it: a tall estate used to arrive as a narrow column with a
   wide empty smear either side of it, which reads as a broken page rather than
   as a small network. Clamped both ways so a very tall or very wide graph
   still fits a screen and still has somewhere to pan.

   The Overview panel keeps a FIXED height on purpose — it is one card in a
   grid of cards, and a panel whose height depends on how many switches an org
   owns would reflow the landing page around it. */
.topo-live--overview .topo-map { height: 400px; }
.topo-live--full .topo-map {
  aspect-ratio: var(--topo-aspect, 16 / 9);
  height: auto;
  min-height: 380px;
  max-height: 78vh;
  touch-action: none;
}
.topo-frame.is-panning .topo-map { cursor: grabbing; }
.topo-hint { font-size: var(--fs-2); margin: var(--sp-2) 0 0; }

/* ── TIER BANDS ───────────────────────────────────────────────────────────
   The quietest thing on the map, and the reason it reads as a hierarchy: a
   dotted rule above each band and its name in the left gutter. Tiers 0-2 are
   named by the role that defines them; a switching band is named by MEASURED
   DEPTH, never by "distribution" or "access" — words for a design intent
   nobody ever told this product. */
.topo-tier-rule {
  stroke: var(--border-subtle);
  stroke-width: 1;
  stroke-dasharray: 2 9;
  opacity: 0.7;
}
.topo-tier-label {
  fill: var(--text-tertiary);
  font-size: var(--topo-meta-fs);
  letter-spacing: 0.09em;
  text-transform: uppercase;
}
.topo-tier-count { fill: var(--text-tertiary); font-size: var(--topo-meta-fs); opacity: 0.65; }

/* ── THE WAN ANCHOR ───────────────────────────────────────────────────────
   An INFERENCE, and drawn as one: dashed everywhere, no fill, no health, and
   a title that names the firewall it was inferred from. It is absent entirely
   on an estate with no firewall, which is the lab, so the honest picture is
   the default one. */
.topo-wan-node .topo-shape-body {
  fill: none;
  stroke: var(--text-tertiary);
  stroke-width: 1.4;
  stroke-dasharray: 4 4;
}
.topo-wan-link {
  fill: none;
  stroke: var(--text-tertiary);
  stroke-width: 1.2;
  stroke-dasharray: 3 6;
  opacity: 0.5;
}
.topo-sublabel { fill: var(--text-tertiary); font-size: var(--topo-meta-fs); }

/* ── EDGES ────────────────────────────────────────────────────────────────
   Two lanes per edge: `out` is traffic leaving the measured device, `in` is
   traffic arriving at it. They are separate strokes because they are separate
   facts — a backup job pulling one direction must not read like a balanced
   trunk. A [data-route="step"] edge is an orthogonal descent between tiers; a
   [data-route="arc"] edge is a LATERAL between siblings, bowed above the band
   so a peer link can never be mistaken for a hierarchy. */
/* WIDTH IS COMPOSED FROM THREE INDEPENDENT FACTORS, and that is a fix as much
   as a feature. Band and media both used to set `stroke-width` outright, and
   the media rules sit LATER in this file at the same specificity — so on any
   link whose media we knew, the media width silently won and the utilization
   band's width channel did nothing at all. §11's promise that band is carried
   by colour AND width was therefore only half true wherever the pack had
   evidence. Each factor now contributes a MULTIPLIER on its own custom
   property, and one rule multiplies them, so nothing overrides anything.

   THE BASE follows the composition: at spotlight scale the trunk is the star
   and gets real presence; at dense scale the same weight would be a mat. */
.topo-map {
  --lane-w: 1.6;
  --band-w: 1;
  --media-w: 1;
  --dash: 7 11;
  --dash-cycle: 36;
}
.topo-map[data-scale="spotlight"] { --lane-w: 2.6; --dash: 10 15; --dash-cycle: 50; }
.topo-map[data-scale="dense"] { --lane-w: 1.3; }
.topo-lane {
  fill: none;
  stroke: var(--text-tertiary);
  stroke-width: calc(var(--lane-w) * var(--band-w) * var(--media-w));
  stroke-linecap: round;
  /* A CONSTANT dash geometry within a composition: only the DURATION varies,
     so what the eye compares between two links is speed and nothing else. */
  stroke-dasharray: var(--dash);
  opacity: 0.85;
}
.topo-lane.lane-in { opacity: 0.55; }
/* MOTION: one dash cycle per --flow seconds, set inline per lane from the
   measured rate. A lane with no --flow never animates — the fallback value is
   `paused`, so a missing measurement CANNOT become movement. */
.topo-lane { animation: topo-flow var(--flow, 1s) linear infinite; }
.topo-lane:not([style*="--flow"]) { animation-play-state: paused; }
.topo-map[data-motion="off"] .topo-lane { animation-play-state: paused; }
@keyframes topo-flow {
  from { stroke-dashoffset: var(--dash-cycle); }
  to   { stroke-dashoffset: 0; }
}
.lane-in { animation-direction: reverse; }

/* BANDS — utilization, on the semantic states. The resting band is
   deliberately NOT green: green is a health verdict in this product, and a
   link at 3% of capacity is the resting state of every healthy network, not a
   verdict. Width carries the same signal for a reader who cannot see hue. */
.topo-edge[data-band="busy"]      { --band-w: 1.4; }
.topo-edge[data-band="hot"]       { --band-w: 1.9; }
.topo-edge[data-band="saturated"] { --band-w: 2.4; }
.topo-edge[data-band="busy"]      .topo-lane { stroke: var(--state-watch); }
.topo-edge[data-band="hot"]       .topo-lane { stroke: var(--state-degrading); }
.topo-edge[data-band="saturated"] .topo-lane { stroke: var(--state-critical); }
.topo-edge[data-band="unknown"]   .topo-lane { stroke: var(--state-unknown); stroke-dasharray: 2 5; }

/* STALE — the motion stops and the line dims. The frozen glyph beside it is
   what makes the state readable rather than merely different. */
.topo-edge[data-stale="1"] .topo-lane { animation-play-state: paused; opacity: 0.35; }
.topo-frozen {
  font-size: var(--topo-meta-fs);
  fill: var(--text-tertiary);
  letter-spacing: -0.08em;
}
/* DOWN — static, dashed, unmistakable, and it outranks every band above. */
.topo-edge[data-down="1"] .topo-lane {
  stroke: var(--state-critical) !important;
  stroke-dasharray: 3 6;
  animation-play-state: paused;
  opacity: 0.9;
}
/* AMBIGUOUS — never silently resolved (B2 doctrine, carried through). */
.topo-edge[data-ambiguous="1"] .topo-lane { stroke: var(--state-watch); stroke-dasharray: 4 6; }
/* An edge inside an OPEN incident inherits the incident treatment. */
.topo-edge[data-incident="1"] .topo-lane { stroke: var(--state-critical); }

/* MEDIA — pattern and structure, never colour alone.
   fiber : a thin bright core inside a faint sheath (two strokes)
   copper: a solid, slightly heavier single stroke
   stack : doubled and squared off — chassis-internal, structural
   unknown: dotted, quiet, claiming nothing */
.topo-sheath {
  fill: none;
  stroke: var(--accent-soft);
  stroke-width: calc(var(--lane-w) * 3.8);
  opacity: 0.12;
  stroke-linecap: round;
}
.topo-edge[data-media="fiber"]  { --media-w: 0.9; }
.topo-edge[data-media="copper"] { --media-w: 1.5; }
.topo-edge[data-media="stack"]  { --media-w: 2.8; }
.topo-edge[data-media="copper"] .topo-lane { stroke-linecap: butt; }
.topo-edge[data-media="stack"]  .topo-lane { stroke-linecap: square; opacity: 0.9; }
.topo-edge[data-media="unknown"] .topo-lane { stroke-dasharray: 1.5 5; }

/* CLICKING A LINE OPENS ITS EVIDENCE. This hit path used to wear a pointer
   cursor and do nothing — a promise the map never kept. It is now a fragment
   link into the disclosure below the drawing, which is where the reasoning the
   hover no longer carries went.

   Edges are still NOT tab stops (`tabindex="-1"` in the markup): a dense graph
   would put a hundred of them between the reader and the actions row, and the
   same evidence is reachable — better — by tabbing to the disclosure itself,
   which is ordinary HTML in the document's own order. Past the evidence bound
   a line is inert and says so by not being a link at all. */
.topo-edge-hit { fill: none; stroke: transparent; stroke-width: 16; cursor: pointer; }
.topo-edge-hit--inert { cursor: default; }
.topo-bundle circle { fill: var(--surface-overlay); stroke: var(--border-strong); }
.topo-bundle text {
  fill: var(--text-secondary);
  font-family: var(--font-mono);
  font-size: var(--topo-meta-fs);
  text-anchor: middle;
}

/* ── THE NODE: ONE CIRCLE, TWELVE GLYPHS ──────────────────────────────────
   The previous version drew twelve bespoke silhouettes — a shield, a slab, a
   disc under arcs. Each read its role well and together they made the map a
   field of unlike objects, where the only things that should differ at a
   glance are health and what is moving. One repeated circle reads as ONE
   NETWORK, and the role goes inside it as a mark.

   Fill says MONITORED; outline says DISCOVERED ONLY. The glyph strokes in the
   SURFACE colour so it reads as cut out of the body rather than drawn on top
   of it — which is what keeps it legible when the health colour underneath is
   a saturated red. */
.topo-shape-body { fill: var(--state-unknown); stroke: none; }
.topo-shape-glyph {
  fill: none;
  stroke: var(--surface-page);
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* The glyphs are authored once at r=20 and scaled to the composition, so the
   stroke has to refuse the transform or it thickens with the node and
   vanishes inside a popover row. */
.topo-shape-glyph path,
.topo-pop-icon path,
.k-shape path { vector-effect: non-scaling-stroke; }
/* SCOPED TO `.topo-device`, and that is load-bearing rather than tidy: an
   unscoped `.topo-shape.state-unknown .topo-shape-body` is (0,3,0) and would
   out-rank the ghost outline rule below at (0,2,0), filling every discovered
   node solid grey and erasing the monitored / discovered-only distinction
   entirely. Health fill belongs to monitored devices, which are the only
   things this product scores. */
.topo-device .topo-shape.state-healthy   .topo-shape-body { fill: var(--state-healthy); }
.topo-device .topo-shape.state-watch     .topo-shape-body { fill: var(--state-watch); }
.topo-device .topo-shape.state-degrading .topo-shape-body { fill: var(--state-degrading); }
.topo-device .topo-shape.state-critical  .topo-shape-body { fill: var(--state-critical); }
.topo-device .topo-shape.state-unknown   .topo-shape-body { fill: var(--state-unknown); }

/* DISCOVERED ONLY — outline, at a glance and without reading a word. */
.topo-ghost .topo-shape-body,
.topo-ambiguous .topo-shape-body {
  fill: none;
  stroke: var(--text-tertiary);
  stroke-width: 1.6;
}
.topo-ghost .topo-shape-glyph,
.topo-ambiguous .topo-shape-glyph { stroke: var(--text-tertiary); }
/* AMBIGUOUS — an identity that matched more than one device we monitor. It is
   drawn precisely BECAUSE it is unresolved, so it wears the watch colour and a
   broken outline: hiding a resolution failure is the last thing this map does. */
.topo-ambiguous .topo-shape-body { stroke: var(--state-watch); stroke-dasharray: 3 3; }
.topo-mark {
  fill: var(--state-watch);
  font-family: var(--font-mono);
  font-size: var(--topo-meta-fs);
}

.topo-poll { stroke: var(--surface-card); stroke-width: 1.5; fill: var(--state-unknown); }
.topo-poll.poll-ok { fill: var(--state-healthy); }
.topo-poll.poll-amber { fill: var(--state-watch); }
.topo-poll.poll-error { fill: var(--state-critical); }

/* INCIDENT — the strongest treatment on the map belongs to the device an open
   incident is ANCHORED to. This pulse is state, not decoration: it exists
   only while an incident is open, and it stops when reduced motion is asked
   for, leaving the ring (which is what actually carries the meaning). */
.topo-alarm {
  fill: none;
  stroke: var(--state-critical);
  stroke-width: 2;
  animation: topo-alarm 2.6s var(--ease-out) infinite;
}
.topo-radius {
  fill: none;
  stroke: var(--state-critical);
  stroke-width: 1;
  stroke-dasharray: 3 5;
  opacity: 0.6;
}
/* The pulse expands from whatever radius the composition gave the node, so
   the alarm stays a ring around the device at every scale instead of a fixed
   circle that a spotlight node would sit outside of. */
@keyframes topo-alarm {
  0%, 100% { opacity: 0.85; r: var(--alarm-r, 26); }
  50%      { opacity: 0.25; r: calc(var(--alarm-r, 26) * 1.27); }
}

.topo-label {
  fill: var(--text-secondary);
  font-size: var(--topo-label-fs);
  paint-order: stroke;
  stroke: var(--surface-card);
  stroke-width: 3;
  stroke-linejoin: round;
}
/* HIERARCHY BY OPACITY AND COLOUR, NEVER BY SIZE. A discovered node's name is
   one muted step below a monitored device's and is exactly the same size: the
   size step would say "different kind of text", and it is the same kind of
   text about a less certain thing. */
.topo-device .topo-label { fill: var(--text-primary); }
.topo-ghost .topo-label,
.topo-ambiguous .topo-label { fill: var(--text-tertiary); opacity: 0.85; }
/* Past the label limit the names step aside and come back on hover/focus —
   the alternative is a hairball of overlapping text, and every name is still
   in the node's <title>. */
.topo-map[data-labels="off"] .topo-label { opacity: 0; }
.topo-map[data-labels="off"] .topo-node:hover .topo-label,
.topo-map[data-labels="off"] .topo-node:focus-visible .topo-label { opacity: 1; }

/* ── ZOOM BUCKETS — which names survive being zoomed away from ────────────
   SVG text scales with the viewBox, so a label never starts OVERLAPPING
   another (the layout reserves its box and enforces the gap); it simply stops
   being readable. These are the two sizes at which the map admits that,
   in RENDERED PIXELS, and they are `topology_live.LABEL_LEGIBLE_PX` /
   `LABEL_MINIMUM_PX` — the same numbers `labels_hidden_at` is asserted
   against, so the rule cannot be one thing in Python and another here.

     near  (≥9px)  every name
     mid   (≥6px)  monitored devices only — the structure of the estate is the
                   last thing worth losing, a discovered neighbour's name the
                   first
     far   (<6px)  none. The dots stay, and hover still carries every name.

   Set by the full page's zoom handler. The Overview panel has no zoom and no
   handler, so it stays on `near` — the server's own default — and its density
   case is `data-labels`, above. */
.topo-map[data-zoom="mid"] .topo-node[data-label-class="secondary"] .topo-label,
.topo-map[data-zoom="mid"] .topo-sublabel { opacity: 0; }
.topo-map[data-zoom="far"] .topo-label,
.topo-map[data-zoom="far"] .topo-sublabel { opacity: 0; }
.topo-map[data-zoom="mid"] .topo-node:hover .topo-label,
.topo-map[data-zoom="mid"] .topo-node:focus-visible .topo-label,
.topo-map[data-zoom="far"] .topo-node:hover .topo-label,
.topo-map[data-zoom="far"] .topo-node:focus-visible .topo-label { opacity: 1; }
.topo-node { cursor: pointer; }
.topo-node:focus-visible .topo-shape-body {
  stroke: var(--accent-soft);
  stroke-width: 2.5;
}

/* ── THE EXPAND AFFORDANCE — the map grows under the operator's finger ────
   Full page only. The Overview panel renders none at all: a landing surface
   that grows under the reader is never the same twice, and what it would grow
   into is the other page. */
.topo-expand-pill {
  fill: var(--surface-overlay);
  stroke: var(--border-strong);
  transition: fill var(--motion-fast) var(--ease-out);
}
.topo-expand:hover .topo-expand-pill { fill: var(--surface-raised); stroke: var(--accent); }
.topo-expand-text {
  fill: var(--text-secondary);
  font-family: var(--font-mono);
  font-size: var(--topo-meta-fs);
}
.topo-expand { cursor: pointer; }
.topo-expand:focus-visible .topo-expand-pill { stroke: var(--accent-soft); stroke-width: 2; }

/* ── THE ENDPOINT BADGE — the inhabitants, on the node ────────────────────
   One small count riding the device, and the split behind it on hover or
   keyboard focus. It replaced a row of up to seven labelled pills hanging
   under every switch, which on a two-switch estate was most of the drawing —
   fourteen marks describing the part of the network the map deliberately does
   NOT draw, sitting where the network is.

   The count is exact and the split is one pointer away, so nothing was rounded
   or dropped. The badge is quieter than the expander next to it, and the
   difference is the point: "+2" GROWS the map, "184" LEAVES it. */
.topo-badge {
  fill: var(--surface-overlay);
  stroke: var(--border-default);
  stroke-width: 1;
  transition: stroke var(--motion-fast) var(--ease-out);
}
.topo-badge-text { fill: var(--text-tertiary); font-size: var(--topo-meta-fs); text-anchor: middle; }
.topo-endpoints a { cursor: pointer; }
.topo-endpoints a:hover .topo-badge,
.topo-endpoints a:focus-visible .topo-badge,
.topo-endpoints[data-open] .topo-badge { stroke: var(--accent-soft); }
.topo-endpoints a:hover .topo-badge-text,
.topo-endpoints a:focus-visible .topo-badge-text,
.topo-endpoints[data-open] .topo-badge-text { fill: var(--text-secondary); }

/* THE POPOVER. Hidden by visibility rather than display so it keeps its place
   in the accessibility tree and can be reached by keyboard; it opens on hover
   and on focus, never on a timer, and it carries no animation of its own —
   the only motion permitted on this surface is a measurement.

   WHO OPENS IT, AND WHY THERE ARE TWO RULES. With scripting on, the shared
   floating-surface controller opens it by stamping `data-open` on the badge —
   because it holds ONE open surface for the whole application, and this
   popover used to render stacked underneath the node's own tooltip. The
   `:hover` rule below is the no-JS floor and is switched OFF the moment the
   controller announces itself on <html>, so exactly one mechanism is ever in
   charge and they can never both open something. */
.topo-pop { opacity: 0; visibility: hidden; pointer-events: none; }
.topo-endpoints[data-open] .topo-pop { opacity: 1; visibility: visible; }
:root:not([data-floating-js]) .topo-endpoints a:hover .topo-pop,
:root:not([data-floating-js]) .topo-endpoints a:focus-visible .topo-pop {
  opacity: 1;
  visibility: visible;
}
.topo-pop-card {
  fill: var(--surface-overlay);
  stroke: var(--border-strong);
  stroke-width: 1;
}
.topo-pop-head {
  fill: var(--text-tertiary);
  font-size: var(--topo-meta-fs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.topo-pop-count { fill: var(--text-primary); font-size: var(--topo-meta-fs); text-anchor: end; }
.topo-pop-label { fill: var(--text-secondary); font-size: var(--topo-meta-fs); }
.topo-pop-icon { stroke: var(--text-tertiary); fill: none; stroke-width: 1.6;
  stroke-linecap: round; stroke-linejoin: round; }
/* "Other" and "Not identified" name the LIMIT of what we recognised rather
   than a kind of device. Dimmed so the eye cannot read them as one — and
   deliberately NOT hidden: on the design partner's fleet "Not identified" is
   the largest group, and a map that quietly dropped it would be telling an
   operator their network is smaller than it is. */
.topo-pop-row.is-muted .topo-pop-count,
.topo-pop-row.is-muted .topo-pop-label { fill: var(--text-tertiary); }
.topo-pop-row.is-muted .topo-pop-icon { opacity: 0.6; }

/* A ring that JUST ARRIVED eases in — a state change, and the only way to see
   where the new nodes came from. It fires on the expand response ONLY: the
   server marks `is-arriving` for the ring the click opened, and a 45s refresh
   marks nothing, so the map never replays its entrance at a reader who did
   not ask for anything. The entrance is opacity rather than a stroke-dash
   draw-on because the dash channel already carries the measured rate, and an
   entrance animation there would fight the flow it is supposed to introduce.

   OPACITY ONLY, and that is not laziness: every node carries its position as
   an SVG `transform="translate(x,y)"` ATTRIBUTE, and any CSS `transform` on
   the same element replaces it outright — a scale here would fling the whole
   arriving ring to the origin for the length of the animation. The correct
   fix is a wrapper element per node, which is markup for an entrance. */
.topo-node.is-arriving,
.topo-edge.is-arriving { animation: topo-arrive var(--motion-slow) var(--ease-out); }
@keyframes topo-arrive {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* ── CAPTION + LEGEND ─────────────────────────────────────────────────────
   The caption is ONE SENTENCE: what this is a picture of, and whether it is
   live. The accounting it used to carry — collapsed rings, ambiguous
   identities, frozen edges, where the tiers came from — is `.topo-details`,
   rendered on the full page only, where the reader came for the graph.

   The legend is not optional furniture: §11 says a dense strip of marks may
   carry state only when a legend, a title and hidden text all name it. It is
   also nine rows of keys that were permanently parked under the map. Behind a
   `<details>` it is one word at rest and everything it always was on click —
   and unlike `display:none` it stays in the document, in the accessibility
   tree, and findable by in-page search. */
.topo-caption { font-size: var(--fs-2); color: var(--text-tertiary); margin: 0; }
.topo-details { font-size: var(--fs-1); margin: 0; }
.topo-legend { font-size: var(--fs-2); color: var(--text-tertiary); }
.topo-legend-toggle {
  cursor: pointer;
  font-size: var(--fs-1);
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--text-tertiary);
  width: max-content;
}
.topo-legend-toggle:focus-visible { outline: 2px solid var(--accent-soft); outline-offset: 2px; }
.topo-legend-body {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  padding-top: var(--sp-2);
}
.topo-legend-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-1) var(--sp-3);
}
.topo-legend-head {
  font-size: var(--fs-1);
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--text-tertiary);
  min-width: 3rem;
}
.topo-key { display: inline-flex; align-items: center; gap: var(--sp-1); }
.topo-key i { display: inline-block; width: 16px; height: 3px; border-radius: var(--r-sm); }
.k-shape { width: 18px; height: 18px; flex: none; }
.k-shape .topo-shape-glyph {
  fill: none;
  stroke: var(--text-tertiary);
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.k-fiber { background: var(--accent-soft); box-shadow: 0 0 0 2px var(--accent-bg); }
.k-copper { background: var(--text-tertiary); height: 4px; border-radius: 0; }
.k-stack { background: var(--text-secondary); height: 6px; border-radius: 0; }
.k-media.k-unknown {
  background: repeating-linear-gradient(90deg, var(--state-unknown) 0 2px, transparent 2px 6px);
}
.k-busy { background: var(--state-watch); }
.k-hot { background: var(--state-degrading); }
.k-saturated { background: var(--state-critical); height: 4px; }
.k-band.k-down {
  background: repeating-linear-gradient(90deg, var(--state-critical) 0 3px, transparent 3px 7px);
}
.k-band.k-frozen { background: var(--text-tertiary); opacity: 0.35; }
.k-node { width: 10px; height: 10px; border-radius: var(--r-sm); }
.k-monitored { background: var(--state-unknown); }
.k-node.k-ghost { background: transparent; border: 1.5px solid var(--text-tertiary); }
.k-node.k-endpoint {
  background: transparent;
  border: 1px dashed var(--text-tertiary);
  border-radius: var(--r-full);
}
.k-incident { background: transparent; border: 2px solid var(--state-critical); border-radius: var(--r-full); }
.topo-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-1) var(--sp-3);
  font-size: var(--fs-2);
}
.topo-endnote { display: inline-flex; flex-wrap: wrap; gap: var(--sp-1); }

/* ── LINK EVIDENCE — the relocated reasoning ──────────────────────────────
   The standard evidence disclosure (§12), hosting what the three-line hover
   stopped saying. It looks like the incident page's membership evidence and
   the pack catalog's verification block because it IS them: one component,
   one look, one promise — the conclusion is the product, the argument is one
   click away.

   `:target` marks the entry a click on the drawing landed on, so a reader who
   arrived from a line among sixty can see which one they opened. */
.topo-evidence-item {
  padding: var(--sp-2) 0;
  border-top: 1px solid var(--border-subtle);
}
.topo-evidence-item:first-of-type { border-top: none; }
.topo-evidence-item:target {
  outline: 1px solid var(--brand);
  outline-offset: var(--sp-1);
  border-radius: var(--r-sm);
}
.topo-evidence-head {
  margin: 0;
  font-size: var(--fs-3);
  font-weight: 600;
  color: var(--text-primary);
}
.topo-evidence-state {
  margin: var(--sp-1) 0 var(--sp-2);
  font-size: var(--fs-2);
  color: var(--text-tertiary);
}

/* The Overview host: the map is a panel like any other, and its head follows
   the console panel convention rather than inventing a second one. */
.console-map { display: flex; flex-direction: column; gap: var(--sp-2); }



/* Deep-linked discovered row (topology ghost click → #ghost-N). */
.module-table tr:target { outline: 1px solid var(--brand); outline-offset: -1px; }

/* ══════════════════════════════════════════════════════════════════════════
   CONFIG INTELLIGENCE — the change surface (§8.10 + config-intelligence).

   The archive page used to be thirteen identical rows, twelve of them saying
   "unchanged, 23955 bytes", with the one that changed and the one that failed
   carrying exactly the same visual weight as the rest. Everything below is in
   service of one sentence: Auvik archives configs, WE TELL YOU WHAT CHANGED.

   Colour discipline, since a diff is the one place a convention outranks a
   palette: +/- are green/red because that convention is older and stronger
   than any product's palette and inverting it would cost a reader real time.
   They are still the SEMANTIC tokens, not new colours, and they appear only
   inside .cfg-diff. "changed" is the violet ACCENT, not a severity — a config
   change is a fact, not an alarm, and the existing `.chip.brand` on the
   roster already says it that way. A FAILURE is --state-critical, because a
   backup that is not happening is a device you cannot restore.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── 1 · THE ANSWER BAND — three cells, above everything ─────────────────── */
.cfg-answer {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: var(--sp-5);
  align-items: start;
}
.cfg-answer-cell { display: flex; flex-direction: column; gap: var(--sp-1); min-width: 0; }
.cfg-answer-value {
  font-family: var(--font-sans);
  font-size: var(--fs-7);
  line-height: var(--lh-tight);
  color: var(--text-primary);
}
.cfg-answer-value.cfg-bad { color: var(--state-critical); }
.cfg-answer-value.muted { color: var(--text-tertiary); }
.cfg-answer-detail {
  font-size: var(--fs-3);
  line-height: var(--lh-relaxed);
  color: var(--text-secondary);
  max-width: var(--prose-measure);
}
.cfg-answer-sub { font-size: var(--fs-2); color: var(--text-tertiary); }
.cfg-answer-chip {
  display: flex; align-items: baseline; gap: var(--sp-2); flex-wrap: wrap;
  font-size: var(--fs-2); color: var(--text-tertiary);
}
.cfg-add { color: var(--state-healthy); }
.cfg-del { color: var(--state-critical); }
.cfg-bad { color: var(--state-critical); }
.cfg-delta { font-family: var(--font-mono); white-space: nowrap; }

/* ── 2 · THE TIMELINE STRIP — one tick per capture, no smoothing ─────────── */
.cfg-timeline {
  list-style: none; margin: var(--sp-3) 0 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: 2px; align-items: flex-end;
}
.cfg-tick {
  display: block; width: 5px; height: 22px;
  border-radius: var(--r-sm);
  background: var(--border-strong);          /* unchanged: quiet, never green */
  transition: transform var(--motion-instant) var(--ease-out);
}
a.cfg-tick:hover { transform: scaleY(1.15); }
.cfg-tick.is-changed { background: var(--accent); }
.cfg-tick.is-failed  { background: var(--state-critical); }
/* An INCOMPLETE capture is amber, never red and never the quiet grey of an
   unchanged one: we reached the device and stored what it sent, and what it
   sent is not the whole file. */
.cfg-tick.is-incomplete { background: var(--state-degrading); }
.cfg-legend { display: flex; gap: var(--sp-3); font-size: var(--fs-2); color: var(--text-tertiary); }
.cfg-key { display: inline-flex; align-items: center; gap: var(--sp-1); }
.cfg-key .cfg-tick { width: 4px; height: 12px; }

/* ── 3 · THE DIFF — unified, numbered, gutter-marked ─────────────────────── */
.cfg-diff-meta { font-size: var(--fs-2); color: var(--text-tertiary); }
.cfg-sections {
  margin: var(--sp-2) 0 0; font-size: var(--fs-3); color: var(--text-secondary);
}
.cfg-note {
  margin: var(--sp-2) 0 0;
  font-size: var(--fs-2); line-height: var(--lh-relaxed);
  color: var(--text-tertiary); max-width: var(--prose-measure);
}
.cfg-diff-actions {
  display: flex; gap: var(--sp-4); flex-wrap: wrap;
  margin: var(--sp-3) 0 var(--sp-2);
  font-size: var(--fs-2);
}
.cfg-diff {
  border: 1px solid var(--border-default);
  border-radius: var(--r-sm);
  background: var(--surface-page);
  font-family: var(--font-mono);
  font-size: var(--fs-2);
  line-height: 1.6;
  max-height: 60vh;
  overflow: auto;
}
.cfg-diff-row {
  display: grid;
  grid-template-columns: 3.5em 3.5em 1.2em minmax(0, 1fr) auto;
  gap: var(--sp-2);
  padding: 0 var(--sp-3) 0 0;
  white-space: pre-wrap;
  word-break: break-word;
  color: var(--text-secondary);
}
.cfg-num {
  text-align: right;
  color: var(--text-disabled);
  user-select: none;
  background: var(--surface-card);
  padding-right: var(--sp-1);
}
.cfg-gutter { color: var(--text-tertiary); user-select: none; text-align: center; }
.cfg-text { min-width: 0; }
.cfg-diff .diff-add { background: var(--state-healthy-bg); color: var(--state-healthy); }
.cfg-diff .diff-del { background: var(--state-critical-bg); color: var(--state-critical); }
.cfg-diff .diff-hunk { color: var(--text-disabled); background: var(--surface-card); }
.cfg-masked { align-self: center; }
/* An elided run BETWEEN hunks. Stated, never silently dropped: "nothing
   changed for 412 lines" is information the reader needs to trust the view. */
.cfg-gap {
  grid-template-columns: 1fr;
  background: var(--surface-card);
  border-top: 1px solid var(--border-subtle);
  border-bottom: 1px solid var(--border-subtle);
  padding: var(--sp-1) var(--sp-3);
}
.cfg-gap-text { color: var(--text-disabled); font-size: var(--fs-2); }

/* ── CONFIG-2 · SIDE-BY-SIDE COMPARE ──────────────────────────────────────
   ONE scroll container, ONE grid. Each aligned pair is a single row holding
   both sides, so the columns cannot drift apart and scroll together by
   construction. A long line never wraps and never widens its column: it is cut
   with an ellipsis inside its own cell (the full masked line is its title).
   Narrow viewports scroll the one container sideways rather than stacking —
   stacking would separate a line from the line that replaced it. */
.cfg-sbs {
  border: 1px solid var(--border-default);
  border-radius: var(--r-sm);
  background: var(--surface-page);
  font-family: var(--font-mono);
  font-size: var(--fs-2);
  line-height: 1.6;
  max-height: 70vh;
  overflow: auto;
}
.cfg-sbs-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  min-width: 900px;
}
.cfg-sbs-headrow {
  position: sticky; top: 0; z-index: 1;
  background: var(--surface-card);
  border-bottom: 1px solid var(--border-default);
}
.cfg-sbs-head {
  margin: 0;
  padding: var(--sp-2) var(--sp-3);
  font-family: var(--font-sans);
  font-size: var(--fs-2);
  font-weight: 600;
  color: var(--text-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cfg-sbs-cell {
  display: grid;
  grid-template-columns: 3.5em minmax(0, 1fr) auto;
  gap: var(--sp-2);
  min-width: 0;
  white-space: pre;
  color: var(--text-secondary);
}
.cfg-sbs-cell + .cfg-sbs-cell { border-left: 1px solid var(--border-subtle); }
.cfg-sbs-cell .cfg-text { overflow: hidden; text-overflow: ellipsis; }
.cfg-sbs-cell .cfg-masked { line-height: 1.2; }
.cfg-sbs .diff-add { background: var(--state-healthy-bg); color: var(--state-healthy); }
.cfg-sbs .diff-del { background: var(--state-critical-bg); color: var(--state-critical); }
.cfg-sbs-empty { background: var(--surface-card); }
/* CONFIG-3: each column header wears its side's tint, so the eye binds a
   column to its colour before the first row. */
.cfg-sbs-head-del { background: var(--state-critical-bg); }
.cfg-sbs-head-add { background: var(--state-healthy-bg); }
/* A FOLD is ONE full-width statement across both columns — readable at the
   content's own size, on a subtle fill, clickable where its lines rode along. */
.cfg-sbs-foldrow { grid-template-columns: 1fr; }
.cfg-sbs-fold {
  grid-template-columns: 1fr;
  padding: var(--sp-1) var(--sp-3);
  background: var(--surface-card);
  border-top: 1px solid var(--border-subtle);
  border-bottom: 1px solid var(--border-subtle);
  text-align: center;
}
.cfg-fold-btn {
  display: block;
  width: 100%;
  border: 0;
  cursor: pointer;
  font: inherit;
  font-size: var(--fs-2);
  color: var(--text-tertiary);
}
.cfg-fold-btn:hover { color: var(--text-secondary); background: var(--surface-raised); }
/* The masked chip never overlaps a line: the text cell ellipsizes first and
   the chip keeps its own grid column. */
.cfg-sbs-cell .cfg-masked, .cfg-diff-row .cfg-masked { flex-shrink: 0; margin-right: var(--sp-2); }

/* ── 4 · THE CAPTURE LIST — change is the organizing principle ───────────── */
.cfg-captures { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.cfg-capture { padding: var(--sp-3) 0; border-bottom: 1px solid var(--border-subtle); }
.cfg-capture:last-child { border-bottom: none; }
.cfg-capture-head {
  display: flex; align-items: baseline; gap: var(--sp-3); flex-wrap: wrap;
}
.cfg-capture-when { font-size: var(--fs-2); color: var(--text-tertiary); }
.cfg-capture-body {
  margin: var(--sp-1) 0 0; font-size: var(--fs-3); color: var(--text-secondary);
  max-width: var(--prose-measure);
}
.cfg-capture-sub {
  margin: var(--sp-1) 0 0; font-size: var(--fs-2); color: var(--text-tertiary);
  max-width: var(--prose-measure);
}
.cfg-capture-actions { display: flex; gap: var(--sp-4); margin-top: var(--sp-2); font-size: var(--fs-2); }
/* A CHANGE gets weight — in the type, not an edge (report 00059). */
.cfg-capture--change .cfg-capture-head { font-weight: 600; }
/* A FAILURE gets MORE weight, and it is the one row that also gets a fill.
   It must be impossible to scroll past: a silently-broken backup rendered as
   ambient grey is the incumbent's defect, not a style we may copy. */
.cfg-capture--failure {
  background: var(--state-critical-bg);
  padding-left: var(--sp-3);
  padding-right: var(--sp-3);
}
.cfg-capture--failure .cfg-capture-body { color: var(--text-primary); }
/* AN INCOMPLETE CAPTURE GETS THE SAME WEIGHT IN A DIFFERENT COLOUR. It must be
   as unscrollable-past as a failure — it is the same class of defect, a backup
   that looks like one and is not — but it is amber because something WAS
   captured and is readable, and red would send an operator to the connection
   when the connection was fine. */
.cfg-capture--incomplete {
  background: var(--state-degrading-bg);
  padding-left: var(--sp-3);
  padding-right: var(--sp-3);
}
.cfg-capture--incomplete .cfg-capture-body { color: var(--text-primary); }
/* A STEADY RUN is one line until asked. */
.cfg-capture--steady { padding: 0; }
.cfg-steady { margin: var(--sp-1) 0; border-color: transparent; background: transparent; }
.cfg-steady > summary { text-transform: none; letter-spacing: 0; font-family: var(--font-sans); font-size: var(--fs-3); }
.cfg-steady-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--sp-1); }
.cfg-steady-list li { display: flex; gap: var(--sp-3); align-items: baseline; font-size: var(--fs-2); }
.cfg-compare { margin-top: var(--sp-4); }
.cfg-compare > summary { text-transform: none; letter-spacing: 0; font-family: var(--font-sans); font-size: var(--fs-3); }

/* ── 5 · THE ARCHIVE BITS THAT SURVIVE ───────────────────────────────────── */
.config-pre {
  margin: 0; background: var(--surface-page); border: 1px solid var(--border-default);
  border-radius: var(--r-sm);
  padding: var(--sp-3); font-family: var(--font-mono); font-size: var(--fs-2); line-height: 1.55;
  color: var(--text-secondary); white-space: pre-wrap; word-break: break-word;
  max-height: 70vh; overflow-y: auto;
}
.config-versions td { vertical-align: baseline; }
.config-versions tr.selected td { background: var(--surface-raised); }
.config-versions input[type="radio"] { width: auto; margin: 0; }
.config-diff-form { display: flex; flex-direction: column; gap: var(--sp-3); align-items: flex-start; }

/* ──────────────────────────────────────────────────────────────────────────
   Incident engine (slice 1) — the device-page banner. Additive by design:
   it points at the correlated view, it never replaces or hides anything the
   device page already says.
   ────────────────────────────────────────────────────────────────────────── */
.device-incident-banner {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  background: var(--warning-tint); border: 1px solid var(--line);
  padding: 12px 16px;
  margin: 14px 0; text-decoration: none; color: inherit;
}
.device-incident-banner.critical { background: var(--critical-tint); }
.device-incident-banner:hover { filter: brightness(1.15); }
.device-incident-title { color: var(--fg-0); font-size: var(--fs-4); }
.device-incident-meta { color: var(--fg-3); font-size: var(--fs-2); }
.device-incident-arrow { margin-left: auto; color: var(--fg-3); }

/* ──────────────────────────────────────────────────────────────────────────
   Permission denied (403 · role) — the honest denial page. Dark-by-design
   tokens only; the shell stays intact so a denied user is never stranded.
   ────────────────────────────────────────────────────────────────────────── */
.denied-panel {
  border: 1px solid var(--line);
  box-shadow: var(--shadow-card);
  background: var(--ink-1);
  padding: 24px 26px;
  max-width: 720px;
  display: flex;
  flex-direction: column;
  gap: 20px;
}
.denied-block { display: flex; flex-direction: column; gap: 6px; }
.denied-label {
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--fs-1);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--fg-3);
}
.denied-text { margin: 0; color: var(--fg-1); font-size: var(--fs-4); line-height: 1.6; }
.denied-role {
  font-family: var(--font-mono);
  font-size: var(--fs-3);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--fg-0);
}
.denied-actions {
  display: flex;
  align-items: center;
  gap: 18px;
  flex-wrap: wrap;
  border-top: 1px solid var(--line);
  padding-top: 18px;
}

/* Devices list — why a device has no health score, beside the "— · unknown"
   chip. An UNKNOWN the operator has to explain to themselves is only half an
   answer, so the reason rides the chip rather than hiding in a tooltip. */
.health-unknown-why {
  display: block; margin-top: 2px;
  font-size: var(--fs-1); letter-spacing: 0.04em; font-style: italic;
}

/* ═════════════════ VENDOR CAPABILITY PACKS (M1) ═════════════════
   The device page's DEVICE FAMILY card and the platform catalog. The point of
   both surfaces is the negative space: what a device family does NOT report,
   stated in words, instead of an empty chassis panel that reads as "fine".
   Dark by design like every other surface — no new tokens, no light mode. */
.small { font-size: var(--fs-2); line-height: 1.5; }


/* ═════════════════ PACK VISIBILITY (M3) ═════════════════
   The pack disclosures moved onto the SHARED .evidence component
   (DESIGN_SYSTEM.md §12) — .pack-gaps and .pack-evidence are gone rather
   than left as a second disclosure look nobody calls.
   The org-facing vendor catalog (/devices/vendors), the recognition explainer
   on the add/edit device forms, and the "send us this sysObjectID" affordance
   on an unmatched device. Same dark surfaces and same chip tokens as the
   platform catalog — the two audiences share a rendering, so they share CSS. */

.pack-card { scroll-margin-top: 24px; }


/* Recognition explainer — a form CARD, not an alert: nothing is wrong, we are
   just saying how identification works before anyone wonders. */
.recognition-note .field-label { display: block; margin-bottom: 6px; }
.recognition-note .cell-note { margin: 6px 0; }

.vendor-chip-list {
  list-style: none;
  margin: 10px 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.vendor-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 8px;
  border: 1px solid var(--ink-3);
  border-radius: 4px;
  background: var(--ink-1);
}
.vendor-chip-vendor {
  font-size: var(--fs-1); letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--fg-3);
}
.vendor-chip-name { font-size: var(--fs-2); color: var(--fg-1); }

/* Unmatched sysObjectID — readonly, selectable, deliberately NOT a form that
   posts anywhere. Surfacing the identifier is the whole affordance. */
.pack-unmatched { margin-top: 10px; }
.pack-oid-field { margin: 8px 0; }
.pack-oid-field input[readonly] {
  width: 100%;
  cursor: text;
  background: var(--ink-0);
  color: var(--fg-1);
}

/* ═════════════════ INFRASTRUCTURE HUB (IA §2) ═════════════════
   Six inventory surfaces behind one nav entry. The cells are counts, not
   links-with-decoration: the number is the content and it is set in the
   display tier, because "how big is this estate" is the question the page
   answers. */
.estate-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: var(--sp-3);
}
.estate-cell {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  padding: var(--sp-4);
  background: var(--surface-card);
  border: 1px solid var(--border-default);
  border-radius: var(--r-md);
  transition: border-color var(--motion-fast) var(--ease-out),
              background var(--motion-fast) var(--ease-out);
}
.estate-cell:hover {
  border-color: var(--border-strong);
  background: var(--surface-raised);
}
.estate-count {
  font-family: var(--font-mono);
  font-size: var(--fs-9);
  line-height: var(--lh-tight);
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}
/* A counted zero is a FACT, not an unknown — it stays legible and reads as
   "nothing here yet", never as the grey we reserve for "we don't know". */
.estate-cell.is-empty .estate-count { color: var(--text-tertiary); }
.estate-label {
  font-size: var(--fs-4);
  color: var(--text-primary);
  font-weight: var(--fw-medium);
}
.estate-caption,
.estate-measure {
  font-size: var(--fs-2);
  color: var(--text-tertiary);
  line-height: var(--lh-normal);
}
.estate-note {
  margin: var(--sp-4) 0 0;
  font-size: var(--fs-3);
  color: var(--text-secondary);
  line-height: var(--lh-relaxed);
  max-width: var(--prose-measure);
}

/* ═════════════════ NAV — the primary destination ═════════════════
   Incidents is THE "what needs me" surface (IA §2). It used to be the second
   of four rows competing for the same glance; it now reads as the front door.
   The weight is deliberately quiet — a left edge and a slightly brighter
   label, not a colour fill. A permanently-coloured nav row would be a
   permanent alarm, and this product does not cry wolf in its own chrome. */
.nav-item.nav-primary {
  border-left: 2px solid var(--border-strong);
}
.nav-item.nav-primary .nav-label { color: var(--text-primary); }
.nav-item.nav-primary.active { border-left-color: var(--accent-soft); }

/* ═════════════════ OVERVIEW POSTURE BAND (IA §3) ═════════════════
   The ten-second answer. Four cards, server-rendered above the live console:
   health WITH coverage, correlation, config change, vendor recognition.

   The value is display-tier because at demo distance the value is the message;
   the proof line is small and quiet because it is what the reader looks at
   SECOND, once the number has made them curious. That ordering is the whole
   design: a number, then why the number is trustworthy. */
.posture-band {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: var(--sp-3);
  margin-bottom: var(--sp-5);
}
.posture-card {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  padding: var(--sp-4);
  background: var(--surface-card);
  border: 1px solid var(--border-default);
  border-radius: var(--r-md);
  transition: border-color var(--motion-fast) var(--ease-out),
              background var(--motion-fast) var(--ease-out);
}
.posture-card:hover {
  background: var(--surface-raised);
  border-color: var(--border-strong);
}
.posture-label { color: var(--text-tertiary); }
.posture-value {
  font-family: var(--font-mono);
  font-size: var(--fs-9);
  line-height: var(--lh-tight);
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}
.posture-detail {
  font-size: var(--fs-3);
  color: var(--text-secondary);
  line-height: var(--lh-normal);
}
/* The capability sentence. Sans, not mono: a person wrote it. */
.posture-proof {
  margin-top: var(--sp-2);
  padding-top: var(--sp-2);
  border-top: 1px solid var(--border-subtle);
  font-size: var(--fs-2);
  color: var(--text-tertiary);
  line-height: var(--lh-relaxed);
}

/* THE VALUE carries the state — its colour, beside the chip word the card
   already renders — and only when the state is real. A card with nothing wrong
   stays neutral: colour means something happened. There is NO coloured edge:
   a top stripe is the left stripe turned ninety degrees (docs/DESIGN.md §7,
   report 00057). */
.posture-card.accent-ok .posture-value       { color: var(--state-healthy); }
.posture-card.accent-amber .posture-value    { color: var(--state-watch); }
.posture-card.accent-error .posture-value    { color: var(--state-degrading); }
.posture-card.accent-critical .posture-value { color: var(--state-critical); }

/* UNKNOWN IS GREY. Not a dimmer green, not an empty card — the value reads
   "unknown"/"—" in the neutral, and the card keeps its full weight because
   "we do not know" is a real answer, not a missing one. */
.posture-card.is-unknown .posture-value { color: var(--state-unknown); }

/* ═════════════════ EMPTY-STATE RULE, APPLIED TO THE CONSOLE ═════════════════
   THE single biggest visual win available (DESIGN_SYSTEM.md §10).

   The three console panels — fleet health, activity feed, 24h timeline — each
   reserve `min-height: 200px`. At lab scale all three are empty, so the
   landing page spent ~600px of vertical space, plus borders and headings,
   announcing that nothing had happened. Three panels' worth of chrome to
   deliver eleven words.

   A panel whose visible content is an absence line collapses to the height of
   that line. `:has()` does this without touching the JS that toggles [hidden]
   — the markup and the behaviour are untouched, which is what keeps a purely
   visual slice purely visual. */
.console-panel:has(> .console-panel-empty:not([hidden])) {
  min-height: 0;
  padding: var(--sp-3) var(--sp-4);
  gap: var(--sp-1);
}
/* An absence reads as absent: quiet, one line, and never styled like data. */
.console-panel-empty {
  margin: 0;
  font-size: var(--fs-3);
  color: var(--text-tertiary);
  line-height: var(--lh-normal);
}
/* SHELL-2: no leading dash on an empty panel line (see .absence). */
/* When BOTH lower panels are empty they stop being a two-column grid of
   nothing and sit on one line together. */
.console-lower:has(.console-panel-empty:not([hidden])) { gap: var(--sp-3); }

/* A rail row that stands alone where a group would otherwise sit. Carries the
   separation a nav-section-label gave, without repeating its own name as a
   heading directly above itself. */
.nav-item--section { margin-top: var(--sp-4); }

/* ═══════════════════════════════════════════════════════════════════════════
   CONNECTIONS, GROUPED — the summary band, the category sections, the row.

   The page this styles used to be one table of 264 rows whose first column
   repeated the page's own title. What replaces the wall is three components,
   and the restraint is the design: no new colour, no new type step, no new
   radius. The register comes from the space around the lead number and ONE
   animation that is a measurement (see .conn-row[data-new]).
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── THE SUMMARY BAND ─────────────────────────────────────────────────────
   The answer, then the split. The lead number reuses .stat-num and the chips
   are the category filter — a count you can click is one control, not two. */
.conn-summary {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-4) var(--sp-6);
  padding: var(--sp-4) var(--panel-pad);
  margin: 0 0 var(--stack-gap);
  background: var(--surface-card);
  border: 1px solid var(--border-default);
  border-radius: var(--r-md);
}
.conn-summary-lead {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  min-width: 8ch;
}
.conn-summary-side {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  flex: 1 1 40ch;
}
.conn-summary-caption {
  margin: 0;
  font-size: var(--fs-2);
  color: var(--text-tertiary);
  letter-spacing: 0.04em;
}
.conn-chips { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.conn-chip {
  display: inline-flex;
  align-items: baseline;
  gap: var(--sp-2);
  padding: var(--sp-1) var(--sp-3);
  border: 1px solid var(--border-default);
  border-radius: var(--r-full);
  background: var(--surface-raised);
  color: var(--text-secondary);
  font-size: var(--fs-3);
  transition: border-color var(--motion-fast) var(--ease-out),
              color var(--motion-fast) var(--ease-out);
}
.conn-chip:hover { border-color: var(--accent); color: var(--text-primary); }
/* ACTIVE = this filter is on. Violet is identity/interaction here, never a
   severity (§5) — it says "you chose this", which is exactly what it means
   everywhere else in the app. */
.conn-chip.active {
  border-color: var(--accent);
  background: var(--accent-bg);
  color: var(--text-primary);
}
.conn-chip.active::after { content: "×"; color: var(--accent-soft); }
/* "Other" and "Not identified" name the LIMIT of what we recognised rather
   than a kind of device. Quieter, and never hidden. */
.conn-chip.muted { color: var(--text-tertiary); }
.conn-chip-n { font-size: var(--fs-4); color: var(--text-primary); font-weight: var(--fw-semibold); }
.conn-chip.muted .conn-chip-n { color: var(--text-secondary); }

/* ── THE CATEGORY SECTIONS ────────────────────────────────────────────────
   Proper disclosure widgets (<details>), so keyboard, screen-reader and
   no-JS behaviour is the platform's rather than ours. */
.conn-groups { display: flex; flex-direction: column; gap: var(--sp-3); }
.conn-group {
  border: 1px solid var(--border-default);
  border-radius: var(--r-md);
  background: var(--surface-card);
  overflow: hidden;
}
.conn-group-head {
  display: flex;
  align-items: baseline;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--panel-pad);
  cursor: pointer;
  list-style: none;
  background: var(--surface-raised);
}
.conn-group-head::-webkit-details-marker { display: none; }
.conn-group-head::before {
  content: "▸";
  color: var(--text-disabled);
  transition: transform var(--motion-fast) var(--ease-out);
}
.conn-group[open] > .conn-group-head::before { transform: rotate(90deg); }
.conn-group-head:hover .conn-group-label { color: var(--text-primary); }
/* The label is .section-title, the shared component — nothing here
   re-declares a family or a size. */
.conn-group-label { flex: 0 0 auto; }
.conn-group-count {
  font-size: var(--fs-4);
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}
/* What a closed section says it holds. Truncates with an ellipsis rather than
   wrapping the head onto two lines — the count above it is the exact fact. */
.conn-group-preview {
  flex: 1 1 auto;
  min-width: 0;
  font-size: var(--fs-2);
  color: var(--text-tertiary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.conn-group[open] > .conn-group-head .conn-group-preview { visibility: hidden; }

/* ── THE ROW ──────────────────────────────────────────────────────────────
   Inherits .data-table, minus the whole-row pointer: every destination in
   this table is a real link now (the port name, the neighbour, the evidence
   disclosure), so there is no JS row-click handler and no cursor promising
   one. */
.conn-table { border: none; background: transparent; }
.conn-table tbody tr { cursor: default; }
.conn-table th, .conn-table td { padding: var(--sp-2) var(--panel-pad); }
.conn-portclass { margin-left: var(--sp-2); }
.conn-nbr .chip { margin-left: var(--sp-2); }
/* The evidence column is as narrow as its own word at rest, and takes the room
   it needs when opened — otherwise the disclosure body would wrap inside a
   1%-wide cell, which is a paragraph one word per line. */
.conn-ev-cell { width: 1%; white-space: nowrap; }
.conn-ev-cell:has(details[open]) { width: auto; white-space: normal; }
/* The per-row evidence sits INSIDE the row, so it must not carry the panel
   chrome the standalone component has. */
.conn-evidence { margin: 0; border: none; background: transparent; }
.conn-evidence > summary { padding: 0; }
.conn-evidence[open] { position: relative; }
.conn-ev-list {
  grid-template-columns: minmax(11ch, 20ch) 1fr;
  padding: var(--sp-2) 0 0;
  white-space: normal;
  max-width: var(--prose-measure);
}

/* ── THE ONE ANIMATION, AND IT IS A MEASUREMENT (§13) ─────────────────────
   A row marked `data-new` is a relationship that did NOT exist the last time
   this switch was polled: `first_seen` inside the freshness budget. It eases
   in once, and there is no animation at all when nothing is new — the motion
   is the state change, not decoration, and it cannot run on a quiet page.
   The FACT is also a chip ("new"), because §11 applies to motion exactly as it
   applies to colour: nobody may have to notice a movement to learn something.
   Reduced motion drops the animation and keeps the chip. */
@keyframes conn-arrive {
  from { opacity: 0; transform: translateY(-2px); background: var(--accent-bg); }
  to   { opacity: 1; transform: none; background: transparent; }
}
.conn-row[data-new="1"] {
  animation: conn-arrive var(--motion-slow) var(--ease-out) 1;
}
.conn-new-chip { letter-spacing: 0.06em; }

/* ═══════════════════════════════════════════════════════════════════════════
   FIREWALL DEVICE PAGE (M3.2a) — the switch device page's components with a
   firewall's sections. Almost nothing new is needed, which is the point: the
   header, vitals strip, attention queue, faceplate, inspector, evidence
   disclosure and module tables are all the shared ones. What IS here is a
   numeric-column rule for hand-written tables, the operator's monitored
   TOGGLE, the zone group's frame, and the inline sparklines beside the load
   numbers. Tokens only.
   ═══════════════════════════════════════════════════════════════════════════ */

/* Numeric columns in a hand-written module table. The macro-built tables get
   this from .data-table; a table written out in a template (because its cells
   carry controls, not text) needs it too, or a column of session counts stops
   lining up at the decimal. */
.module-table th.num,
.module-table td.num { text-align: right; font-variant-numeric: tabular-nums; }

/* The monitored toggle. A SWITCH, not the word "mark": an operator changing
   what pages them at 2am should be operating a control whose current state is
   readable without reading a verb. role="switch" + aria-checked carry the same
   state to a screen reader, and the label is always rendered — the track alone
   would be colour as the only channel (DESIGN_SYSTEM §11). */
.fw-toggle {
  display: inline-flex; align-items: center; gap: 8px;
  background: transparent; border: 0; padding: 0; cursor: pointer;
  color: var(--fg-2); font-size: var(--fs-2); letter-spacing: 0.06em;
}
.fw-toggle-track {
  position: relative; display: inline-block; width: 28px; height: 14px;
  background: var(--ink-4); border: 1px solid var(--line-2); border-radius: 999px;
  transition: background var(--motion-fast) var(--ease-out),
              border-color var(--motion-fast) var(--ease-out);
}
.fw-toggle-knob {
  position: absolute; top: 1px; left: 1px; width: 10px; height: 10px;
  background: var(--fg-2); border-radius: 999px;
  transition: transform var(--motion-fast) var(--ease-out),
              background var(--motion-fast) var(--ease-out);
}
.fw-toggle.on { color: var(--accent-soft); }
.fw-toggle.on .fw-toggle-track { background: var(--accent-bg); border-color: var(--accent-edge); }
.fw-toggle.on .fw-toggle-knob { background: var(--accent-soft); transform: translateX(14px); }
.fw-toggle:hover .fw-toggle-track { border-color: var(--accent-edge); }
.fw-toggle-label { white-space: nowrap; }

/* One zone group. A frame rather than a heading, because the interfaces
   inside it are the content and the zone is their container. */
.fw-zone { margin: 18px 0 6px; padding: 0 0 0 10px; border-left: 2px solid var(--line-2); }
.fw-zone .section-title { display: flex; align-items: center; gap: 8px; margin: 0 0 6px; }
/* The unzoned bucket is a real finding on a firewall, so it is marked — but
   quietly: it is a policy question, not a fault. */
.fw-zone-unzoned .section-title { color: var(--status-warning); }

/* Sparklines beside a load number, not instead of it. The number is the
   answer; the line is the shape of the last ninety minutes. */
.fw-spark { display: inline-flex; align-items: center; gap: 8px; margin-left: 10px; }
.fw-perf .if-sparkline { width: 120px; height: 20px; }
.fw-perf dd { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }

/* A firewall's squares may carry a NAME rather than a number — "dmz", "mgmt",
   a singleton with no number to show — and a name does not fit in 32px. Scoped
   to the firewall plate so the switch's fixed grid is untouched: there, every
   square holds one or two digits by construction. */
.fw-plate .hp-port { width: auto; min-width: 32px; padding: 0 5px; }

/* An absence line that is reporting something WRONG rather than something
   missing. Same component, one tint (no edge — report 00059). */
.absence.critical-note { background: var(--critical-tint); }

/* ── THE WAN LANES ───────────────────────────────────────────────────────────
   One lane per circuit, and the lane is the row: identity, state, role, then
   the measurements. A table was rejected because the thing an operator wants —
   "which one is carrying the traffic" — is a RANK, and a rank reads badly as a
   cell among five other cells. Grid rather than flex so every lane's columns
   line up whatever its name is, and so the whole thing stacks on a narrow
   viewport without a media query per column. */
.wan-lanes { display: flex; flex-direction: column; gap: var(--sp-2); }
.wan-lane {
  display: grid;
  grid-template-columns: minmax(9rem, 1.2fr) auto minmax(0, 2.4fr);
  align-items: center;
  gap: var(--sp-2) var(--sp-3);
  padding: var(--sp-2) var(--row-x);
  border: 1px solid var(--border-subtle);
  border-radius: var(--r-sm);
  background: var(--surface-card);
}
/* The ROLE is the chip beside the name — the word. The coloured left edge that
   used to repeat it is gone (DESIGN.md §7, report 00059). */
.wan-lane-id { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.wan-lane-name {
  font-size: var(--fs-4);
  color: var(--text-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.wan-lane-if { font-size: var(--fs-2); color: var(--text-tertiary); }
.wan-lane-chips { display: flex; align-items: center; gap: var(--sp-1); }
.wan-lane-metrics {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-1) var(--sp-3);
  margin: 0;
  justify-content: flex-end;
}
.wan-metric { display: flex; flex-direction: column; gap: 1px; min-width: 4.5rem; }
.wan-metric dt {
  font-size: var(--fs-1);
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--text-disabled);
}
.wan-metric dd { margin: 0; font-size: var(--fs-3); color: var(--text-secondary); }
/* The disclosure spans the whole lane on its own row: it is the argument, and
   the argument sits under the answer rather than beside it. */
.wan-lane-why { grid-column: 1 / -1; margin: 0; }
.wan-why-list { margin: 0 0 var(--sp-2); padding-left: var(--sp-3); }
.wan-why-list li { margin: 2px 0; }
.wan-failover { font-size: var(--fs-4); color: var(--text-primary); }

@media (max-width: 860px) {
  .wan-lane { grid-template-columns: 1fr; }
  .wan-lane-metrics { justify-content: flex-start; }
}

/* THE SITE-WIDE INTERNET READING sits BELOW the lanes and looks different from
   them on purpose: it is one reading for the whole site rather than one per
   circuit, and rendering it as a fourth lane would be the visual form of the
   claim this module refuses to make. */
.wan-site {
  display: grid;
  grid-template-columns: minmax(9rem, 1fr) minmax(0, 2.4fr);
  align-items: center;
  gap: var(--sp-2) var(--sp-3);
  padding: var(--sp-2) var(--row-x);
  margin-top: var(--sp-2);
  border: 1px dashed var(--border-subtle);
  border-radius: var(--r-sm);
  background: var(--surface-sunken);
}
.wan-site-label {
  font-size: var(--fs-2);
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--text-tertiary);
}
.wan-site .evidence { grid-column: 1 / -1; margin: 0; }
.wan-unranked { font-size: var(--fs-3); }
@media (max-width: 860px) {
  .wan-site { grid-template-columns: 1fr; }
}

/* ── INTERFACE GROUPS ────────────────────────────────────────────────────────
   A tab is a subject, not a table (DESIGN_SYSTEM §18). Fifty-three interfaces
   of five kinds in one list is a dump; five named groups with counts, physical
   open and the rest closed, is a reading. The rows stay in the document when a
   group is closed, so in-page search and a screen reader still reach them —
   which `display: none` would not. */
.fw-if-group { margin: 0 0 var(--sp-2); }
.fw-if-group-head {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-1) var(--row-x);
  cursor: pointer;
  list-style: none;
  border-bottom: 1px solid var(--border-subtle);
}
.fw-if-group-head::-webkit-details-marker { display: none; }
.fw-if-group-head::before {
  content: "▸";
  color: var(--text-disabled);
  font-size: var(--fs-2);
}
.fw-if-group[open] > .fw-if-group-head::before { content: "▾"; }
.fw-if-group-label {
  font-size: var(--fs-3);
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--text-secondary);
}
.fw-if-group-count { font-size: var(--fs-2); color: var(--text-tertiary); }
.fw-if-hidden {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-1) var(--row-x);
  font-size: var(--fs-2);
  color: var(--text-tertiary);
}
.fw-sslvpn-stats { margin-bottom: var(--sp-2); }

/* ── THE LIFECYCLE BAND ──────────────────────────────────────────────────────
   Part of the module-page standard, so it is styled here and not in a module's
   own stylesheet — there is no module-specific landing CSS and there must not
   be one. */
.module-lifecycle { display: flex; flex-direction: column; gap: var(--sp-2); }
.module-lifecycle .evidence { margin: 0; }

/* SORTABLE COLUMN HEADERS. The affordance appears only once the shared
   controller has stamped the header, so a page with no JavaScript shows a
   plain heading rather than a control that does nothing. */
.module-table th.sortable { cursor: pointer; user-select: none; }
.module-table th.sortable::after {
  content: "";
  display: inline-block;
  width: 0.7em;
  color: var(--text-disabled);
}
.module-table th.sortable[aria-sort="ascending"]::after { content: "▲"; }
.module-table th.sortable[aria-sort="descending"]::after { content: "▼"; }
.module-table th.sortable:hover { color: var(--text-primary); }

/* ── A STAT STRIP ────────────────────────────────────────────────────────────
   Label, value, and at most a three-word qualifier. The predecessor put whole
   sentences in a definition-list cell, which wrapped into ribbons a dozen
   characters wide — the "data all over the place" the live review reported.
   Nothing longer than a qualifier may live here; the argument is one
   `<details class="evidence">` below. */
.fw-stats {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3) var(--sp-5);
  padding: var(--sp-2) var(--row-x);
}
.fw-stat { display: flex; flex-direction: column; gap: 2px; min-width: 7rem; }
.fw-stat-label {
  font-size: var(--fs-1);
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--text-tertiary);
}
.fw-stat-value { font-size: var(--fs-6); color: var(--text-primary); line-height: var(--lh-tight); }
.fw-stat-unit { font-size: var(--fs-2); color: var(--text-tertiary); }
.fw-stat-caption { font-size: var(--fs-2); color: var(--text-tertiary); }
.fw-stat .fw-spark { margin-left: 0; }

/* Short factual lists — alarmed sensors, tunnels that are down. Chips and a
   name, never a sentence. */
.fw-inline-list { list-style: none; margin: 0; padding: var(--sp-1) var(--row-x); }
.fw-inline-list li { display: flex; align-items: center; gap: var(--sp-2); padding: 2px 0; }

/* The interface table's filters. Same vocabulary as the connections page's. */
.fw-filters {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--sp-2) var(--sp-3);
  padding: var(--sp-2) var(--row-x);
}
.fw-filter { display: flex; flex-direction: column; gap: 2px; }
.fw-filter-label {
  font-size: var(--fs-1);
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--text-tertiary);
}
/* The interface name is a control, not a link: it opens the inspector in place.
   Styled as the link it reads as, so nothing wears an affordance it cannot
   honour and nothing loses one it can. */
.fw-if-open { background: none; border: 0; padding: 0; cursor: pointer; font: inherit; }

/* ── THE ONBOARDING CANVAS (PLAN 3.3) ──────────────────────────────────────
   A vertical list rather than the dashboard's three-across grid, because it is
   five ORDERED steps with a dependency between them and a row of equal cards
   says the opposite. State is carried by the shared state marks in the head —
   colour is never the only channel here either. */

.setup-progress {
  font-size: var(--fs-4);
  color: var(--fg-2);
  margin: 0 0 20px;
}

.setup-canvas {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.setup-step {
  background: var(--ink-1);
  border: 1px solid var(--line);
  padding: 18px 20px;
  display: flex;
  gap: 16px;
  align-items: flex-start;
}

/* A LOCKED STEP IS DIMMED AND STILL FULLY READABLE. It is not hidden and its
   text is not removed: the reason it is locked is the most useful thing on it. */
.setup-step[data-state="locked"] { opacity: 0.72; }

.setup-step-num {
  font-family: var(--font-mono);
  font-size: var(--fs-3);
  font-weight: 600;
  color: var(--accent-soft);
  letter-spacing: 0.1em;
  padding-top: 2px;
}

.setup-step[data-state="locked"] .setup-step-num { color: var(--fg-3); }

.setup-step-body {
  display: flex;
  flex-direction: column;
  gap: 6px;
  flex: 1;
  min-width: 0;
}

.setup-step-head {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}

.setup-step-title { font-size: var(--fs-5); color: var(--fg-0); }

.setup-step-what { color: var(--fg-2); font-size: var(--fs-3); line-height: 1.5; margin: 0; }

.setup-step-evidence {
  font-family: var(--font-mono);
  font-size: var(--fs-2);
  color: var(--fg-1);
  margin: 0;
}

.setup-step-blocked { color: var(--fg-2); font-size: var(--fs-3); margin: 0; }

.setup-step-note { color: var(--fg-2); font-size: var(--fs-3); line-height: 1.5; margin: 0; }

.setup-step-link {
  margin-top: 4px;
  font-family: var(--font-mono);
  font-size: var(--fs-2);
  letter-spacing: 0.08em;
  color: var(--brand-soft);
  text-transform: uppercase;
}

.setup-step-link:hover { color: var(--accent-soft); }

/* The inline credential blocks on the add-device form (PLAN 3.6). */
.nc-block { margin-top: 10px; }

/* ═══════════════════════════════════════════════════════════════════════════
   THE DESIGN LANGUAGE (report 00054 · docs/DESIGN.md)

   The base component classes, and the refresh applied to the pre-00054
   primitives so every page inherits the look at once. It is LAST in this file
   on purpose: for a class it restyles, it wins the cascade over the older
   rules above without an !important anywhere.

   Built only from tokens.css. Colour roles (§2):
     --accent   blue    the ONLY action colour
     --accent-2 purple  hints: links, eyebrows, secondary series, context
     status             only for status, always with the word
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── GROUND, TYPE ─────────────────────────────────────────────────────────── */
html, body {
  background: var(--ground);
  color: var(--body);
  font-family: var(--font);
  font-size: var(--fs-4);
}
/* No grain, no gradient behind the app (docs/DESIGN.md §7). */
body { background-image: none; }

/* ── LINKS — hints are purple, never an action ──────────────────────────── */
a:not([class]), .link, .btn-link {
  color: var(--accent-2);
}
a:not([class]):hover, .link:hover, .btn-link:hover { color: var(--accent-2-hover); }

/* ── EYEBROW ─────────────────────────────────────────────────────────────── */
.eyebrow {
  font-size: var(--fs-1);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--accent-2);
}
.eyebrow.eyebrow-muted { color: var(--muted); }

/* ── TYPE SCALE, APPLIED ────────────────────────────────────────────────── */
.page-title {
  font-size: var(--fs-8);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--tracking-title);
  line-height: var(--lh-tight);
  color: var(--text);
}
.section-title {
  font-size: var(--fs-5);
  font-weight: var(--fw-semibold);
  letter-spacing: 0;
  text-transform: none;
  color: var(--text);
}
.caption, .muted-caption { font-size: var(--fs-2); color: var(--faint); }

/* ── CARD — elevation, not a border ─────────────────────────────────────── */
.card,
.panel,
.form-card,
.kpi-card,
.stat-card,
.module-card,
.token-card,
.denied-panel,
.tenant-context {
  background: var(--surface);
  color: var(--body);
  border: 0;
  border-radius: var(--r-card);
  box-shadow: var(--shadow-card);
}
.card { padding: var(--sp-4); }
/* Cards never carry a left-border stripe (docs/DESIGN.md §7). */
.module-card.active,
.token-card,
.denied-panel,
.tenant-context { border-left: 0; }
.token-card::before { content: none; }

/* ── BUTTONS ─────────────────────────────────────────────────────────────── */
.btn,
.btn-primary,
.btn-quiet,
.btn-secondary,
.btn-danger {
  min-height: var(--control-h);
  padding: 0 var(--sp-4);
  border-radius: var(--r-control);
  font-family: var(--font);
  font-size: var(--fs-4);
  font-weight: var(--fw-semibold);
  letter-spacing: 0;
  text-transform: none;
  line-height: 1;
  cursor: pointer;
  border: 1px solid transparent;
  transition: background var(--motion-fast) var(--ease-out),
              border-color var(--motion-fast) var(--ease-out),
              color var(--motion-fast) var(--ease-out);
}
.btn { display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2); }
.btn-primary {
  background: var(--accent);
  color: var(--on-accent);
  box-shadow: none;
}
.btn-primary:hover { background: var(--accent-hover); color: var(--on-accent); }
.btn-quiet,
.btn-secondary {
  background: transparent;
  color: var(--body);
  border-color: var(--hairline);
}
.btn-quiet:hover,
.btn-secondary:hover { background: var(--raised); color: var(--text); border-color: var(--hairline-strong); }
.btn-danger {
  background: transparent;
  color: var(--critical-text);
  border-color: var(--critical-edge);
}
.btn-danger:hover { background: var(--critical-tint); }
.btn:disabled,
.btn-primary:disabled,
.btn-quiet:disabled,
.btn-secondary:disabled,
.btn-danger:disabled { opacity: 0.5; cursor: not-allowed; }

/* ── FORMS — label above, help below, error under the field ─────────────── */
.label,
.field > label,
.field label {
  display: block;
  font-size: var(--fs-3);
  font-weight: var(--fw-medium);
  letter-spacing: 0;
  text-transform: none;
  color: var(--label);
  margin-bottom: var(--sp-1);
}
.input,
.field input:not([type="checkbox"]):not([type="radio"]),
.field select,
.field textarea {
  min-height: var(--control-h);
  box-sizing: border-box;
  background: var(--ground);
  color: var(--text);
  border: 1px solid var(--hairline);
  border-radius: var(--r-control);
  padding: 0 var(--sp-3);
  font-family: var(--font);
  font-size: var(--fs-4);
}
.field textarea, textarea.input { padding: var(--sp-2) var(--sp-3); }
.input:focus,
.field input:focus,
.field select:focus,
.field textarea:focus {
  border-color: var(--accent);
  box-shadow: var(--focus);
}
.help, .field-hint { display: block; font-size: var(--fs-2); color: var(--faint); margin-top: var(--sp-1); }
.error, .field-error {
  display: block;
  font-size: var(--fs-3);
  color: var(--critical-text);
  margin-top: var(--sp-1);
}
.input.is-invalid,
.field.has-error input,
.field.has-error select,
.field.has-error textarea { border-color: var(--critical); }
input[type="checkbox"], input[type="radio"] { accent-color: var(--accent); }

/* ── CHIPS — dot + word, the word is mandatory ──────────────────────────── */
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  padding: 2px var(--sp-2);
  border: 0;
  border-radius: var(--r-chip);
  background: var(--raised);
  color: var(--body);
  font-size: var(--fs-2);
  font-weight: var(--fw-medium);
  letter-spacing: 0.02em;
}
.chip-nominal::before, .chip.ok::before,
.chip-warning::before, .chip.amber::before, .chip.warn::before,
.chip-critical::before, .chip.critical::before, .chip.error::before,
.chip-unknown::before, .chip-paused::before {
  content: "";
  flex: none;
  width: 6px;
  height: 6px;
  border-radius: var(--r-full);
  background: var(--unknown);
}
.chip-nominal::before, .chip.ok::before        { background: var(--nominal); }
.chip-warning::before, .chip.amber::before,
.chip.warn::before                             { background: var(--warning); }
.chip-critical::before, .chip.critical::before,
.chip.error::before                            { background: var(--critical); }
.chip-unknown::before, .chip-paused::before    { background: var(--unknown); }
.chip.ok, .chip.amber, .chip.warn, .chip.critical, .chip.error,
.chip-nominal, .chip-warning, .chip-critical { color: var(--body); background: var(--raised); }
/* Unknown and paused are GREY — never green (the no-false-green rule). */
.chip-unknown, .chip-paused, .chip.muted { color: var(--state-unknown-text); background: var(--raised); }
.chip.brand { color: var(--accent-2); background: var(--accent-2-tint); }

/* ── BANNER — one line, tinted, one action right ─────────────────────────
   Modifiers of the existing .banner (which is also the page-header grid), so
   `class="banner banner-warning"` fully replaces that layout. */
.banner-warning,
.banner-critical,
.banner-info {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  padding: var(--sp-3) var(--sp-4);
  border: 0;
  border-radius: var(--r-control);
  color: var(--body);
  font-size: var(--fs-4);
}
.banner-warning  { background: var(--warning-tint); }
.banner-critical { background: var(--critical-tint); }
.banner-info     { background: var(--accent-2-tint); }
.banner-warning > :last-child:not(:first-child),
.banner-critical > :last-child:not(:first-child),
.banner-info > :last-child:not(:first-child) { flex: none; }

/* ── TABLE — no vertical rules, hairline rows, 44px, tabular numbers ────── */
.table,
.data-table,
.module-table {
  background: var(--surface);
  border: 0;
  border-radius: var(--r-card);
  box-shadow: var(--shadow-card);
}
.table table, table.table, .data-table table, .module-table table {
  width: 100%;
  border-collapse: collapse;
}
.table th, .data-table th, .module-table th {
  height: var(--row-h);
  font-size: var(--fs-2);
  font-weight: var(--fw-semibold);
  letter-spacing: 0.04em;
  color: var(--muted);
  text-align: left;
  border-left: 0;
  border-right: 0;
  border-bottom: 1px solid var(--raised);
  background: transparent;
}
.table td, .data-table td, .module-table td {
  height: var(--row-h);
  font-size: var(--fs-3);
  color: var(--body);
  border-left: 0;
  border-right: 0;
  border-bottom: 1px solid var(--raised);
  font-variant-numeric: tabular-nums;
}
.table tbody tr:hover, .data-table tbody tr:hover, .module-table tbody tr:hover {
  background: var(--raised);
}
.num, td.num { font-variant-numeric: tabular-nums; text-align: right; }

/* ── DIALOG + SCRIM ──────────────────────────────────────────────────────── */
.scrim {
  position: fixed;
  inset: 0;
  background: var(--scrim);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 300;
}
.dialog,
.bulk-modal-card {
  width: min(var(--dialog-w), calc(100vw - 2 * var(--sp-4)));
  background: var(--surface);
  color: var(--body);
  border: 0;
  border-radius: var(--r-card);
  box-shadow: var(--shadow-dialog);
  padding: var(--sp-5);
}

/* ── FOCUS — one ring, the brief's ───────────────────────────────────────── */
:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

/* ── THE SHELL ───────────────────────────────────────────────────────────── */
.app-sidebar {
  background: var(--deep);
  border-right: 0;
}
.nav-section-label {
  font-size: var(--fs-1);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--muted);
}
.nav-item {
  border-left: 0;
  border-radius: var(--r-control);
  color: var(--label);
  font-size: var(--fs-4);
  letter-spacing: 0;
}
.nav-item:hover { color: var(--text); background: var(--surface); }
.nav-item.active,
.nav-item.nav-primary.active {
  color: var(--accent);
  background: var(--accent-tint);
  border-left: 0;
}
.nav-icon svg { width: var(--icon); height: var(--icon); }
.nav-badge {
  background: var(--accent);
  color: var(--on-accent);
  border-radius: var(--r-full);
}
.app-main {
  max-width: calc(var(--content-max) + 2 * var(--gutter));
  padding: var(--gutter);
}
.user-role { color: var(--accent-2); border: 0; background: var(--accent-2-tint); border-radius: var(--r-chip); }

/* The mark and the wordmark. */
.brand-lockup { display: inline-flex; align-items: center; gap: var(--sp-2); }
.brand-wordmark {
  font-size: var(--fs-6);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--tracking-title);
  color: var(--text);
}
.hh-mark { flex: none; display: block; }
.hh-mark-outer { fill: none; stroke: var(--accent); stroke-width: 2; stroke-linejoin: round; }
.hh-mark-inner { fill: var(--accent-2); fill-opacity: 0.95; }

/* The footer — small, --faint, on every page. */
.app-footer {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-4) var(--gutter);
  font-size: var(--fs-2);
  color: var(--faint);
}
.app-shell ~ .app-footer { margin-left: var(--sidebar-width); }
.app-footer .mono { font-size: var(--fs-2); }
.app-footer .hellohive-site-link { color: var(--faint); }
.app-footer .hellohive-site-link:hover { color: var(--accent-2); }
.app-footer-conn { display: inline-flex; align-items: center; gap: var(--sp-1); }
.conn-dot { width: 6px; height: 6px; border-radius: var(--r-full); background: var(--nominal); }
.app-footer[data-conn="lost"] .conn-dot { background: var(--critical); }
.app-footer[data-conn="lost"] .conn-word { color: var(--critical-text); }
.app-footer-product { color: var(--text-secondary); }

/* ── NO GRADIENTS (docs/DESIGN.md §7) ───────────────────────────────────────
   The auth pages keep exactly two radial glows (purple bottom-left, blue
   top-right) and are the only place a gradient is written; that rule lives
   in the AUTH section below. Everything that used a decorative gradient
   before 00054 is flattened here. */
.auth-side { background: var(--deep); }
.grid-overlay { display: none; }
.topo-map { background: var(--surface); border: 0; box-shadow: var(--shadow-card); }

/* ── AUTH PAGES — dark, never cream ──────────────────────────────────────── */
.auth-shell {
  background: var(--ground);
  color: var(--body);
}
.auth-side {
  /* THE ONE GRADIENT IN THE PRODUCT: two radial glows on the auth panel. */
  background:
    radial-gradient(closest-side at 12% 88%, var(--accent-2-glow), transparent),
    radial-gradient(closest-side at 88% 12%, var(--accent-glow), transparent),
    var(--deep);
  border-right: 0;
}
.auth-card { background: var(--surface); color: var(--body); box-shadow: var(--shadow-card); border: 0; border-radius: var(--r-card); }
.auth-title { color: var(--text); letter-spacing: var(--tracking-title); }
.auth-eyebrow { color: var(--accent-2); border: 0; background: none; padding: 0; }


/* ─── Platform shell: large titles, grouped inset lists, widgets (00066, DESIGN.md §5a) ─ */
.ios-header{display:flex;align-items:flex-end;justify-content:space-between;gap:var(--sp-4);margin-bottom:var(--sp-5)}
.ios-eyebrow{font-size:var(--fs-3);color:var(--label);margin:0 0 var(--sp-1)}
.ios-title{font-size:var(--fs-9);font-weight:700;letter-spacing:-.02em;color:var(--text);margin:0}
.widget-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:var(--sp-3);margin-bottom:var(--sp-5)}
.widget{background:var(--surface);border-radius:var(--r-group);box-shadow:var(--shadow-card);padding:var(--sp-4)}
.widget-label{font-size:var(--fs-3);color:var(--label);margin:0}
.widget-value{font-size:var(--fs-8);font-weight:600;color:var(--text);margin:var(--sp-1) 0;font-variant-numeric:tabular-nums}
.widget-caption{font-size:var(--fs-2);color:var(--muted);margin:0}
.widget-bar{height:6px;border-radius:var(--r-full);background:var(--raised);margin-top:var(--sp-2);overflow:hidden}
.widget-bar span{display:block;height:100%;width:var(--pct);background:var(--accent)}
.group-section{margin-bottom:var(--sp-5)}
.group-title{font-size:var(--fs-5);font-weight:600;color:var(--text);margin:0 0 var(--sp-2);display:flex;justify-content:space-between;align-items:baseline}
.group-more{font-size:var(--fs-3);font-weight:400}
.group{list-style:none;margin:0;padding:0;background:var(--surface);border-radius:var(--r-group);box-shadow:var(--shadow-card);overflow:hidden}
.group-row+.group-row{border-top:1px solid var(--hairline)}
.group-link{display:flex;align-items:center;gap:var(--sp-3);padding:var(--sp-3) var(--sp-4);color:var(--body);text-decoration:none;min-height:44px}
a.group-link:hover{background:var(--raised)}
.group-text{display:flex;flex-direction:column;flex:1;min-width:0}
.group-primary{color:var(--text);font-size:var(--fs-4)}
.group-secondary{color:var(--muted);font-size:var(--fs-2)}
.group-value{margin-left:auto;color:var(--label);font-variant-numeric:tabular-nums}
.group-action{color:var(--accent-2);font-size:var(--fs-3)}
.group-row.is-muted .group-primary,.group-row.is-muted .group-value{color:var(--faint)}
.group-caption{font-size:var(--fs-2);color:var(--faint);margin:var(--sp-2) var(--sp-4) 0}
.group-columns{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:var(--sp-5)}
.chev{color:var(--faint);font-size:var(--fs-6);line-height:1}
.avatar{width:var(--tile);height:var(--tile);border-radius:var(--r-full);background:var(--accent-2-tint);color:var(--accent-2);display:inline-flex;align-items:center;justify-content:center;font-size:var(--fs-2);font-weight:600;flex:none}
.nav-tile{width:var(--tile);height:var(--tile);border-radius:var(--r-tile);background:var(--raised);display:inline-flex;align-items:center;justify-content:center;flex:none}
.nav-item.active .nav-tile{background:var(--accent-tint)}
.nav-dot{width:8px;height:8px;border-radius:var(--r-full);margin-left:auto;flex:none}
.nav-dot.error,.nav-dot.critical{background:var(--critical)}
.nav-dot.amber{background:var(--warning)}
.nav-dot.muted{background:var(--unknown)}
.nav-search{padding:0 var(--sp-3) var(--sp-3)}
.nav-search input{width:100%;height:32px;border-radius:var(--r-control);border:1px solid var(--hairline);background:var(--surface);color:var(--body);padding:0 var(--sp-3);font-size:var(--fs-3)}
.nav-account{display:flex;align-items:center;gap:var(--sp-2);padding:var(--sp-3);border-top:1px solid var(--hairline);margin-top:var(--sp-3)}
.nav-account-text{display:flex;flex-direction:column;min-width:0;font-size:var(--fs-2);color:var(--label)}
.nav-account-text span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.filter-chips{display:flex;flex-wrap:wrap;gap:var(--sp-2);margin:var(--sp-3) 0 var(--sp-5)}
.filter-chips a{text-decoration:none}
.customers-search .input{max-width:420px}
.msp-node>summary{list-style:none;cursor:pointer}
.msp-node>summary::-webkit-details-marker{display:none}
.msp-orgs{margin:var(--sp-2) 0 0 var(--sp-5)}
.breadcrumb{font-size:var(--fs-3);color:var(--muted);margin-bottom:var(--sp-3)}
.segmented{display:inline-flex;gap:2px;padding:2px;border-radius:var(--r-control);background:var(--raised);margin:0 0 var(--sp-5)}
.segment{padding:var(--sp-1) var(--sp-4);border-radius:var(--r-lg);color:var(--label);text-decoration:none;font-size:var(--fs-3)}
.segment.active{background:var(--surface);color:var(--text);box-shadow:var(--shadow-card)}
/* 00073: a segment may be a <button> (the Customer page's tabs switch panels in place). */
button.segment{border:0;background:transparent;font:inherit;cursor:pointer}
.customer-tabs{flex-wrap:wrap}
.filter-row{display:flex;flex-wrap:wrap;gap:var(--sp-3);align-items:flex-end;margin-bottom:var(--sp-4)}

/* Connection-test steps (00071): one row per thing polling runs, one word each. */
.test-steps{list-style:none;margin:var(--sp-2) 0 0;padding:0;display:grid;gap:var(--sp-1)}
.test-steps li{display:grid;grid-template-columns:10rem auto 1fr;gap:var(--sp-2);align-items:baseline}
.test-step-label{color:var(--fg-2)}

/* ── THE HIVE, SHARED (HEALTH-1, POLISH-3 00076) ─────────────────────────────
   One hex per device, one breathing core whose PACE is its state. Shared by
   the Health page's field and the device page's RIGHT NOW card, so the same
   device looks the same on both. Colour always travels with the status word. */
.group-label {
  display: block;
  margin: 0;
  font-size: var(--fs-1);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--muted);
}
/* The /health cluster is centred in its card by layout, not by a computed
   offset (RUCKUS-1): the field is a flex box, the script sizes the cluster
   box to the honeycomb, and the cells are placed inside that box. */
.hive-field { display: flex; flex-direction: column; align-items: center; justify-content: center; }
.hive-cluster { position: relative; flex: none; margin-block: auto; }
.hive-cell { position: absolute; top: 0; left: 0; padding: 0; border: 0; background: transparent; cursor: pointer;
  color: var(--text); font: inherit; border-radius: var(--r-chip); }
.hive-cell:focus-visible { box-shadow: var(--focus); }
.hive-cell svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.hive-cell .rim { fill: var(--unknown); filter: brightness(.55); }
.hive-cell .base { fill: var(--surface); }
.hive-cell .core { fill: var(--unknown); opacity: .7; transform-box: fill-box; transform-origin: center; }
.hive-cell.lvl-healthy .rim, .hive-cell.lvl-healthy .core { fill: var(--nominal); }
.hive-cell.lvl-watch .rim, .hive-cell.lvl-watch .core { fill: var(--warning); }
.hive-cell.lvl-critical .rim, .hive-cell.lvl-critical .core { fill: var(--critical); }
.hive-cell.is-selected .rim { fill: var(--accent); filter: none; }
.hive-cell.lvl-healthy .core { animation: hive-breathe 4s ease-in-out infinite; }
.hive-cell.lvl-watch .core { animation: hive-breathe 2s ease-in-out infinite; }
.hive-cell.lvl-critical .core { animation: hive-breathe 1s ease-in-out infinite; }
@keyframes hive-breathe {
  0%, 100% { opacity: .35; transform: scale(.96); }
  50% { opacity: .9; transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .hive-cell .core { animation: none !important; opacity: .7; transform: none; }
}
.hive-cell .txt { position: absolute; inset: 20% 12%; display: flex; flex-direction: column; align-items: center;
  justify-content: center; gap: 2px; text-align: center; pointer-events: none; line-height: 1.15; }
.hive-cell .nm { display: none; max-width: 100%; overflow: hidden; font-weight: 600; font-size: var(--fs-2); }
.hive-cell .sc { display: none; font-size: var(--fs-6); font-weight: 700; font-variant-numeric: tabular-nums; }
/* The status word: lowercase, 11 px, a caption under the score. */
.hive-cell .wd { display: none; font-size: var(--fs-1); text-transform: lowercase; letter-spacing: 0; color: var(--hive-word); }
/* ≥ 120 px: the whole name on up to two lines (break at - and _). */
.hive-cell[data-size="xl"] .nm { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  white-space: normal; overflow-wrap: anywhere; font-size: var(--fs-3); }
/* 90–119 px: one line, the middle elided by the script ("FortiGate…mary"). */
.hive-cell[data-size="l"] .nm { display: block; white-space: nowrap; text-overflow: ellipsis; }
.hive-cell[data-size="xl"] .sc, .hive-cell[data-size="xl"] .wd,
.hive-cell[data-size="l"] .sc, .hive-cell[data-size="l"] .wd { display: block; }
.hive-cell[data-size="m"] .sc { display: block; font-size: var(--fs-3); }
.hive-cell .sess { position: absolute; top: 14%; right: 18%; width: 8px; height: 8px; border-radius: 50%; background: var(--text); }
/* A cell that stands alone (the device page): positioned in flow, fixed size. */
.hive-cell.hive-cell-solo { position: relative; display: block; width: 140px; height: 161.66px; cursor: default; flex: none; }

.hive-signals { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.hive-signals li { display: grid; grid-template-columns: 10px minmax(0, 1fr) 64px auto; gap: var(--sp-2); align-items: center;
  padding: var(--sp-2) 0; border-top: 1px solid var(--raised); font-size: var(--fs-3); }
.hive-signals li:first-child { border-top: 0; }
.hive-signals .sd { width: 8px; height: 8px; border-radius: 50%; background: var(--unknown); }
.hive-signals .sd.lvl-ok { background: var(--nominal); }
.hive-signals .sd.lvl-watch { background: var(--warning); }
.hive-signals .sd.lvl-critical { background: var(--critical); }
.hive-signals .sn { color: var(--text); }
.hive-signals .snote { display: block; color: var(--muted); font-size: var(--fs-2); }
.hive-signals svg { width: 64px; height: 20px; }
/* A sparkline is drawn in its signal's OWN level colour — purple is for hint
   text, never for a measurement. */
.hive-signals svg polyline { fill: none; stroke: var(--unknown); stroke-width: 1.5; }
.hive-signals svg.lvl-ok polyline { stroke: var(--nominal); }
.hive-signals svg.lvl-watch polyline { stroke: var(--warning); }
.hive-signals svg.lvl-critical polyline { stroke: var(--critical); }
.hive-signals .sv { color: var(--body); font-variant-numeric: tabular-nums; text-align: right; }

.hive-hist-blocks { display: grid; grid-template-columns: repeat(24, 1fr); gap: 2px; }
.hive-hist-blocks span { height: 12px; border-radius: 2px; background: var(--unknown-fill); }
.hive-hist-blocks .lvl-ok { background: var(--nominal-fill); }
.hive-hist-blocks .lvl-watch { background: var(--warning); }
.hive-hist-blocks .lvl-critical { background: var(--critical); }
.hive-hist-note { color: var(--muted); font-size: var(--fs-2); margin: var(--sp-2) 0 0; }
/* LIVE-1 (report 00079): a condition that cleared is a HINT in the hint
   colour beside the state — never the cell colour. Shared by the hive's detail
   panel, its header and the device page's RIGHT NOW card. */
.live-recovered { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.live-recovered li, .live-recovered-count { color: var(--accent-2); font-size: var(--fs-2); }
.live-cause { color: var(--label); font-size: var(--fs-3); margin: 0; }

/* ── THE DEVICE PAGE (POLISH-3, report 00076) ─────────────────────────────────
   Header, RIGHT NOW, then two columns of grouped cards. Elevation over
   borders; every card opens with an 11 px uppercase .group-label. */
.dv-head { display: flex; justify-content: space-between; align-items: flex-end; gap: var(--sp-4); flex-wrap: wrap; }
.dv-head-main { display: flex; flex-direction: column; gap: var(--sp-1); min-width: 0; }
.dv-title { margin: 0; font-size: var(--fs-9); word-break: break-word; }
.dv-meta { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-1) var(--sp-3); margin: 0;
  color: var(--label); font-size: var(--fs-3); }
.dv-meta > span + span::before { content: "·"; margin-right: var(--sp-3); color: var(--faint); }
.dv-meta > #poll-status-chip::before { content: none; }
.dv-actions { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; }
.dv-actions .inline-form { margin: 0; }
.dv-btn { min-height: 32px; padding: 0 var(--sp-3); font-size: var(--fs-3); background: var(--raised);
  box-shadow: var(--shadow-card); display: inline-flex; align-items: center; text-decoration: none; }
.dv-test { display: block; }
.dv-test[hidden] { display: none; }

.dv-note { font-size: var(--fs-2); margin: 0; }
.dv-card { display: flex; flex-direction: column; gap: var(--sp-3); }
.dv-card-head { display: flex; justify-content: space-between; align-items: center; gap: var(--sp-3); }
.dv-card .error-text { margin: 0; }
.dv-facts { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: var(--sp-2) var(--sp-4); margin: 0;
  font-size: var(--fs-3); }
.dv-facts dt { color: var(--muted); }
.dv-facts dd { margin: 0; color: var(--body); overflow-wrap: anywhere; }
.dv-disclose > summary { cursor: pointer; color: var(--accent-2); font-size: var(--fs-3); list-style: none; }
.dv-disclose > summary::-webkit-details-marker { display: none; }
.dv-disclose > summary::before { content: "▸"; display: inline-block; margin-right: var(--sp-1); color: var(--faint); }
.dv-disclose[open] > summary::before { content: "▾"; }
.dv-disclose-body { display: flex; flex-direction: column; gap: var(--sp-2); padding-top: var(--sp-2); }
.dv-add > summary .dv-add-link { color: var(--accent-2); }
.dv-add-form { display: flex; flex-direction: column; gap: var(--sp-3); padding-top: var(--sp-3); }
.dv-add-pair { display: grid; grid-template-columns: max-content 96px max-content 96px; gap: var(--sp-2); align-items: center; }
.dv-add-pair label { margin: 0; }
.dv-add-form > button { align-self: flex-start; }
.dv-family-line { margin: 0; color: var(--text); font-size: var(--fs-4); }
.dv-oid { margin: 0; color: var(--muted); font-family: var(--font-mono); font-size: var(--fs-2); overflow-wrap: anywhere; }
.dv-chips { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.dv-access-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3); }
.dv-scope { display: flex; flex-direction: column; gap: var(--sp-2); padding-top: var(--sp-3); border-top: 1px solid var(--raised); }
.dv-scope form { margin: 0; }
.dv-danger-link { color: var(--critical-text); background: transparent; border: 0; padding: 0; cursor: pointer; font: inherit; font-size: var(--fs-3); }
.dv-danger-link:hover { color: var(--critical-text); text-decoration: underline; }

.dv-columns { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-4); align-items: start; }
@media (max-width: 1100px) { .dv-columns { grid-template-columns: 1fr; } }
.dv-col { display: flex; flex-direction: column; gap: var(--sp-4); min-width: 0; }

.dv-now { display: flex; flex-direction: column; gap: var(--sp-3); }
.dv-now-grid { display: grid; grid-template-columns: max-content minmax(0, 1.3fr) minmax(0, 1fr); gap: var(--sp-5); align-items: start; }
@media (max-width: 1100px) { .dv-now-grid { grid-template-columns: 1fr; } }
.dv-now-cell { display: flex; align-items: center; gap: var(--sp-4); }
.dv-now-verdict { display: flex; flex-direction: column; align-items: flex-start; gap: var(--sp-2); max-width: 220px; }
.dv-now-score { font-size: var(--fs-9); font-weight: 700; color: var(--text); font-variant-numeric: tabular-nums; line-height: 1; }
.dv-now-unscored { margin: 0; color: var(--body); font-size: var(--fs-4); }
.dv-now-side { display: flex; flex-direction: column; gap: var(--sp-2); }
.dv-now-changed { margin-top: var(--sp-2); }
.dv-now-story { margin: 0; color: var(--body); font-size: var(--fs-3); }

/* ── THE SKELETON'S TYPE SLOT (POLISH-4, report 00077) ──────────────────────
   A server, GPU node, PDU or fabric switch puts its own dashboard in the right
   column under ACCESS; that column takes three fifths of the width so the
   dashboard has room, and scrolls sideways rather than overflow the page. */
.dv-columns.dv-typed { grid-template-columns: minmax(0, 2fr) minmax(0, 3fr); }
@media (max-width: 1100px) { .dv-columns.dv-typed { grid-template-columns: 1fr; } }
.dv-slot { min-width: 0; overflow-x: auto; }

/* ── THE SIDEBAR SHELL (SHELL-1) ─────────────────────────────────────────────
   Org card + session card at the top, collapsible sections, one user block
   with a menu at the bottom — xPassword's structure in xClarus's tokens.
   Section headers are HINTS (--accent-2, DESIGN.md: eyebrows and context),
   never a button colour; the only action colour stays --accent. */
.shell-org { margin: var(--sp-2) var(--sp-3) 0; }
.shell-org-card {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-1) var(--sp-2);
  background: var(--surface-raised);
  border: 1px solid var(--border-subtle);
  border-radius: var(--r-md);
  list-style: none;
}
.shell-org-switch > summary { cursor: pointer; }
.shell-org-switch > summary::-webkit-details-marker { display: none; }
.shell-org-switch > summary:hover { border-color: var(--border-default); }
.shell-org-switch > summary:focus-visible { outline: var(--focus-ring); outline-offset: var(--focus-offset); }
.shell-org-switch[open] .shell-chev { transform: rotate(180deg); }
.shell-avatar {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px;
  border-radius: var(--r-sm);
  background: var(--accent-bg);
  color: var(--accent);
  font-weight: var(--fw-semibold);
  font-size: var(--fs-4);
}
.shell-avatar-org { background: var(--surface-overlay); color: var(--text-primary); }
.shell-org-text { display: flex; flex-direction: column; min-width: 0; gap: 2px; flex: 1 1 auto; }
.shell-org-name {
  color: var(--text-primary); font-weight: var(--fw-semibold); font-size: var(--fs-4);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* SHELL-2: nothing in the shell wraps — one line each, ellipsis, the full
   value in title=. */
.shell-org-line { color: var(--text-tertiary); font-size: var(--fs-2); display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* SHELL-3: the role chip sits on its OWN line under the email, full width —
   never truncated, never wrapped (it was squeezed to 45% beside the email). */
.shell-role-chip {
  font-size: var(--fs-1); letter-spacing: var(--tracking-eyebrow);
  display: block; width: 100%; box-sizing: border-box; text-align: center;
  white-space: nowrap;
}
.shell-chev { flex: 0 0 auto; color: var(--text-tertiary); transition: transform var(--motion-fast) var(--ease-out); }
.shell-org-pop {
  margin-top: var(--sp-2);
  padding: var(--sp-3);
  background: var(--surface-overlay);
  border: 1px solid var(--border-default);
  border-radius: var(--r-md);
  max-height: 320px; overflow-y: auto;
}
.shell-org-pop ul { list-style: none; margin: var(--sp-2) 0 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.shell-org-pop input { width: 100%; }
.shell-org-pop button {
  width: 100%; text-align: left; background: none; border: 0; cursor: pointer;
  padding: var(--sp-1) var(--sp-2); border-radius: var(--r-sm);
  color: var(--text-secondary); font-size: var(--fs-3);
}
.shell-org-pop button:hover { background: var(--surface-raised); color: var(--text-primary); }
.shell-org-pop form { margin: 0; }

.shell-session {
  margin: var(--sp-1) var(--sp-3) 0;
  padding: var(--sp-1) var(--sp-2);
  font-size: var(--fs-2);
  color: var(--text-tertiary);
}
.shell-session p { margin: 0; }
/* SHELL-2: the session card is one line, no box — it was three. */
.shell-session-line {
  display: flex; align-items: center; gap: var(--sp-1); color: var(--text-secondary);
  white-space: nowrap; overflow: hidden; min-width: 0;
}
.shell-session-line > * { flex: 0 0 auto; }
.shell-session-status { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.shell-session-sep { color: var(--text-tertiary); }
.shell-session-dot { width: 6px; height: 6px; border-radius: var(--r-full); background: var(--nominal); flex: 0 0 auto; }
.shell-session-ip { font-family: var(--font-mono); color: var(--text-secondary); }

.nav-section { display: flex; flex-direction: column; gap: 2px; }
.nav-section-toggle {
  display: flex; align-items: center; justify-content: space-between;
  width: 100%;
  background: none; border: 0; cursor: pointer; text-align: left;
  color: var(--accent-2);
  font-family: inherit;
}
.nav-section-toggle:hover { color: var(--accent-2-hover); }
.nav-section-toggle:focus-visible { outline: var(--focus-ring); outline-offset: var(--focus-offset); border-radius: var(--r-sm); }
.nav-section-chev { transition: transform var(--motion-fast) var(--ease-out); }
.nav-section-toggle[aria-expanded="false"] .nav-section-chev { transform: rotate(-90deg); }
.nav-section-items { display: flex; flex-direction: column; gap: 2px; }
.nav-section-items[hidden] { display: none; }

.shell-user {
  position: relative;
  display: flex; flex-direction: column; gap: var(--sp-1);
}
.shell-user p { margin: 0; }
.shell-user-btn {
  display: flex; align-items: center; gap: var(--sp-2);
  width: 100%; min-width: 0;
  padding: var(--sp-1) var(--sp-2);
  background: none; border: 1px solid transparent; border-radius: var(--r-md);
  cursor: pointer; color: var(--text-primary); text-align: left; font-family: inherit;
}
.shell-user-btn:hover, .shell-user-btn[aria-expanded="true"] { background: var(--surface-raised); border-color: var(--border-subtle); }
.shell-user-btn:focus-visible { outline: var(--focus-ring); outline-offset: var(--focus-offset); }
.shell-user-btn[aria-expanded="true"] .shell-chev { transform: rotate(180deg); }
.shell-user-id { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: var(--sp-1); }
.shell-user-email { display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--fs-3); }
.shell-user-context { color: var(--text-tertiary); font-size: var(--fs-2); padding: 0 var(--sp-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.shell-user-slug { color: var(--text-tertiary); font-size: var(--fs-2); padding: 0 var(--sp-2); font-family: var(--font-mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.shell-user-menu {
  position: absolute; left: var(--sp-3); right: var(--sp-3); bottom: calc(100% - var(--sp-2));
  z-index: 200;
  padding: var(--sp-1);
  background: var(--surface-overlay);
  border: 1px solid var(--border-default);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-overlay);
  display: flex; flex-direction: column;
}
.shell-user-menu[hidden] { display: none; }
.shell-menu-item {
  display: flex; align-items: center; gap: var(--sp-2);
  width: 100%;
  padding: var(--sp-2) var(--sp-3);
  background: none; border: 0; border-radius: var(--r-sm);
  color: var(--text-secondary); font-size: var(--fs-3); font-family: inherit;
  text-align: left; text-decoration: none; cursor: pointer;
}
.shell-menu-item:hover { background: var(--surface-raised); color: var(--text-primary); }
.shell-menu-item:focus-visible { background: var(--surface-raised); color: var(--text-primary); outline: var(--focus-ring); outline-offset: calc(-1 * var(--focus-offset)); }
.shell-menu-sep { height: 1px; margin: var(--sp-1) 0; background: var(--border-subtle); }
.shell-menu-form { margin: 0; }
.shell-menu-danger, .shell-menu-danger:hover, .shell-menu-danger:focus-visible { color: var(--critical-text); }
.shell-user-noscript { font-size: var(--fs-2); color: var(--text-tertiary); }

/* CONFIG-4b: this disclosure's summary reads in sentence case. The global
   .evidence summary is an eyebrow (uppercase mono); this one is a question a
   reader asks, so it reads like one — the .cfg-steady precedent. */
.cfg-notcompared .evidence > summary {
  text-transform: none; letter-spacing: 0;
  font-family: var(--font-sans); font-size: var(--fs-3);
}

/* ── SCAN THE NETWORK (DISCOVER-1) ──────────────────────────────────────── */
.discover-panel { display: flex; flex-direction: column; gap: var(--sp-4); }
.discover-state { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3); color: var(--fg-1); }
.discover-state .discover-actions { margin-left: auto; }
.discover-why { color: var(--critical-text); }
.discover-capacity { display: flex; flex-wrap: wrap; gap: var(--sp-5); color: var(--fg-1); }
.discover-outcome { padding: var(--sp-3) var(--sp-4); border-radius: var(--radius); background: var(--surface-raised); color: var(--fg-0); }
.discover-outcome ul { margin: var(--sp-2) 0 0; padding-left: var(--sp-4); }
.discover-limit { margin-right: auto; color: var(--warning); }
.discover-group { display: flex; flex-direction: column; gap: var(--sp-2); margin-top: var(--sp-4); }
.discover-enrolment { display: flex; flex-direction: column; gap: var(--sp-2); }
.discover-retry { justify-content: flex-start; flex-wrap: wrap; }
/* DISCOVER-4 — the scan page reads as a tool: a summary strip (the Overview's
   console-strip), two dense tables at Devices-page height, one status chip per
   row, the detail behind a ▸. Existing tokens only. */
.discover-summary { align-items: center; }
.discover-summary .console-strip-status { flex-wrap: wrap; }
.discover-table table { width: 100%; }
.discover-table .col-expand { width: 28px; padding-right: 0; }
.discover-table .col-action { width: 1%; white-space: nowrap; text-align: right; }
.discover-expand { background: none; border: 0; padding: 0 var(--sp-1); color: var(--text-tertiary); cursor: pointer; font: inherit; }
.discover-expand:hover, .discover-expand[aria-expanded="true"] { color: var(--accent-soft); }
.discover-status { white-space: nowrap; }
.discover-status select { margin-left: var(--sp-2); }
.discover-rowact { display: inline-flex; align-items: center; gap: var(--sp-1); margin-left: var(--sp-2); color: var(--text-secondary); font-size: var(--fs-2); }
.discover-rowact input { width: auto; margin: 0; }
.discover-detail > td { background: var(--surface-raised); }
/* Detail rows are CLOSED only once the script is running (it sets the flag on
   <html>, which the panel's re-fetch never replaces); without it every detail
   row is simply shown and the ▸ hides. */
html[data-discover-js] .discover-detail:not(.is-open) { display: none; }
html:not([data-discover-js]) .discover-expand { display: none; }
.discover-detail-list { display: grid; grid-template-columns: max-content 1fr; gap: var(--sp-1) var(--sp-4); margin: 0; }
.discover-detail-list dt { color: var(--text-tertiary); font-size: var(--fs-2); text-transform: uppercase; letter-spacing: 0.06em; }
.discover-detail-list dd { margin: 0; color: var(--text-secondary); overflow-wrap: anywhere; }
.discover-detail-list ul { margin: 0; padding-left: var(--sp-4); }
.discover-end > summary, .discover-indirect > summary { cursor: pointer; list-style: none; }
.discover-end > summary::-webkit-details-marker, .discover-indirect > summary::-webkit-details-marker { display: none; }
.discover-end > summary .section-title, .discover-indirect > summary .section-title { display: inline; }
.discover-end > summary::before, .discover-indirect > summary::before { content: "▸ "; color: var(--text-tertiary); }
.discover-end[open] > summary::before, .discover-indirect[open] > summary::before { content: "▾ "; }
.discover-indirect { margin-top: var(--sp-3); }
.field-inline { display: inline-flex; align-items: center; gap: var(--sp-2); }
.first-collection { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3); padding: var(--sp-3) var(--sp-4); border-radius: var(--radius); background: var(--surface-raised); color: var(--fg-1); }

/* NAV-1: a kind beneath its parent module (Network › Switches · Routers ·
   Firewalls · Wireless). Indented to the parent's label, no icon of its own. */
.nav-item.nav-child { padding-left: calc(var(--sp-4) + var(--icon) + var(--sp-2)); font-size: var(--fs-3); }
/* I-40: the Health badge turns red when anything is critical. */
.nav-badge.critical { background: var(--critical); color: var(--on-accent); }

/* ── RACKS (RACK-1, report 00078) ────────────────────────────────────────────
   INVENTORY, NOT HEALTH: nothing here reads a status colour. The elevation is a
   CSS grid, one row per U with the top unit first; a mounted device is a block
   whose row and span arrive as custom properties (--rk-row, --rk-span). The
   stencils are inline SVG coloured from tokens; their one LED is the accent. */
.rk-page { display: flex; flex-direction: column; gap: var(--sp-4); }
.rk-head { display: flex; justify-content: space-between; align-items: flex-end; gap: var(--sp-4); }
.rk-refusal { margin: 0; padding: var(--sp-3) var(--sp-4); border-radius: var(--r-control);
  background: var(--raised); color: var(--critical-text); font-size: var(--fs-3); }
.rk-note { margin: 0; color: var(--muted); font-size: var(--fs-3); }
.rk-empty { display: flex; flex-direction: column; gap: var(--sp-3); align-items: flex-start; }

.rk-crumbs { display: flex; flex-wrap: wrap; align-items: flex-start; gap: var(--sp-3); }
.rk-level { display: flex; flex-direction: column; gap: var(--sp-2); min-width: 0; }
.rk-level-name { font-size: var(--fs-1); text-transform: uppercase; letter-spacing: .14em; color: var(--muted); }
.rk-sep { color: var(--faint); padding-top: var(--sp-5); }
.rk-pills { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin: 0; padding: 0; list-style: none; }
.rk-pill { display: inline-flex; align-items: center; height: 30px; padding: 0 var(--sp-3); border-radius: var(--r-control);
  background: var(--surface); color: var(--label); font-size: var(--fs-3); text-decoration: none; }
.rk-pill:hover { background: var(--raised); color: var(--text); }
.rk-pill.active { background: var(--accent-tint); color: var(--accent); }
.rk-none { font-size: var(--fs-3); padding: var(--sp-1) 0; }
.rk-face { display: flex; gap: var(--sp-1); }
.rk-add > summary { cursor: pointer; color: var(--accent-2); font-size: var(--fs-3); list-style: none; }
.rk-add > summary::-webkit-details-marker { display: none; }
.rk-add-form { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; padding-top: var(--sp-2); }
.rk-add-form input[type="text"] { width: 160px; }
.rk-num { width: 72px; }

.rk-grid { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 4fr) minmax(0, 4fr); gap: var(--sp-4); align-items: start; }
@media (max-width: 1200px) { .rk-grid { grid-template-columns: 1fr 1fr; } .rk-elev-card { grid-row: span 2; } }
@media (max-width: 800px) { .rk-grid { grid-template-columns: 1fr; } .rk-elev-card { grid-row: auto; } }
.rk-col { display: flex; flex-direction: column; gap: var(--sp-4); min-width: 0; }
.rk-card { display: flex; flex-direction: column; gap: var(--sp-3); min-width: 0; }
.rk-card-head { display: flex; justify-content: space-between; align-items: center; gap: var(--sp-3); }
.rk-facts { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: var(--sp-2) var(--sp-4); margin: 0; font-size: var(--fs-3); }
.rk-facts dt { color: var(--muted); }
.rk-facts dd { margin: 0; color: var(--body); overflow-wrap: anywhere; }
.rk-soft { color: var(--label); }
.rk-feed { display: inline-block; margin-right: var(--sp-2); }
.rk-actions { display: flex; flex-wrap: wrap; gap: var(--sp-2); }

/* The elevation: ears | bay | ears. */
.rk-elev { --rk-u: 22px; display: grid; grid-template-columns: 28px minmax(0, 1fr) 28px; gap: var(--sp-1);
  padding: var(--sp-2); border-radius: var(--r-control); background: var(--deep); }
/* The device page's mini elevation (RUCKUS-1): the SAME drawing at a smaller
   U. It used to be a 7px U with the ears hidden — a 1U block became a 5px
   sliver in a black box with no numbers. A mini U is tall enough to draw a
   faceplate and to print its number; empty U's are hairlines. */
.rk-mini { --rk-u: 12px; grid-template-columns: 22px minmax(0, 1fr) 22px; gap: var(--sp-1); padding: var(--sp-1); }
.rk-mini .rk-ears { font-size: var(--fs-1); }
.rk-mini .rk-unit { margin: 1px 0; border-radius: 2px; }
.rk-slot[data-free] { border-bottom-color: var(--hairline); }
.rk-ears { display: grid; grid-template-rows: repeat(var(--rk-units), var(--rk-u)); margin: 0; padding: 0; list-style: none;
  color: var(--faint); font-size: var(--fs-1); font-variant-numeric: tabular-nums; text-align: right; }
.rk-ears li { line-height: var(--rk-u); }
.rk-ears-right { text-align: left; }
.rk-bay { display: grid; grid-template-rows: repeat(var(--rk-units), var(--rk-u)); grid-template-columns: minmax(0, 1fr);
  position: relative; background: var(--ground); border-radius: var(--r-chip); }
.rk-slot { grid-column: 1; border-bottom: 1px solid var(--grid); }
.rk-unit { grid-column: 1; grid-row: var(--rk-row) / span var(--rk-span); position: relative; display: block; overflow: hidden;
  margin: 1px 0; border-radius: 3px; color: var(--text); text-decoration: none; background: var(--surface); }
.rk-unit:hover { outline: 1px solid var(--accent); }
.rk-unit.rk-selected { outline: 2px solid var(--accent); }
.rk-unit.rk-dim { opacity: .35; }
.rk-label { position: absolute; left: 44px; top: 50%; transform: translateY(-50%); display: flex; gap: var(--sp-2); align-items: baseline;
  max-width: calc(100% - 52px); padding: 0 var(--sp-1); border-radius: 3px; background: var(--deep); font-size: var(--fs-2);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rk-host { color: var(--text); }
.rk-pn { color: var(--label); }
.rk-device-card .rk-elev { max-width: 280px; }
.rk-path { margin: 0; color: var(--body); font-size: var(--fs-3); }

/* The stencils — token-coloured, drawn to the block's size. */
.st { display: block; width: 100%; height: 100%; }
.st-image { object-fit: fill; }
.st-body { fill: var(--raised); stroke: var(--hairline); stroke-width: 1; vector-effect: non-scaling-stroke; }
.st-port { fill: var(--deep); }
.st-bay { fill: var(--surface); stroke: var(--hairline); stroke-width: 1; vector-effect: non-scaling-stroke; }
.st-vent { fill: var(--grid); }
.st-plate { fill: var(--surface); }
.st-led { fill: var(--accent); }

/* The tray and Place. */
.rk-tray { display: flex; flex-direction: column; gap: var(--sp-2); margin: 0; padding: 0; list-style: none; }
.rk-tray-search { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; margin-bottom: var(--sp-3); }
.rk-tray-search input[type="search"] { flex: 1 1 220px; }
.rk-delete > summary { color: var(--muted); }
.rk-faceplate { margin-top: var(--sp-3); }
.rk-tray-item { display: flex; flex-direction: column; gap: var(--sp-1); padding: var(--sp-2) var(--sp-3);
  border-radius: var(--r-control); background: var(--raised); }
.rk-tray-item.rk-selected { outline: 1px solid var(--accent); }
.rk-tray-name { font-size: var(--fs-3); }
.rk-tray-meta { color: var(--muted); font-size: var(--fs-2); }
.rk-place { display: flex; flex-direction: column; gap: var(--sp-2); padding-top: var(--sp-2); }
.rk-place-fields { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.rk-place-fields .field { width: 120px; }
.rk-place > button { align-self: flex-start; }
.rk-unplace { margin-top: var(--sp-2); }

/* The platform hardware catalog. */
.rk-thumb { height: 22px; max-width: 120px; object-fit: contain; vertical-align: middle; border-radius: 2px; background: var(--deep); }
.rk-catalog-form { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: var(--sp-3); align-items: end; }
.rk-recorded { display: flex; flex-direction: column; gap: var(--sp-1); margin: 0; padding: 0; list-style: none; font-size: var(--fs-3); }

/* CONFIG-1 — the Config tab's one line above the config and the footer line. */
.cfg-infoline {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2) var(--sp-3);
  margin-bottom: var(--sp-3); font-size: var(--fs-3); color: var(--text-secondary);
}
.cfg-infoline > span:not(.cfg-infoline-actions):not(.chip) + span:not(.cfg-infoline-actions)::before {
  content: "·"; margin-right: var(--sp-3); color: var(--text-tertiary);
}
.cfg-infoline-actions { display: flex; gap: var(--sp-2); margin-left: auto; align-items: center; }
.cfg-infoline-actions form { margin: 0; }
.cfg-footline {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2) var(--sp-3);
  margin: var(--sp-3) 0 0; font-size: var(--fs-2); color: var(--text-tertiary);
}
.cfg-footline .inline-form { margin: 0; }
.cfg-attempt { display: flex; gap: var(--sp-2); align-items: center; font-size: var(--fs-3); color: var(--text-secondary); }
/* Two DISTINCT actions, visibly apart — never run-together text (CONFIG-3). */
.cfg-version-actions { white-space: nowrap; }
.cfg-version-actions .cfg-action-sep { color: var(--text-disabled); margin: 0 var(--sp-2); }
.cfg-notcompared { margin-top: var(--sp-2); font-size: var(--fs-2); }

/* ── SHELL-2: "Add later" form sections ─────────────────────────────────────
   A collapsed form card. The summary reads like the section title it
   replaced, with a quiet "add later" beside it; open, it is the same card. */
.form-later > summary.form-later-summary {
  cursor: pointer; list-style: none;
  display: flex; align-items: baseline; gap: var(--sp-2);
}
.form-later > summary.form-later-summary::-webkit-details-marker { display: none; }
.form-later > summary .section-title { margin: 0; }
.form-later > summary .muted { font-size: var(--fs-2); }
.form-later[open] > summary { margin-bottom: var(--sp-3); }
.form-later > summary:focus-visible { outline: var(--focus-ring); outline-offset: var(--focus-offset); }
.console-step-done { margin-left: var(--sp-2); white-space: nowrap; }

/* ── GET YOUR MONITORING ONLINE (SHELL-3) — the one onboarding wizard ───── */
.wiz-progress { list-style: none; margin: 0 0 var(--sp-4); padding: 0; display: flex; gap: var(--sp-2); flex-wrap: wrap; }
.wiz-progress-step { display: inline-flex; align-items: center; gap: var(--sp-2); padding: var(--sp-1) var(--sp-3);
  border-radius: var(--r-control); background: var(--surface); color: var(--fg-3); font-size: var(--fs-3); }
.wiz-progress-step[data-state="current"] { background: var(--raised); color: var(--text); font-weight: 600; }
.wiz-progress-step[data-state="done"] { color: var(--fg-2); }
.wiz-progress-num { font-family: var(--font-mono); }
.wiz-choice { border: 0; margin: 0 0 var(--sp-3); padding: 0; display: flex; flex-direction: column; gap: var(--sp-2); font-size: var(--fs-3); }
.wiz-modules { border: 0; margin: var(--sp-3) 0; padding: 0; display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4); }
.wiz-module { display: inline-flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-3); }
.wiz-current { font-size: var(--fs-3); color: var(--fg-2); }
.wiz-code-note { font-size: var(--fs-2); }
.wiz-list { margin: 0 0 var(--sp-3); padding-left: var(--sp-4); display: flex; flex-direction: column; gap: var(--sp-2); }
.console-step-todo { margin-left: var(--sp-2); white-space: nowrap; }

/* ── ACCOUNT & SECURITY (ACCOUNT-1) ─────────────────────────────────────── */
.account-page .account-kv { grid-template-columns: 180px 1fr; }
.account-page .account-codes {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-2) var(--sp-5);
  margin: 0;
  padding-left: var(--sp-5);
  font-size: var(--fs-4);
  color: var(--fg-0);
}
.account-page .account-qr {
  display: inline-block;
  padding: var(--sp-3);
  background: var(--text);
  color: var(--ground);
  border-radius: var(--radius);
  line-height: 0;
}
.account-page .account-qr svg { display: block; }
.account-page .account-secret { letter-spacing: 0.08em; }
.account-page .account-toggle {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  color: var(--fg-0);
}
.account-page .account-tier { color: var(--fg-0); }
@media (max-width: 640px) {
  .account-page .account-kv { grid-template-columns: 1fr; }
  .account-page .account-codes { grid-template-columns: 1fr; }
}

/* ── DISCOVERY: A SIGN OF LIFE (DISCOVER-3) ─────────────────────────────────
   The family mark turns while a scan is queued or running, and beside a
   device still waiting for its first collection. It is rendered ONLY in
   those states, so the motion is the state and stops when the state ends —
   the one kind of animation DESIGN.md allows (motion that encodes something
   true). Reduced motion stills it: the mark stays, the turn does not. */
.discover-life { display: inline-flex; align-items: center; perspective: 800px; vertical-align: middle; }
.discover-life .hh-mark { animation: discover-turn 1.6s cubic-bezier(.65, 0, .35, 1) infinite; transform-style: preserve-3d; }
@keyframes discover-turn {
  from { transform: rotateY(0deg); }
  to { transform: rotateY(360deg); }
}
.discover-indirect > summary { cursor: pointer; color: var(--accent-2); }
@media (prefers-reduced-motion: reduce) {
  .discover-life .hh-mark { animation: none; }
}

