/* ===========================================================================
   Lodestar — administration console (S4).

   Two layers, in this order and nothing else:
     1. TOKENS   — colour, type scale, spacing, radius, elevation.
     2. COMPONENTS — header, product switcher, nav, card, table, pill, button,
                     field, empty state, skeleton, error state, alert/toast.

   The CSP served with this page is `default-src 'self'` with no inline code
   and no remote origin, so there is no framework and no CDN here — only this
   one local stylesheet, the local bundle, and the brand faces vendored as
   real files under `fonts/` (see the `@font-face` block immediately below,
   which needed no CSP change whatsoever to work).

   ---------------------------------------------------------------------------
   TOKEN PROVENANCE — this is a COPY, not a shared library.

   The colour tokens below are copied verbatim from Lodestar Insight's
   `lodestar-launch/services/metrics-web/src/theme.js`, so that Control Plane,
   Insight and the host monitor read as one product. The two codebases stay
   separate for MVP1 (the brief permits it), so the values are duplicated
   deliberately rather than vendored or symlinked across repositories.

   TO DE-DUPLICATE LATER: when the surfaces are merged, extract this `:root`
   block into a single package and delete the copy from both repos. Until
   then, a colour changed in Insight must be changed here by hand. Near-miss
   colours are an anti-pattern in the Pulsar guidelines — nothing here is
   approximated, so a mismatch is a bug, not a variation.
   =========================================================================== */


/* ===========================================================================
   THE BRAND FACES, SELF-HOSTED — the thing the token comment below used to
   say was "the real option" and "not blocked by anything here". It is now
   done, so this is what was actually shipped and what was deliberately not.

   FOUR REAL `.woff2` FILES, NOT BASE64. The obvious way to embed a font in a
   console that serves one stylesheet is a `data:` URI, and on THIS page it
   does not work: the policy is `default-src 'self'` with NO `font-src`
   directive, so font loads fall back to `default-src`, and `'self'` does not
   admit a `data:` URL. Embedding would therefore have forced
   `font-src 'self' data:` into `admin-assets.ts` — a real (if small)
   loosening of the exact policy this console's whole design leans on, in
   exchange for a stylesheet a third of a megabyte larger, since base64 costs
   ~33% over the bytes it encodes. Plain files under this origin cost nothing,
   need no directive at all, and cache as separate resources. They only
   required `.woff2` to be added to the served content types, because the
   asset route refuses anything whose extension it does not recognise — a
   whitelist, which is why a new file type is an explicit decision here rather
   than a silent one.

   WHY ONLY FOUR FACES. Lato's family has no true 500 or 600 face — it runs
   100 / 300 / 400 / 700 / 900 — and this stylesheet only ever asks for 400,
   500, 600 and 700. CSS weight matching sends a 500 request down to 400 and a
   600 request up to 700, which is precisely what real Lato does, so shipping
   Light / Black / Hairline would add ~90KB to the console to change nothing
   on screen. The italics ARE shipped even though this stylesheet sets no
   `font-style: italic` today: they cost 45KB together, and the alternative
   the first time someone writes an `<em>` is a synthetic oblique — an
   upright Lato mechanically sheared — which looks like a rendering fault.

   LICENCE. Lato is SIL OFL 1.1, which permits redistribution inside a product
   like this one. See `fonts/OFL-NOTICE.md`, which travels with the binaries.
   =========================================================================== */

@font-face {
  font-family: "Lato";
  src: url("./fonts/lato-regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  /* `swap`, not `block`. The fallback stack in `--font-sans` is a reviewed
     set of system faces, so rendering it for a few frames is a better outcome
     than rendering NOTHING — which is what `block` does, and which for a
     same-origin file the browser has almost certainly already cached is a
     flash of invisible text bought for no benefit at all. */
  font-display: swap;
}

@font-face {
  font-family: "Lato";
  src: url("./fonts/lato-italic.woff2") format("woff2");
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}

@font-face {
  font-family: "Lato";
  src: url("./fonts/lato-bold.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Lato";
  src: url("./fonts/lato-bold-italic.woff2") format("woff2");
  font-weight: 700;
  font-style: italic;
  font-display: swap;
}

:root {
  color-scheme: dark;

  /* --- Pulsar palette (copied from Insight; see note above) --- */
  --coral: #D26F6B;
  --pink: #BE3A74;
  --purple: #655BD5;
  --blue: #00B0F0;
  --green: #92BB35;
  --yellow: #D2C91E;
  --navy: #2E3252;
  --purple-dk: #4436C8;
  --coral-dk: #C74F49;

  /* --- Ground and surfaces (copied from Insight) --- */
  --bg: #0E0F1A;
  --bg-alt: #161828;
  --bg-card: #1C1F35;
  --bg-nav: rgba(14, 15, 26, 0.88);
  --border: rgba(255, 255, 255, 0.08);
  --border-strong: rgba(255, 255, 255, 0.16);

  /* --- Ink (copied from Insight) --- */
  --text: #FFFFFF;
  --text-body: #C8CAD8;
  --text-muted: #A0A4BD;

  /* --- Semantic roles. Derived here, not in Insight: the console has states
         Insight has no need for (a destructive action, a step-up prompt). --- */
  --accent: var(--purple);
  --accent-ink: #FFFFFF;
  --link: var(--blue);
  --danger: var(--coral);
  --warn: var(--yellow);
  --ok: var(--green);

  /* --- Type scale. One ratio, six steps; nothing sets a raw px size. --- */
  /* --- The two brand faces. THEY ARE NOT SYMMETRICAL, AND THAT IS THE POINT.

     `--font-sans` names Lato first and MEANS it now: four real faces are
     vendored under `fonts/` and declared above, so Lato is what renders, for
     everyone, with no CDN and no CSP change. The system faces behind it are a
     few-frame bridge while a same-origin file is read, nothing more.

     `--font-display` names Montserrat first and does NOT mean it yet. The
     product owner's font kit contains exactly ONE Montserrat face —
     Montserrat Thin, a ~100-weight hairline — and this stylesheet asks for
     display text at 600 and 700. Declaring that single file as `Montserrat`
     would have been actively destructive rather than merely incomplete: CSS
     weight matching picks the nearest face WITHIN a declared family, so every
     heading, `.product` and `.kpi-value` in the console would have snapped to
     the hairline and then been synthetically emboldened by the browser — a
     smeared 100-weight face impersonating a bold, where a perfectly
     respectable system face stands today. That is a downgrade, so it was not
     done, and the missing weights are a reported gap rather than a papered
     one.

     Montserrat Thin is deliberately NOT vendored either. Nothing in this
     console is large enough to carry a hairline: the largest step in the
     scale is `--text-2xl` at 38px, and a 100-weight face needs roughly 48px
     before it stops reading as a rendering fault. Shipping 25KB that nothing
     may correctly reference is worse than the gap it pretends to close.

     WHEN MONTSERRAT 400/600/700 ARRIVE, the change is three more `@font-face`
     blocks above and NOTHING ELSE — no token edit, no rule edit, no CSP edit,
     no build-script edit beyond the file list. That is the whole reason the
     stack is written this way round.

     The Google Fonts <link> is still not the answer and never was: this page
     is `default-src 'self'`, the link would be blocked outright, the faces
     would not load, and the console would have bought a failed third-party
     request and a CSP violation in the log for a page that renders in the
     fallbacks anyway. --- */
  --font-display: Montserrat, "Segoe UI", -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, sans-serif;
  --font-sans: Lato, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --text-xs: 11px;
  --text-sm: 12px;
  --text-base: 14px;
  --text-md: 16px;
  --text-lg: 22px;
  --text-xl: 30px;
  --text-2xl: 38px;
  --leading: 1.55;

  /* --- Spacing scale (4px base). --- */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 22px;
  --space-6: 28px;
  --space-7: 40px;
  --space-8: 64px;

  /* --- Radius. --- */
  --radius-sm: 8px;
  --radius-md: 14px;
  --radius-lg: 18px;
  --radius-pill: 999px;

  /* --- Elevation. Two steps only: a card, and a thing floating over it.
         Light tints them navy and drops the alpha hard: a 45%-black shadow
         that reads as depth on #0E0F1A reads as dirt on #EEF1F6. --- */
  --elev-card: 0 18px 38px rgba(0, 0, 0, 0.28);
  --elev-float: 0 22px 48px rgba(0, 0, 0, 0.45);

  /* --- Everything below was a COLOUR LITERAL at a call site until P2-2. ---

     Each one is here for the same reason: a literal in the component layer
     is a colour the second theme cannot reach, and — because every one of
     them turned out to be a wash, a hairline rule or a shimmer — the way it
     fails on the wrong ground is "looks slightly off", never "is obviously
     broken". Those are the ones that ship. --- */

  /* The ambient bloom at the top of the page. */
  --bg-glow: #1a1440;
  /* The hairline between table rows: lighter than `--border`, on purpose —
     a full-strength rule under every row turns a table into a grid. */
  --row-rule: rgba(255, 255, 255, 0.04);
  --skeleton-base: rgba(255, 255, 255, 0.05);
  --skeleton-sheen: rgba(255, 255, 255, 0.12);

  /* The focus ring, which is the one colour here with a hard floor under it:
     brand blue is 7.7:1 on the dark ground and 2.5:1 on white, so the light
     blocks move it to `--purple-dk`, already 8.1:1 there. */
  --focus: var(--blue);

  /* Status washes and hairlines, one triple per semantic role. `-wash` is
     the fill behind a pill, `-line` its border, `-panel` the calmer fill an
     alert or an error state uses across a whole card. */
  --accent-wash: rgba(101, 91, 213, 0.18);
  --accent-line: rgba(101, 91, 213, 0.5);
  --accent-pill-ink: #B9B2FF;
  --info-wash: rgba(0, 176, 240, 0.14);
  --info-line: rgba(0, 176, 240, 0.45);
  --info-pill-ink: #7FD8FF;
  --ok-wash: rgba(146, 187, 53, 0.1);
  --ok-line: rgba(146, 187, 53, 0.4);
  --ok-panel: rgba(146, 187, 53, 0.07);
  --warn-wash: rgba(210, 201, 30, 0.1);
  --danger-wash: rgba(210, 111, 107, 0.12);
  --danger-line: rgba(210, 111, 107, 0.45);
  --danger-panel: rgba(210, 111, 107, 0.08);
  --muted-wash: rgba(160, 164, 189, 0.1);
}

/* ===========================================================================
   THE OTHER TWO STATES (P2-2).

   The traceability matrix carried `B5-13 | Light/dark compatibility |
   styles.css:30 color-scheme: dark; no light block | GAP (deferred to P2-2)`.
   This is that block, and the console's half of closing it.

   THERE ARE THREE STATES, NOT TWO — light, dark, and follow-the-system — and
   follow-the-system is the DEFAULT, which is why `index.html` carries no
   `data-theme` attribute and the unconditional `:root` above is simply "dark
   unless told otherwise".

     no attribute + dark system   -> :root (dark)                 [default]
     no attribute + light system  -> the @media block (light)
     data-theme="light"           -> the explicit block (light), on ANY system
     data-theme="dark"            -> :root (dark), on ANY system

   THE GUARD `:not([data-theme="dark"])` IS THE WHOLE TRICK. Without it, an
   administrator on a light-configured laptop who chose dark would be handed
   the media query's light values anyway: the toggle would work in one
   direction only. That bug is invisible to anyone developing on a dark
   machine, which is everyone who has worked on this console.

   THE TWO LIGHT BLOCKS ARE IDENTICAL AND HAVE TO BE. A selector list cannot
   straddle an `@media` boundary, so plain CSS has no way to share one set of
   declarations between them; they are duplicated deliberately, and
   `theme.test.ts` parses both and fails if they drift.

   ORDER MATTERS: the explicit block comes AFTER the media query. Both
   selectors weigh (0,2,0) and a media query adds no specificity, so source
   order is the entire mechanism by which a deliberate choice beats an
   inferred one.

   THE LIGHT INKS ARE THE BRAND HUES, DARKENED — same hue, same saturation,
   lower lightness — not new colours chosen to look similar. Brand yellow is
   11.0:1 on #0E0F1A and 1.7:1 on white; reusing it on paper would be
   decoration impersonating a status. Near-miss colours are an anti-pattern in
   the Pulsar guidelines, and a darkened hue is not a near miss.
   =========================================================================== */

@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) {
    --bg: #EEF1F6;
    --bg-alt: #F6F8FB;
    --bg-card: #FFFFFF;
    --bg-nav: rgba(246, 248, 251, 0.92);
    --border: rgba(46, 50, 82, 0.14);
    --border-strong: rgba(46, 50, 82, 0.26);

    --text: #2E3252;
    --text-body: #3A3A3A;
    --text-muted: #666677;

    --accent: var(--purple);
    --accent-ink: #FFFFFF;
    --link: var(--purple-dk);
    --danger: #B93D38;
    --warn: #706B10;
    --ok: #587120;

    --elev-card: 0 18px 38px rgba(46, 50, 82, 0.1);
    --elev-float: 0 22px 48px rgba(46, 50, 82, 0.18);

    --bg-glow: #E4E2F7;
    --row-rule: rgba(46, 50, 82, 0.08);
    --skeleton-base: rgba(46, 50, 82, 0.06);
    --skeleton-sheen: rgba(46, 50, 82, 0.14);

    --focus: var(--purple-dk);

    --accent-wash: rgba(101, 91, 213, 0.12);
    --accent-line: rgba(101, 91, 213, 0.35);
    --accent-pill-ink: #4436C8;
    --info-wash: rgba(0, 176, 240, 0.14);
    --info-line: rgba(0, 112, 153, 0.35);
    --info-pill-ink: #007099;
    --ok-wash: rgba(88, 113, 32, 0.12);
    --ok-line: rgba(88, 113, 32, 0.35);
    --ok-panel: rgba(88, 113, 32, 0.08);
    --warn-wash: rgba(112, 107, 16, 0.12);
    --danger-wash: rgba(185, 61, 56, 0.12);
    --danger-line: rgba(185, 61, 56, 0.4);
    --danger-panel: rgba(185, 61, 56, 0.08);
    --muted-wash: rgba(102, 102, 119, 0.1);

    color-scheme: light;
  }
}

:root[data-theme="light"] {
  --bg: #EEF1F6;
  --bg-alt: #F6F8FB;
  --bg-card: #FFFFFF;
  --bg-nav: rgba(246, 248, 251, 0.92);
  --border: rgba(46, 50, 82, 0.14);
  --border-strong: rgba(46, 50, 82, 0.26);

  --text: #2E3252;
  --text-body: #3A3A3A;
  --text-muted: #666677;

  --accent: var(--purple);
  --accent-ink: #FFFFFF;
  --link: var(--purple-dk);
  --danger: #B93D38;
  --warn: #706B10;
  --ok: #587120;

  --elev-card: 0 18px 38px rgba(46, 50, 82, 0.1);
  --elev-float: 0 22px 48px rgba(46, 50, 82, 0.18);

  --bg-glow: #E4E2F7;
  --row-rule: rgba(46, 50, 82, 0.08);
  --skeleton-base: rgba(46, 50, 82, 0.06);
  --skeleton-sheen: rgba(46, 50, 82, 0.14);

  --focus: var(--purple-dk);

  --accent-wash: rgba(101, 91, 213, 0.12);
  --accent-line: rgba(101, 91, 213, 0.35);
  --accent-pill-ink: #4436C8;
  --info-wash: rgba(0, 176, 240, 0.14);
  --info-line: rgba(0, 112, 153, 0.35);
  --info-pill-ink: #007099;
  --ok-wash: rgba(88, 113, 32, 0.12);
  --ok-line: rgba(88, 113, 32, 0.35);
  --ok-panel: rgba(88, 113, 32, 0.08);
  --warn-wash: rgba(112, 107, 16, 0.12);
  --danger-wash: rgba(185, 61, 56, 0.12);
  --danger-line: rgba(185, 61, 56, 0.4);
  --danger-panel: rgba(185, 61, 56, 0.08);
  --muted-wash: rgba(102, 102, 119, 0.1);

  color-scheme: light;
}

/* ===========================================================================
   COMPONENT LAYER
   =========================================================================== */

* { box-sizing: border-box; }

body {
  margin: 0;
  /* The same radial ground Insight uses, so the two surfaces match at a
     glance rather than merely sharing a hex value. */
  background:
    radial-gradient(ellipse at 50% 0%, var(--bg-glow) 0%, var(--bg) 62%),
    var(--bg);
  background-attachment: fixed;
  color: var(--text-body);
  font-family: var(--font-sans);
  font-size: var(--text-base);
  line-height: var(--leading);
  -webkit-font-smoothing: antialiased;
}

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

h1 { font-size: var(--text-lg); font-weight: 700; margin: 0; letter-spacing: -0.01em; color: var(--text); }
h2 {
  font-size: var(--text-sm); font-weight: 600; margin: 0 0 var(--space-4);
  color: var(--text); text-transform: uppercase; letter-spacing: 0.09em;
}
h3 { font-size: var(--text-base); font-weight: 600; margin: 0 0 var(--space-2); color: var(--text); }

/* Focus is never removed and never subtle: this console authorises devices
   and replaces credentials, so knowing what has focus is a safety property. */
:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

[hidden] { display: none !important; }

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

.muted { color: var(--text-muted); }
.mono { font-family: var(--font-mono); font-size: var(--text-sm); }
h1, h2, h3, .product, .kpi-value { font-family: var(--font-display); letter-spacing: -0.01em; }
.small { font-size: var(--text-sm); }

/* --- Header + product switcher ------------------------------------------
   The switcher is the single strongest signal that Control Plane, Insight and
   Health are one product: the same control, in the same place, on all three. */
.app-header {
  position: sticky; top: 0; z-index: 20;
  background: var(--bg-nav);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--border);
  padding: var(--space-3) var(--space-6);
  display: flex; align-items: center; gap: var(--space-5); flex-wrap: wrap;
}
.app-header .identity { display: flex; align-items: center; gap: var(--space-3); min-width: 0; }
/* The mark carries the brand; the product name sits beside it as a divider
   pair rather than competing with it. */
.app-header .brand-mark { display: block; height: 30px; width: auto; }
.app-header .product {
  display: flex; flex-direction: column; line-height: 1.15;
  font-size: var(--text-md); font-weight: 700; color: var(--text);
  padding-left: var(--space-3); border-left: 1px solid var(--border);
}
.app-header .product-surface {
  font-family: var(--font-sans);
  font-size: var(--text-xs); font-weight: 600; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--text-muted);
}
.app-header .spacer { flex: 1 1 auto; }

/* WHO IS SIGNED IN. */
.app-header .who { display: flex; align-items: center; gap: var(--space-3); min-width: 0; }
.who-avatar {
  flex: 0 0 auto; width: 32px; height: 32px; border-radius: 50%;
  display: grid; place-items: center;
  font-size: 12px; font-weight: 700; letter-spacing: 0.02em; color: var(--accent-ink);
  /* The brand gradient, used as a small accent rather than a surface. */
  background: linear-gradient(135deg, var(--purple), var(--pink));
}
.who-text { display: flex; flex-direction: column; min-width: 0; line-height: 1.25; }
.who-name {
  font-size: var(--text-sm); font-weight: 600; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 22ch;
}
.who-sub {
  font-family: var(--font-mono); font-size: 11px; color: var(--text-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 24ch;
}
.who-badges { display: flex; gap: var(--space-1); flex-wrap: wrap; }
.pill-admin { background: var(--accent-wash); color: var(--accent-pill-ink); border-color: var(--accent-line); }
.pill-stepup { background: var(--info-wash); color: var(--info-pill-ink); border-color: var(--info-line); }

.switcher { display: flex; gap: var(--space-1); flex-wrap: wrap; }
.switcher a {
  padding: 5px 13px; border-radius: var(--radius-pill);
  font-size: var(--text-sm); font-weight: 600; text-decoration: none;
  color: var(--text-muted); border: 1px solid var(--border);
}
.switcher a:hover { color: var(--text); border-color: var(--border-strong); }
.switcher a[aria-current="page"] {
  background: linear-gradient(135deg, var(--purple), var(--purple-dk));
  color: var(--text); border-color: transparent;
}

/* --- Section nav (the information architecture) -------------------------- */
.nav {
  display: flex; gap: var(--space-1); flex-wrap: wrap;
  padding: var(--space-3) var(--space-6) 0;
  border-bottom: 1px solid var(--border);
}
.nav button, .nav a {
  appearance: none; background: transparent; border: 0;
  border-bottom: 2px solid transparent;
  color: var(--text-muted); cursor: pointer; text-decoration: none;
  font: inherit; font-size: var(--text-sm); font-weight: 600;
  padding: var(--space-2) var(--space-3) var(--space-3);
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
}
.nav button:hover, .nav a:hover { color: var(--text); }
.nav button[aria-current="page"] { color: var(--text); border-bottom-color: var(--purple); }
.nav a::after {
  /* Insight and Health leave this console; say so rather than surprising. */
  content: " \2197"; color: var(--text-muted);
}

main { padding: var(--space-6) var(--space-6) var(--space-8); max-width: 1400px; margin: 0 auto; }

/* --- Card ---------------------------------------------------------------- */
.card {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: var(--space-5);
  box-shadow: var(--elev-card);
}
.card + .card { margin-top: var(--space-5); }
.card > .hint { margin-top: 0; }
/* THE OVERVIEW ALIGNMENT DEFECT, and why it was invisible to whoever wrote it.
   The rule above is a FLOW rule: cards stacked down a panel need air between
   them, and `.card + .card` is the cheapest way to say "every card but the
   first". The four KPI tiles are also `.card`, and they are also adjacent
   siblings — so three of the four inherited a 22px top margin inside a GRID,
   where the gap already does that job. The first tile (Devices) got none,
   which is precisely why it alone sat higher than the other three.

   The height difference had the same single cause. `.kpi` asked for
   `height: 100%`, which resolves against the grid AREA and knows nothing
   about margins, so tiles 2-4 rendered a full row tall and were then pushed
   22px down — overflowing their row and colliding with the "Tenant" heading
   underneath. Devices, margin-free, filled its row exactly and looked short.

   So: inside a grid the flow margin is wrong by construction, and it is
   cancelled here for every grid, not just this one — `.grid.two` holds cards
   too and had the same latent skew. */
.grid > .card + .card { margin-top: 0; }

.grid { display: grid; gap: var(--space-5); }
/* The brand rule is explicit: cards in the same row share height AND width.
   `auto-fit` let the last row stretch its survivors, so four tiles came out
   unequal. Explicit equal tracks, and equal rows, so they cannot. */
.grid.kpis { grid-template-columns: repeat(4, minmax(0, 1fr)); grid-auto-rows: 1fr; align-items: stretch; }
@media (max-width: 1100px) { .grid.kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 560px) { .grid.kpis { grid-template-columns: minmax(0, 1fr); } }
.grid.two { grid-template-columns: repeat(auto-fit, minmax(420px, 1fr)); align-items: start; }

/* No `height: 100%`. The grid's own `align-items: stretch` above already
   gives every tile the full row height, and it does so through the alignment
   container — which accounts for margins, where a percentage height does not.
   `justify-content: space-between` then pins the sub-label to the bottom of
   whatever that height is, so a three-part sub-label ("3 authorised · 0
   suspended · 0 revoked") and a one-line one produce identical tiles: the
   tallest content sets the row, and `grid-auto-rows: 1fr` gives that height
   to all four. */
.kpi { min-height: 128px; display: flex; flex-direction: column; justify-content: space-between; }
.kpi .label { font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-muted); }
.kpi .num { font-size: var(--text-2xl); font-weight: 700; color: var(--text); line-height: 1.05; }
.kpi .sub { font-size: var(--text-sm); color: var(--text-muted); }

/* --- Definition list (the "facts about this thing" component) ------------ */
/* The "Tenant" heading under the KPI row. `h3` is margin-top: 0 by this
   sheet's own reset, so this sub-heading sat flush against the grid above it
   and had nothing but the tiles' overflow between them. It gets the same
   separation any two blocks in a card get. */
.section-sub { margin-top: var(--space-5); }

dl { display: grid; grid-template-columns: max-content 1fr; gap: var(--space-1) var(--space-5); margin: 0; }
dt { color: var(--text-muted); font-size: var(--text-sm); }
dd { margin: 0; color: var(--text-body); }

/* --- Table --------------------------------------------------------------- */
.scroll { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: var(--text-base); }
th {
  text-align: left; font-weight: 600; color: var(--text-muted);
  font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.07em;
  padding: 0 var(--space-3) var(--space-2) 0; border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
td { padding: var(--space-2) var(--space-3) var(--space-2) 0; border-bottom: 1px solid var(--row-rule); vertical-align: top; }
td.n, th.n { text-align: right; font-variant-numeric: tabular-nums; }
tbody tr:last-child td { border-bottom: none; }

/* --- Status pill --------------------------------------------------------- */
.pill {
  display: inline-block; padding: 2px 10px; border-radius: var(--radius-pill);
  font-size: var(--text-xs); font-weight: 700; letter-spacing: 0.06em;
  text-transform: uppercase; border: 1px solid currentColor; white-space: nowrap;
}
.pill-pending   { color: var(--warn);   background: var(--warn-wash); }
.pill-authorized{ color: var(--ok);     background: var(--ok-wash); }
.pill-suspended { color: var(--text-muted); background: var(--muted-wash); }
.pill-revoked   { color: var(--danger); background: var(--danger-wash); }
.pill-healthy   { color: var(--ok);     background: var(--ok-wash); }
.pill-degraded  { color: var(--danger); background: var(--danger-wash); }
.pill-unknown   { color: var(--text-muted); background: var(--muted-wash); }

/* --- Buttons ------------------------------------------------------------- */
button, .btn {
  font: inherit; font-size: var(--text-sm); font-weight: 600;
  padding: 7px 16px; border-radius: var(--radius-pill);
  border: 1px solid transparent; cursor: pointer;
  background: linear-gradient(135deg, var(--purple), var(--purple-dk));
  color: var(--accent-ink);
}
button:hover, .btn:hover { filter: brightness(1.08); }
button:disabled { opacity: 0.45; cursor: not-allowed; filter: none; }
button.secondary {
  background: transparent; border-color: var(--border-strong); color: var(--text);
}
button.ghost {
  background: transparent; border-color: transparent; color: var(--link);
  padding-left: var(--space-2); padding-right: var(--space-2);
}
button.danger {
  background: transparent; border-color: var(--danger); color: var(--danger);
}
button.danger:hover { background: var(--danger-wash); filter: none; }
.actions { display: flex; gap: var(--space-2); flex-wrap: wrap; }

/* --- Form field ---------------------------------------------------------- */
form { margin: 0; }
.field { display: flex; flex-direction: column; gap: var(--space-1); margin-bottom: var(--space-3); max-width: 30rem; }
.field label { color: var(--text-muted); font-size: var(--text-sm); font-weight: 600; }
.field .hint { margin: 0; font-size: var(--text-sm); color: var(--text-muted); }
input, select, textarea {
  width: 100%; font: inherit; color: var(--text);
  background: var(--bg-alt); border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm); padding: 8px 11px;
}
select[multiple] { min-height: 8.5rem; }
input::placeholder { color: var(--text-muted); }
.row { display: flex; gap: var(--space-3); flex-wrap: wrap; align-items: flex-end; }
.row .field { margin-bottom: 0; }
.hint { color: var(--text-muted); font-size: var(--text-sm); max-width: 70ch; }
/* A checkbox beside its label rather than stretched above it (the TypeSafe
   toggles). The global `input { width: 100% }` is what this undoes. */
.check-row { display: flex; align-items: center; gap: var(--space-2); flex-direction: row; }
.check-row input[type="checkbox"] { width: auto; padding: 0; }
.check-row label { color: var(--text); font-weight: 400; }

/* --- In-product help (S8) ------------------------------------------------
   One quiet link per section heading, mounted at boot and only when this
   deployment has a knowledge base configured. Deliberately understated: it is
   an offer, and a heading that shouts twice says nothing. */
h2 .help-link {
  float: right;
  font-size: var(--text-sm);
  font-weight: 500;
  text-transform: none;
  letter-spacing: 0;
  color: var(--link);
  text-decoration: none;
  border-bottom: 1px solid transparent;
}
h2 .help-link:hover,
h2 .help-link:focus-visible { border-bottom-color: currentColor; }

/* --- Empty state --------------------------------------------------------- */
.empty {
  color: var(--text-muted); text-align: center;
  padding: var(--space-6) var(--space-4);
  border: 1px dashed var(--border-strong); border-radius: var(--radius-md);
}
.empty strong { display: block; color: var(--text); margin-bottom: var(--space-1); font-size: var(--text-md); }

/* --- Destructive action (E6) ---------------------------------------------
   A destructive control is SEPARATED from the fields above it and carries its
   consequence beside it, so it cannot be reached as the next tab stop after
   typing. Device revocation earns this by being a row action behind a
   confirm; credential replacement is a form submit, so the separation has to
   be drawn here. */
.destructive-action {
  margin-top: var(--space-5); padding-top: var(--space-4);
  border-top: 1px solid var(--border-strong);
}
.destructive-action .hint { margin: 0 0 var(--space-3); max-width: 42rem; }

/* --- Loading skeleton ---------------------------------------------------- */
.skeleton { display: grid; gap: var(--space-2); }
.skeleton span {
  display: block; height: 12px; border-radius: var(--radius-sm);
  background: linear-gradient(90deg, var(--skeleton-base), var(--skeleton-sheen), var(--skeleton-base));
  background-size: 200% 100%;
}
.skeleton span:nth-child(2) { width: 82%; }
.skeleton span:nth-child(3) { width: 64%; }
@media (prefers-reduced-motion: no-preference) {
  .skeleton span { animation: shimmer 1.4s ease-in-out infinite; }
  @keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
}

/* --- Error state (inside a section) and alert/toast (page level) ---------- */
.state-error {
  border: 1px solid var(--danger-line); background: var(--danger-panel);
  border-radius: var(--radius-md); padding: var(--space-3) var(--space-4); color: var(--text-body);
}
.state-error strong { color: var(--danger); }

.alert {
  border: 1px solid var(--border-strong); background: var(--bg-alt);
  border-radius: var(--radius-md); padding: var(--space-3) var(--space-4);
  margin-bottom: var(--space-4); color: var(--text-body);
}
.alert-error { border-color: var(--danger-line); background: var(--danger-panel); }
.alert-ok { border-color: var(--ok-line); background: var(--ok-panel); }

.toast {
  position: fixed; left: 50%; bottom: var(--space-5); transform: translateX(-50%);
  z-index: 40; max-width: min(46rem, calc(100vw - 2 * var(--space-4)));
  box-shadow: var(--elev-float); margin: 0;
}

/* --- Signed-out card ----------------------------------------------------- */
.signed-out { min-height: 72vh; display: grid; place-items: center; padding: var(--space-6) var(--space-4); }
.signed-out .card { max-width: 30rem; text-align: center; padding: var(--space-7) var(--space-6); }
.signed-out .brand {
  margin: 0 0 var(--space-2); font-size: var(--text-xs); font-weight: 700;
  letter-spacing: 0.22em; text-transform: uppercase; color: var(--accent);
}
.signed-out h1 { font-size: var(--text-xl); margin-bottom: var(--space-3); }
.signed-out p { color: var(--text-muted); margin: 0 0 var(--space-5); }
.signed-out button { font-size: var(--text-base); padding: 10px 24px; }

/* --- Link-out cards for Insight and Health ------------------------------- */
.linkout { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); flex-wrap: wrap; }

/* --- Footer --------------------------------------------------------------
   A real footer, not a caption. It carries the product, the company and the
   copyright on the left-to-right reading order the brand uses everywhere
   else: mark, then product, then who makes it.

   A HAIRLINE ABOVE IT, because the footer is now permanent — it stays up on
   the signed-out screen too — and without a seam it reads as one more
   paragraph of the page rather than as the page's edge. The rule is the same
   `--border` every other seam in this console uses; it is not a new idea.

   NO GRADIENT PLATE. The brand offers offset gradient backplates for
   dashboard KPIs, hero panels and feature tiles. A footer is none of those,
   and putting the loudest device in the system on the quietest element in
   the page would be brand-shaped rather than on-brand. The one piece of
   brand colour down here is the symbol mark, which carries the gradient
   itself. ----------------------------------------------------------------- */
footer {
  max-width: 1400px; margin: var(--space-6) auto 0; padding: var(--space-5) var(--space-6) var(--space-6);
  border-top: 1px solid var(--border);
  color: var(--text-muted); font-size: var(--text-sm);
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-3) var(--space-5); flex-wrap: wrap;
}

/* Both halves are flex rows rather than bare spans so the separators, the
   mark and the text share one baseline however the row wraps. */
.footer-identity, .footer-meta {
  display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; min-width: 0;
}

/* Intrinsic ratio 994.1 : 828.5; the width/height attributes on the element
   match, so the row does not reflow when the mark finishes loading. */
.footer-mark { display: block; width: 22px; height: 18px; flex: 0 0 auto; }

/* The product is the one thing down here allowed to look like a name. It is
   set in the display face at body size — Montserrat's voice, not its scale,
   because a footer that shouts is a footer nobody reads past. */
.footer-product { font-family: var(--font-display); font-weight: 600; color: var(--text-body); }

/* A drawn rule rather than a "·" character: a punctuation separator is read
   aloud by some screen readers as "middle dot", and this one means nothing
   at all. `aria-hidden` on an empty element is belt and braces. */
.footer-rule { width: 1px; height: 14px; background: var(--border-strong); flex: 0 0 auto; }

.footer-org { color: var(--text-muted); }

/* The copyright is the quietest thing in the footer on purpose. It has to be
   PRESENT, legible and selectable; it does not have to compete with the
   hosting region, which is the only line here an administrator ever needs to
   read in a hurry. --text-muted on --bg clears AA for this size. */
.footer-copyright { color: var(--text-muted); font-size: var(--text-xs); }

.footer-region:empty { display: none; }

/* Underlined on hover only, so the footer reads as one quiet line at rest
   and the link still announces itself as a link on approach. Focus is the
   console's standard ring — see the global :focus-visible rule. */
/* THE THEME CONTROL (P2-2). A segmented control rather than three loose
   buttons: one outline round the set says "pick one of these", which three
   outlines do not. Every colour is a token, which is the point — this control
   has to look deliberate in the theme it is switching TO as well as the one
   it is switching FROM, and it gets that for free only by never naming a
   colour. The `button` reset is needed because the console's base `button`
   rule paints the brand gradient. */
.theme-control {
  display: inline-flex; align-items: center; gap: 2px;
  border: 1px solid var(--border); border-radius: var(--radius-pill); padding: 2px;
}
.theme-control button {
  background: none; border: none; padding: 2px 10px; border-radius: var(--radius-pill);
  font-size: var(--text-xs); font-weight: 600; color: var(--text-muted);
}
.theme-control button:hover { filter: none; color: var(--text); background: var(--accent-wash); }
.theme-control button[aria-pressed="true"] { color: var(--text); background: var(--accent-wash); }
.theme-control button[aria-pressed="true"]:hover { background: var(--accent-wash); }
.theme-control button:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }

.footer-link { color: var(--text-muted); text-decoration: none; font-size: var(--text-xs); }
.footer-link:hover { color: var(--link); text-decoration: underline; }

/* --- Responsive. The console is usable at ~900px and does not break at
       phone width: cards stack, the nav wraps, tables scroll horizontally
       inside their own container rather than widening the page. ----------- */
@media (max-width: 900px) {
  .grid.two { grid-template-columns: 1fr; }
  main { padding-left: var(--space-4); padding-right: var(--space-4); }
  .app-header, .nav { padding-left: var(--space-4); padding-right: var(--space-4); }
  footer { padding-left: var(--space-4); padding-right: var(--space-4); }
  .app-header .spacer { flex-basis: 100%; height: 0; }
}
@media (max-width: 560px) {
  .kpi .num { font-size: var(--text-xl); }
  dl { grid-template-columns: 1fr; gap: 0 0; }
  dt { margin-top: var(--space-2); }
}

/* The signed-out card is the product's first impression, so the mark leads it. */
.signed-out-mark { display: block; height: 40px; width: auto; margin: 0 auto var(--space-5); }

/* --- Device-configuration authoring (G-01) -------------------------------
   The form sits under the read-only facts view in the Models section, so it
   needs a visible seam: a rule and a heading, not another card. Every style
   is here rather than on the element, because the console is served under
   `style-src 'self'` with no 'unsafe-inline'. */
.authoring {
  margin-top: var(--space-4);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border);
}
.authoring h3 { margin: 0 0 var(--space-2); font-size: var(--text-md); }
.authoring fieldset {
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: var(--space-3);
  max-width: 46rem;
}
.authoring legend {
  padding: 0 var(--space-2);
  color: var(--text-muted);
  font-size: var(--text-sm);
  font-weight: 600;
}
.authoring dl { margin: 0; }
.mcp-row {
  align-items: flex-end;
  margin-bottom: var(--space-3);
  padding-bottom: var(--space-3);
  border-bottom: 1px dashed var(--border);
}
.mcp-row:last-of-type { border-bottom: 0; padding-bottom: 0; }
.mcp-row .field { max-width: 16rem; }
.mcp-row label { display: flex; flex-direction: column; gap: var(--space-1); }

/* Workstation detail (G-11): the owner's effective capability set. A list
   rather than a table — it is usually two or three rows, and a table of two
   rows reads as a report when it should read as an answer. */
.capability-list {
  list-style: none;
  margin: 0 0 var(--space-4) 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.capability-list li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg-alt);
}

/* G-18: four declarations that used to be inline `style` attributes in
   index.html, where they were DEAD — this console's CSP is `style-src 'self'`
   with no `'unsafe-inline'`, so the browser dropped all four. They are moved
   here rather than deleted because each states a real intent, and here they
   take effect for the first time. The visible change is small and deliberate:
   the directory list loses the UA's 40px list indent, and three blocks gain
   the separation they were always asking for.

   `margin-top:22px` is exactly --space-5. The credential form's 18px fell
   between two steps of the scale and is snapped to --space-4 rather than
   carried in as a literal: an off-scale value in a tokenised sheet is how the
   scale stops being one. */
.spaced { margin-top: var(--space-5); }
#credential-form.spaced { margin-top: var(--space-4); }

/* `.actions` is already display:flex, so the marker never rendered — but the
   UA's list padding did, indenting the whole row. */
#directory-results { list-style: none; padding: 0; }

/* --- Context block and setup guide (the credential card's way out) --------
   Two new blocks in the Model credential card, both of them answering the
   same question: "the model list is empty — now what?". They are styled from
   the existing tokens only; nothing here is a new colour. */
.context-block {
  margin-top: var(--space-4);
  padding: var(--space-4);
  border: 1px solid var(--border);
  border-left: 3px solid var(--border-strong);
  border-radius: var(--radius-md);
  background: var(--bg-alt);
}
.context-block h3 { margin: 0 0 var(--space-3); font-size: var(--text-sm); text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-muted); }
.context-block .hint { margin-bottom: 0; }

/* The empty state is centred text, so the button it now carries has to be
   centred too — `.actions` is flex and would otherwise hang it off the left
   edge of a block whose every other line is centred. */
.empty .actions { justify-content: center; margin-top: var(--space-4); }

.guide { margin-top: var(--space-4); }
.guide > summary {
  cursor: pointer;
  color: var(--text-body);
  font-size: var(--text-sm);
  font-weight: 600;
  padding: var(--space-2) 0;
}
.guide-steps { margin: var(--space-2) 0 0; padding-left: var(--space-5); color: var(--text-muted); font-size: var(--text-sm); }
.guide-steps li { margin-bottom: var(--space-2); }
.guide-steps strong { color: var(--text); font-weight: 600; }
