/* Foundry — light glass UI: fixed top bar + left sidebar over a soft mesh canvas.
   ONE file, no build step. Layers, in cascade order:
     1 font + tokens   2 base   3 shell   4 components (A-Z)   5 utilities
     6 auth screens    7 responsive + preference media queries
   Breakpoints are 560 / 880 (1200 reserved) — custom properties cannot be used in a media
   query, so they are written out literally in layer 7 and nowhere else.
   Legacy spellings (.btn.primary, .btn.auto, .btn.danger, .grid, .grid2 ...) are kept as aliases
   next to their replacement so unmigrated templates render unchanged. */

/* ================= 1. FONT + TOKENS ================= */
/* Self-hosted: an admin console makes no third-party requests. Relative url so it resolves
   under any static path. */
@font-face {
  font-family: "Inter"; font-style: normal; font-weight: 100 900; font-display: swap;
  src: url("../fonts/inter-variable.woff2") format("woff2");
}

:root {
  /* slate scale — only the steps in use */
  --slate-50: #f8fafc; --slate-100: #f1f5f9; --slate-200: #e2e8f0; --slate-300: #cbd5e1;
  --slate-400: #94a3b8; --slate-700: #334155; --slate-900: #0f172a;

  /* surfaces + text */
  --bg: #f5f7fa; --panel: #ffffff; --panel-2: #f7f9fb;
  --line: var(--slate-200);
  --line-strong: #7b8594;          /* control boundaries: >= 3:1 on every surface incl. mesh peaks */
  --ink: var(--slate-900); --ink-2: var(--slate-700);
  --muted: #55606e;                /* AA on --bg, --panel, --panel-2 and the mesh peaks */

  /* brand: --brand is the FILL (borders, rings, accents) and fails AA as text (3.74:1).
     --brand-strong is the text-grade teal and the primary button background. */
  --brand: #0d9488; --brand-strong: #0f766e; --brand-deep: #115e59;
  --brand-ink: #ffffff; --brand-soft: #e6f4f2;

  /* semantic: bare name = fill/border/icon, -fg = AA text, -bg/-bd = tinted surface */
  --ok: #0c8a5d;      --ok-fg: #0b6b47;      --ok-bg: #e7f6ef;      --ok-bd: #b7e4cd;
  --warn: #b7791f;    --warn-fg: #7a5208;    --warn-bg: #fdf4e3;    --warn-bd: #f0d9a8;
  --danger: #dc2626;  --danger-fg: #b91c1c;  --danger-bg: #fdecec;  --danger-bd: #f4c2c2;
  --danger-deep: #991b1b;
  --info: #0284c7;    --info-fg: #0c5f80;    --info-bg: #e6f3fb;    --info-bd: #b9dcf2;
  --neutral-fg: var(--muted); --neutral-bg: var(--slate-100); --neutral-bd: var(--slate-300);

  /* spacing (4px grid) */
  --sp-1: .25rem; --sp-2: .5rem; --sp-3: .75rem; --sp-4: 1rem; --sp-5: 1.25rem; --sp-6: 1.5rem;
  --sp-7: 2rem; --sp-8: 3rem;

  /* type — rem is the 16px root, NOT the 14.5px body size */
  --font: "Inter", -apple-system, "Segoe UI", Roboto, system-ui, sans-serif;
  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --fs-xs: .75rem; --fs-sm: .8125rem; --fs-base: .90625rem; --fs-md: 1rem; --fs-lg: 1.125rem;
  --fs-xl: 1.4rem; --fs-2xl: 1.75rem;

  /* shape + depth */
  --r-sm: 6px; --r-md: 8px; --r-lg: 14px; --r-pill: 999px;
  --e1: 0 1px 2px rgba(15,23,42,.05), 0 1px 3px rgba(15,23,42,.07);
  --e2: 0 4px 12px rgba(15,23,42,.10), 0 2px 4px rgba(15,23,42,.06);
  --e3: 0 1px 2px rgba(15,23,42,.04), 0 12px 32px -12px rgba(15,23,42,.18);
  --hilite: inset 0 1px 0 rgba(255,255,255,.8);      /* glass top edge */
  --ring: 0 0 0 4px rgba(13,148,136,.22);            /* soft halo; the solid outline carries contrast */
  --ease: cubic-bezier(0.16, 1, 0.3, 1);

  /* glass — OPAQUE by default; the @supports block below turns it translucent only where
     backdrop-filter exists, and layer 7 turns it back for reduced-transparency / more-contrast. */
  --glass: var(--panel);            /* cards, tiles */
  --glass-strong: var(--panel);     /* tables, toasts, modal: dense text */
  --chrome: var(--panel);           /* topbar + sidebar */
  --glass-bd: var(--line);
  --glass-fx: none;

  /* layout */
  --topbar-h: 56px; --side-w: 236px;
  --console-bg: var(--slate-900); --console-fg: var(--slate-200);
}

/* Alpha stays >= .86 wherever text sits: worst-case composite over the mesh keeps --muted at
   6.1:1 and --brand-strong at 5.2:1 (table in the Phase 0 notes). */
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  :root {
    --glass: rgba(255,255,255,.86); --glass-strong: rgba(255,255,255,.94);
    --chrome: rgba(255,255,255,.88); --glass-bd: rgba(255,255,255,.9);
    --glass-fx: blur(14px) saturate(160%);
  }
}

/* ================= 2. BASE ================= */
*, *::before, *::after { box-sizing: border-box; }
html, body { height: 100%; }
body {
  margin: 0; background: var(--bg); color: var(--ink);
  font: 14.5px/1.55 var(--font);
  -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility;
}
/* The canvas is a fixed pseudo-element, not background-attachment:fixed — the latter repaints
   the whole page on scroll under blurred layers. Low-alpha blobs anchored at different corners:
   no point of the canvas is darker than a single blob's peak (#e0eef0). */
body::before {
  content: ""; position: fixed; inset: 0; z-index: -1; pointer-events: none;
  background:
    radial-gradient(60rem 36rem at 0% 0%, rgba(13,148,136,.09), transparent 60%),
    radial-gradient(56rem 34rem at 100% 0%, rgba(14,165,233,.08), transparent 60%),
    radial-gradient(52rem 32rem at 100% 100%, rgba(99,102,241,.06), transparent 60%),
    radial-gradient(48rem 30rem at 0% 100%, rgba(16,185,129,.05), transparent 60%);
}
/* Form controls do not inherit the font by default — buttons were rendering in the UA face. */
button, input, select, textarea { font-family: inherit; }
/* The hidden attribute loses to ANY author display rule (.input, .chk, .pill all set one), and
   the JS-enhanced components toggle it on exactly those. One of two !important in the file
   (the other is the reduced-motion guard): "hidden" must mean hidden. */
[hidden] { display: none !important; }

h1 { font-size: var(--fs-xl); font-weight: 650; letter-spacing: -.015em; line-height: 1.25; }
h2 { font-size: var(--fs-md); font-weight: 600; letter-spacing: -.01em; margin: 1rem 0 .5rem; }
h3 { font-size: var(--fs-base); font-weight: 600; margin: 1rem 0 .4rem; }
a { color: var(--brand-strong); text-decoration: none; }
a:hover { text-decoration: underline; }
code { background: var(--panel-2); border: 1px solid var(--line); padding: .05em .4em; border-radius: var(--r-sm); font-family: var(--mono); font-size: .9em; }
::selection { background: rgba(13,148,136,.22); }

/* One focus ring for everything focusable. :where() keeps specificity at zero so a component
   can move the offset (never remove the ring). Inputs and buttons add the --ring halo. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--brand); outline-offset: 2px;
}

/* ================= 3. SHELL ================= */
/* ---------- top bar ---------- */
.topbar {
  position: fixed; inset: 0 0 auto 0; height: var(--topbar-h); z-index: 40;
  display: flex; align-items: center; gap: .5rem; padding: 0 1rem;
  background: var(--chrome); border-bottom: 1px solid var(--line);
  -webkit-backdrop-filter: var(--glass-fx); backdrop-filter: var(--glass-fx);
}
.hamburger { display: none; background: none; border: 0; padding: .35rem; color: var(--ink); cursor: pointer; border-radius: var(--r-sm); }
.hamburger svg { width: 22px; height: 22px; }
.topright { margin-left: auto; display: flex; align-items: center; gap: 1rem; }
.logo { display: flex; align-items: center; }
.logo:hover { text-decoration: none; }
/* logo image is height-capped to the top bar; width scales to keep aspect ratio */
.logo-img { height: 26px; width: auto; display: block; }
.welcome { color: var(--muted); font-size: var(--fs-base); }
.welcome b { color: var(--ink); font-weight: 600; }
/* Current page name, phones only (layer 7 shows it): with the sidebar off-canvas the bar is
   the only place left that says where you are. */
.topbar-title { display: none; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
.profile { position: relative; }

/* Hidden until focused, then above the top bar. First tab stop on every page. */
.skip-link, .skip {
  position: fixed; left: .75rem; top: .6rem; z-index: 70; padding: .45rem .9rem;
  background: var(--brand-strong); color: var(--brand-ink); border-radius: var(--r-pill);
  font-weight: 600; transform: translateY(-300%);
}
.skip-link:focus, .skip:focus { transform: none; text-decoration: none; }

/* ---------- sidebar ---------- */
.shell { display: flex; padding-top: var(--topbar-h); min-height: 100%; }
.sidebar {
  position: fixed; top: var(--topbar-h); bottom: 0; left: 0; width: var(--side-w); z-index: 30;
  background: var(--chrome); border-right: 1px solid var(--line);
  -webkit-backdrop-filter: var(--glass-fx); backdrop-filter: var(--glass-fx);
  display: flex; flex-direction: column;
}
.search { padding: .8rem .8rem .4rem; position: relative; }
.search svg { position: absolute; left: 1.35rem; top: 1.28rem; width: 16px; height: 16px; color: var(--muted); }
.search input {
  width: 100%; padding: .5rem .6rem .5rem 2.1rem; border: 1px solid var(--line-strong); border-radius: var(--r-md);
  background: var(--panel); color: var(--ink); font-size: var(--fs-base);
}
.sidenav { flex: 1; overflow-y: auto; padding: .4rem .6rem 1rem; scrollbar-width: thin; scrollbar-color: var(--slate-300) transparent; }
.nav-item {
  display: flex; align-items: center; gap: .65rem; padding: .5rem .65rem; margin-bottom: 2px;
  border-radius: var(--r-md); color: var(--ink-2); font-size: var(--fs-base); font-weight: 500;
}
.nav-item:hover { background: var(--slate-100); text-decoration: none; color: var(--ink); }
/* [aria-current] is the real state; .active stays for the unmigrated macro. */
.nav-item.active, .nav-item[aria-current="page"] { background: var(--brand-soft); color: var(--brand-strong); font-weight: 600; }
.nav-item:focus-visible { outline-offset: -2px; }
.nav-ico { display: inline-grid; place-items: center; }
.nav-ico svg { width: 18px; height: 18px; }
.nav-group { margin-top: .8rem; }
/* A child of the item above it. Indented to the parent's label rather than its icon,
   so the two read as a group without needing a second heading over a one-item list. */
.nav-item.nav-sub { padding-left: 2.35rem; }
.nav-item.nav-sub .nav-ico { width: 1rem; height: 1rem; opacity: .75; }
.nav-group-label { padding: .3rem .7rem; font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: .07em; color: var(--muted); }
.nav-empty { padding: .8rem .7rem; color: var(--muted); font-size: var(--fs-base); }
.sidebar-foot { padding: .7rem 1rem; border-top: 1px solid var(--line); color: var(--muted); font-size: var(--fs-sm); }
.nav-scrim { display: none; }

/* ---------- content ---------- */
/* 1120 was costing real width on an ordinary 1080p-and-up screen: the releases table wrapped
   the filename, the date and the requirement onto two lines each while ~230px of the window sat
   empty beside it. This is an admin console of dense tables, not an article. 1600 is still a cap
   — it just sits beyond any normal laptop, so those screens fill, and an ultrawide is spared a
   table stretched across a metre of glass. */
/* `min-width: 0` is load-bearing. A flex item defaults to min-width:auto, so it grows to fit its
   widest child — which meant `.grid.wide`'s minimum pushed the whole PAGE into horizontal scroll
   instead of letting the table's own card scroll. Measured: content 1290 inside a 1335 viewport,
   overhanging by 191px. With this, the card scrolls and the page never does. */
.content { flex: 1; min-width: 0; margin-left: var(--side-w); padding: 1.5rem; max-width: 1600px; }
/* Prose does NOT get the extra room. A table earns width; a paragraph read at 200 characters a
   line does not, and the page-intro paragraphs are the only long text here. */
.content > p { max-width: 92ch; }
.content > p.text-center { margin-inline: auto; }   /* centre the 92ch box, not just its text */
/* <main tabindex="-1"> is only a skip-link target; a ring round the whole page is noise. */
.content:focus, .auth-main:focus { outline: none; }
.centered { display: grid; place-items: center; min-height: 100vh; }
.auth-main { width: 100%; }

/* ================= 4. COMPONENTS (A-Z) ================= */
/* ---------- actions ---------- */
.actions { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; }
/* form.inline replaces style="display:inline" exactly (a form in a <td> or beside a span keeps
   flowing inline). Directly inside .actions the form box is dropped so its button is the flex
   item — scoped to the child combinator: a blanket rule would re-lay-out forms in plain cards. */
form.inline { display: inline; }
.actions > form { display: contents; }

/* ---------- addform (disclosure that holds an inline create form) ---------- */
.addform { margin-top: 1rem; }
.addform summary { cursor: pointer; color: var(--brand-strong); }
.addform form { margin-top: .75rem; }

/* ---------- avatar ---------- */
.avatar {
  width: 36px; height: 36px; border-radius: 50%; border: 0; flex: none;
  background: var(--brand-strong); color: var(--brand-ink); font-weight: 700; font-size: var(--fs-sm);
  letter-spacing: .03em; display: inline-grid; place-items: center;
}
button.avatar { position: relative; cursor: pointer; }
.avatar-sm { width: 28px; height: 28px; font-size: var(--fs-xs); }
.avatar-lg { width: 48px; height: 48px; font-size: var(--fs-md); }
/* Eight soft pairs picked from the name (ui.avatar_tone) so people are told apart in a list.
   Every pair is dark-on-light at >= 6:1; the solid brand disc stays the account button. */
.avatar-c0 { background: #d5f0ec; color: #0f5f59; }
.avatar-c1 { background: #dbeafe; color: #1e4a8a; }
.avatar-c2 { background: #e0e2fd; color: #3a3a96; }
.avatar-c3 { background: #ede1fb; color: #5b2f91; }
.avatar-c4 { background: #fde2ea; color: #8f2348; }
.avatar-c5 { background: #fbebc8; color: #744a0b; }
.avatar-c6 { background: #dcf3d6; color: #2b5f23; }
.avatar-c7 { background: var(--slate-200); color: var(--slate-700); }
/* Account-security nudge on the top-bar avatar. The button's aria-label says it in words. */
.avatar-dot { position: absolute; top: -1px; right: -1px; width: 11px; height: 11px; border-radius: 50%; background: var(--warn); border: 2px solid var(--panel); }

/* ---------- breadcrumbs ---------- */
/* Sits on the bare canvas, so links use --muted (5.4:1 at the mesh peak) rather than teal. */
.crumbs ol { display: flex; flex-wrap: wrap; gap: .35rem; list-style: none; padding: 0; margin: 0 0 .35rem; font-size: var(--fs-sm); color: var(--muted); }
.crumbs li + li::before { content: "/"; margin-right: .35rem; color: var(--slate-400); }
.crumbs a { color: var(--muted); }
.crumbs a:hover { color: var(--brand-strong); }
.crumbs [aria-current="page"] { color: var(--ink); font-weight: 500; }

/* ---------- buttons ---------- */
/* --btn-fg carries the label colour so the busy spinner can reuse it once the label itself is
   made transparent. No hover lift on buttons: lift is reserved for a.tile. */
.btn {
  --btn-fg: var(--ink);
  display: inline-flex; align-items: center; justify-content: center; gap: .4rem;
  min-height: 36px; padding: .4rem .9rem; border-radius: var(--r-md); border: 1px solid var(--slate-300);
  background: var(--panel); color: var(--btn-fg); cursor: pointer; text-align: center;
  font-size: var(--fs-base); font-weight: 500; line-height: 1.25; box-shadow: var(--e1);
  transition: background-color .15s var(--ease), border-color .15s var(--ease), box-shadow .15s var(--ease);
}
.btn:hover { background: var(--slate-50); border-color: var(--slate-400); text-decoration: none; }
.btn:active { transform: translateY(1px); }
.btn:focus-visible { box-shadow: var(--ring); }
.btn-primary, .btn.primary {
  --btn-fg: var(--brand-ink); background: var(--brand-strong); border-color: var(--brand-strong);
  box-shadow: var(--e1), inset 0 1px 0 rgba(255,255,255,.18);
}
.btn-primary:hover, .btn.primary:hover { background: var(--brand-deep); border-color: var(--brand-deep); }
/* The default look IS secondary; the class exists so markup can say so next to .btn-primary. */
.btn-secondary { --btn-fg: var(--ink); }
.btn-ghost { background: transparent; border-color: transparent; box-shadow: none; }
.btn-ghost:hover { background: var(--slate-100); border-color: transparent; }
.btn-danger, .btn.danger { --btn-fg: var(--danger-fg); border-color: var(--danger); }
.btn-danger:hover, .btn.danger:hover { background: var(--danger-bg); border-color: var(--danger-fg); }
/* Solid red is for the confirm dialog's final action only — never a page-level button. */
.btn-danger-solid { --btn-fg: var(--brand-ink); background: var(--danger-fg); border-color: var(--danger-fg); }
.btn-danger-solid:hover { background: var(--danger-deep); border-color: var(--danger-deep); }
.btn-sm { min-height: 28px; padding: .2rem .6rem; font-size: var(--fs-sm); border-radius: var(--r-sm); }
.btn-lg { min-height: 44px; padding: .6rem 1.2rem; font-size: var(--fs-md); }
.btn-icon { width: 36px; padding: 0; }
.btn-icon.btn-sm { width: 28px; }
.btn-block { width: 100%; }
/* LEGACY: `.btn.primary` was full-width by default and `.btn.auto` undid it. Both spellings keep
   that width so auth cards and unmigrated forms do not shift. New `.btn-primary` is auto-width;
   ask for `.btn-block`. `.btn.auto` must stay AFTER `.btn.primary` (equal specificity). */
.btn.primary { width: 100%; }
.btn.auto { width: auto; }
.btn:disabled, .btn[aria-disabled="true"] { opacity: .55; cursor: not-allowed; box-shadow: none; }
a.btn[aria-disabled="true"] { pointer-events: none; }
/* Busy is a CLASS, never the disabled attribute: a disabled submit drops its name/value from
   the POST. pointer-events blocks the double-submit instead. */
.btn.is-busy, .btn[aria-busy="true"] { position: relative; color: transparent; pointer-events: none; }
.btn.is-busy::after, .btn[aria-busy="true"]::after {
  content: ""; position: absolute; inset: 0; margin: auto; width: 1em; height: 1em; border-radius: 50%;
  border: 2px solid var(--btn-fg); border-right-color: transparent; animation: spin .7s linear infinite;
}
/* <input type=submit> has no pseudo-elements: keep its label, just dim it. */
input.btn.is-busy, input.btn[aria-busy="true"] { color: var(--btn-fg); opacity: .6; }
.linkbtn { position: relative; background: none; border: 0; color: var(--muted); cursor: pointer; font-size: var(--fs-base); padding: 0 .25rem; border-radius: var(--r-sm); }
.linkbtn:hover { color: var(--brand-strong); }
/* ~20px of text is under the 24px target minimum. The pseudo-element grows the HIT area only,
   so rows and inline runs keep their layout. */
.linkbtn::after { content: ""; position: absolute; inset: -7px -2px; }
.linkbtn.danger { color: var(--danger-fg); }
.linkbtn.is-busy { opacity: .55; pointer-events: none; }
@keyframes spin { to { transform: rotate(360deg); } }

/* ---------- callout ---------- */
/* Standalone block — do not combine with .card (card's surface would win the cascade).
   Same tone semantics as toasts: a 3px bar plus a tinted surface; body text stays --ink. */
.callout {
  padding: .8rem 1rem; margin-bottom: 1rem; border: 1px solid var(--neutral-bd); border-left: 3px solid var(--muted);
  border-radius: var(--r-md); background: var(--neutral-bg);
}
.callout > :first-child { margin-top: 0; }
.callout > :last-child { margin-bottom: 0; }
.callout-title { display: block; font-weight: 600; margin-bottom: .2rem; }
.callout-info { background: var(--info-bg); border-color: var(--info-bd); border-left-color: var(--info); }
.callout-ok { background: var(--ok-bg); border-color: var(--ok-bd); border-left-color: var(--ok); }
.callout-warn { background: var(--warn-bg); border-color: var(--warn-bd); border-left-color: var(--warn); }
.callout-danger { background: var(--danger-bg); border-color: var(--danger-bd); border-left-color: var(--danger); }

/* ---------- cards ---------- */
/* Static cards never lift and never change on hover — only a.tile is interactive. */
.card {
  background: var(--glass); border: 1px solid var(--glass-bd); border-radius: var(--r-lg);
  padding: 1.25rem; margin-bottom: 1rem; box-shadow: var(--e1), var(--hilite), 0 0 0 1px rgba(15,23,42,.04);
  -webkit-backdrop-filter: var(--glass-fx); backdrop-filter: var(--glass-fx);
  min-width: 0; /* a grid-item card must shrink to its column so a .scroll-x table scrolls inside it */
}
.card.narrow { max-width: 420px; margin: 6vh auto; }
/* overflow-x:auto, not hidden: a wide table on a narrow screen must scroll, never be clipped
   away. Still clips the table's corners against the card radius, which is what hidden was for.
   Near-opaque and NOT blurred: blur behind a 300-row table costs scroll performance and
   legibility for nothing. */
.card.nopad { padding: 0; overflow-x: auto; background: var(--glass-strong); -webkit-backdrop-filter: none; backdrop-filter: none; }
/* The ring would be clipped by the overflow above, so it is drawn inside the element. Buttons
   are the exception: they sit inside cell padding (never clipped), and an inset teal ring on a
   filled teal/red button is invisible. */
.card.nopad :focus-visible { outline-offset: -2px; }
.card.nopad .btn:focus-visible { outline-offset: 2px; }
.cardhead { padding: .8rem 1.1rem; border-bottom: 1px solid var(--line); }
.cardbody { padding: 1rem 1.1rem; margin: 0; }          /* prose inside a .card.nopad */
.form-sm { max-width: 520px; }
.form-md { max-width: 680px; }

/* ---------- checklist (multi-select as checkboxes) ---------- */
/* _forms.html checklist(): same posted name/values as the <select multiple> it replaces. */
.checklist-bar { display: flex; align-items: center; flex-wrap: wrap; gap: .4rem .75rem; margin-top: .3rem; }
/* .input.… : the plain .input rule is later in the file and would win on all three. */
.input.checklist-filter { margin-top: 0; max-width: 260px; padding: .35rem .6rem; }
.checklist {
  list-style: none; margin: .4rem 0 0; padding: .35rem .5rem; max-width: 720px; max-height: 300px; overflow: auto;
  columns: 2 16rem; column-gap: 1rem;
  border: 1px solid var(--line-strong); border-radius: var(--r-md); background: var(--panel);
  scrollbar-width: thin; scrollbar-color: var(--slate-300) transparent;
}
.checklist li { break-inside: avoid; }
/* nowrap + a shrinkable label: with flex-wrap a long name dropped WHOLE onto the line under
   its checkbox. The text wraps beside the box instead. */
.checklist .chk { margin: 0; min-height: 32px; padding: .25rem .3rem; border-radius: var(--r-sm); cursor: pointer; flex-wrap: nowrap; align-items: flex-start; }
.checklist .chk input { flex: none; margin-top: .25rem; }
.checklist .chk > span { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
.checklist .chk:hover { background: var(--slate-100); }
.checklist-field.is-invalid .checklist { border-color: var(--danger); box-shadow: 0 0 0 3px rgba(220,38,38,.14); }
.checklist-empty { margin: .3rem 0 0; }

/* ---------- console (log <pre>) ---------- */
.console {
  margin: 0; padding: 1rem; max-height: 60vh; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere;
  font: var(--fs-sm)/1.45 var(--mono); background: var(--console-bg); color: var(--console-fg);
  scrollbar-width: thin; scrollbar-color: var(--slate-400) transparent;
}
.console-sm { max-height: 50vh; }
.card > .console { border-radius: var(--r-md); }

/* ---------- dropdown ---------- */
/* Generic: [data-dropdown] wraps a [data-dropdown-toggle] and a .dropdown[data-dropdown-menu].
   Opaque, not glass: it lives inside the blurred top bar, and a nested backdrop-filter cannot
   see the page behind its parent — it would be translucent with nothing blurred. No
   overflow:hidden either; that clipped the focus ring of its items.
   Inside a .card/.tile the menu's z-index only counts within that surface: backdrop-filter
   makes each one a stacking context, so the NEXT card would paint over the menu. The :has()
   rule lifts the owning surface while its menu is open (app.js keeps aria-expanded in sync).
   Still not for .card.nopad — that one clips. */
[data-dropdown] { position: relative; }
.card:has([data-dropdown-toggle][aria-expanded="true"]),
.tile:has([data-dropdown-toggle][aria-expanded="true"]) { position: relative; z-index: 1; }
.dropdown {
  position: absolute; right: 0; top: calc(100% + 10px); min-width: 190px; z-index: 45;
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-md); box-shadow: var(--e2); padding: .3rem;
}
.dropdown-start { left: 0; right: auto; }
.dropdown-head { padding: .5rem .6rem; border-bottom: 1px solid var(--line); margin-bottom: .3rem; }
.dropdown-name { display: block; overflow: hidden; text-overflow: ellipsis; font-weight: 600; }
.dropdown-role { font-size: var(--fs-sm); color: var(--muted); text-transform: capitalize; }
.dropdown a, .dropdown button {
  display: block; width: 100%; padding: .5rem .6rem; border: 0; border-radius: var(--r-sm); background: none;
  color: var(--ink); font-size: var(--fs-base); text-align: left; cursor: pointer;
}
.dropdown a:hover, .dropdown button:hover { background: var(--slate-100); text-decoration: none; color: var(--brand-strong); }
.dropdown a:focus-visible, .dropdown button:focus-visible { outline-offset: -2px; }
.dropdown-sep { height: 1px; margin: .3rem 0; background: var(--line); }
[data-dropdown-toggle][aria-expanded="true"] { box-shadow: var(--ring); }
.dropdown form { margin: 0; }
.dropdown .icon { margin-right: .3rem; color: var(--muted); }
/* Destructive items come LAST, after a .dropdown-sep, and say so in colour AND position. */
.dropdown .dropdown-danger, .dropdown .dropdown-danger .icon { color: var(--danger-fg); }
.dropdown .dropdown-danger:hover { background: var(--danger-bg); color: var(--danger-fg); }
.dropdown .dropdown-warn, .dropdown .dropdown-warn .icon { color: var(--warn-fg); }
/* Account menu head: avatar beside name + role. */
.dropdown-account { min-width: 230px; }
.dropdown-account .dropdown-head { display: flex; align-items: center; gap: .6rem; }
.dropdown-id { min-width: 0; }
/* app.js places this one with viewport coordinates (row menus): the table card's overflow
   cannot clip a fixed box. left/top are set inline on open. */
.dropdown-fixed { position: fixed; right: auto; top: 0; left: 0; }

/* ---------- empty state ---------- */
.empty-state { display: grid; justify-items: center; gap: .4rem; padding: var(--sp-7) var(--sp-4); text-align: center; }
.empty-state-icon { display: grid; place-items: center; width: 48px; height: 48px; margin-bottom: .3rem; border-radius: 50%; background: var(--brand-soft); color: var(--brand-strong); }
.empty-state-icon svg { width: 24px; height: 24px; }
.empty-state-title { margin: 0; font-size: var(--fs-md); font-weight: 600; }
.empty-state-body { margin: 0; max-width: 60ch; color: var(--muted); }
.empty-state .actions { margin-top: .5rem; justify-content: center; }
/* The same block as a full-width row of a table that keeps its header when empty. */
.empty-row > td { padding: 0; }
.grid tbody tr.empty-row:hover, .table tbody tr.empty-row:hover { background: none; }

/* ---------- forms ---------- */
/* LEGACY label: templates still nest the control inside <label>. The shared macro emits
   .field > label + control instead; both must render. */
label { display: block; text-align: left; margin: .7rem 0; font-size: var(--fs-sm); color: var(--muted); }
.field { margin: .7rem 0; }
.field > label { margin: 0; color: var(--ink); font-weight: 600; }
.req { margin-left: .15em; color: var(--danger-fg); font-weight: 600; }
.input {
  /* Width came free with the wider `.content`, and a field does not want it: a single-line box
     a metre long for "1.7.2" reads as unfinished, and the eye loses the line. Capped, not
     shrunk — the `.grid2` two-column rows sit under this already, so only fields that would
     otherwise span the whole page are affected. */
  display: block; width: 100%; max-width: 720px;
  margin-top: .3rem; padding: .55rem .7rem; background: var(--panel);
  border: 1px solid var(--line-strong); border-radius: var(--r-md); color: var(--ink);
  font-size: var(--fs-base); font-weight: 400; line-height: 1.4;
  transition: border-color .15s var(--ease), box-shadow .15s var(--ease);
}
.input::placeholder, .search input::placeholder { color: var(--muted); opacity: 1; }
/* Solid ring carries the contrast, the halo keeps the glass feel. */
.input:focus, .search input:focus { outline: 2px solid var(--brand); outline-offset: -1px; border-color: var(--brand); box-shadow: var(--ring); }
.input:disabled, .input[readonly] { background: var(--slate-100); color: var(--muted); }
.input[aria-invalid="true"], .input.is-invalid, .field.is-invalid .input, .has-error .input {
  border-color: var(--danger); box-shadow: 0 0 0 3px rgba(220,38,38,.14);
}
.help, .hint { display: block; margin-top: .25rem; color: var(--muted); font-size: var(--fs-xs); font-weight: 400; }
.err { display: block; color: var(--danger-fg); font-size: var(--fs-sm); margin-top: .2rem; }
/* .err is display:block; on a cell that tore it out of the table layout. */
td.err, th.err { display: table-cell; margin-top: 0; font-size: inherit; }
/* Not colour alone: macro-rendered errors get a glyph. Scoped to .field — .err is also used on
   bare <p>/<dd> where a badge would be wrong. */
.field .err::before {
  content: "!"; display: inline-grid; place-items: center; width: 1.15em; height: 1.15em; margin-right: .35em;
  border-radius: 50%; background: var(--danger-fg); color: var(--brand-ink); font-size: .8em; font-weight: 700;
}
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0 1rem; }
.cols { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
.row { display: flex; gap: .6rem; align-items: center; flex-wrap: wrap; }
.row .input { flex: 1; }
.chk { display: flex; align-items: center; gap: .4rem; color: var(--ink); font-size: var(--fs-base); }
.chk input { accent-color: var(--brand-strong); }

/* ---- added by shell pass ---- */
/* radios() in _forms.html: a fieldset/legend names the group; strip the UA box so it lays
   out like any other .field, and give the legend the .field > label look. */
fieldset.field { min-width: 0; padding: 0; border: 0; }
fieldset.field > legend { padding: 0; font-size: var(--fs-sm); font-weight: 600; color: var(--ink); }
fieldset.field .chk { margin: .3rem 0; }
/* copyable() / ext_link() in _ui.html */
.copyable { display: inline-flex; align-items: center; gap: .3rem; max-width: 100%; }
.ext-link .icon { margin-left: .25em; }
/* ---- end shell pass ---- */

/* ---------- icon ---------- */
.icon { width: 18px; height: 18px; flex: none; vertical-align: -.2em; }
.icon-sm { width: 14px; height: 14px; }
.icon-lg { width: 24px; height: 24px; }

/* ---------- key/value ---------- */
.kv { display: grid; grid-template-columns: max-content 1fr; gap: .35rem 1rem; margin: 0; }
.kv dt { color: var(--muted); font-size: var(--fs-sm); }
/* min-width:0 lets the 1fr column shrink; an unbreakable value (a fingerprint, a path)
   otherwise widened the page past a phone's viewport. */
.kv dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }

/* ---------- meter (seat usage) ---------- */
/* The "3/5" text is the value; the bar is the glance. Tone is set by ui.seat_usage. */
.meter-cell { display: inline-flex; align-items: center; gap: .5rem; white-space: nowrap; }
.meter { display: inline-block; width: 56px; height: 6px; overflow: hidden; border-radius: var(--r-pill); background: var(--slate-200); }
.meter-fill { display: block; height: 100%; border-radius: inherit; background: var(--ok); }
.meter-warn .meter-fill { background: var(--warn); }
.meter-danger .meter-fill { background: var(--danger); }

/* ---------- modal (native <dialog class="modal">) ---------- */
/* showModal() gives the focus trap, Esc and the top layer. Tone goes on the dialog:
   .modal-danger for destruction, .modal-warn for secret reveal / key rotation. */
.modal {
  width: min(460px, calc(100vw - 2rem)); max-height: calc(100vh - 2rem); padding: 0; overflow: auto;
  border: 1px solid var(--glass-bd); border-radius: var(--r-lg); background: var(--glass-strong); color: var(--ink);
  box-shadow: var(--e3), var(--hilite);
  -webkit-backdrop-filter: var(--glass-fx); backdrop-filter: var(--glass-fx);
}
.modal[open] { animation: modal-in .2s var(--ease); }
.modal::backdrop { background: rgba(15,23,42,.4); -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px); }
.modal-head { display: flex; align-items: center; gap: .7rem; padding: 1.1rem 1.25rem .4rem; }
.modal-head h2 { margin: 0; font-size: var(--fs-lg); }
.modal-icon { display: grid; place-items: center; flex: none; width: 36px; height: 36px; border-radius: 50%; background: var(--info-bg); color: var(--info-fg); }
.modal-danger .modal-icon { background: var(--danger-bg); color: var(--danger-fg); }
.modal-warn .modal-icon { background: var(--warn-bg); color: var(--warn-fg); }
/* pre-line: consequence lists arrive as newline-separated text in a data attribute. */
.modal-body { padding: .4rem 1.25rem 1rem; color: var(--ink-2); white-space: pre-line; overflow-wrap: anywhere; }
/* Type-to-confirm row; JS un-hides it and keeps the action disabled until the value matches. */
.modal-type { padding: 0 1.25rem 1rem; }
.modal-type label { margin: 0; color: var(--ink); }
.modal-type .input { max-width: none; font-family: var(--mono); }
.modal-foot { display: flex; justify-content: flex-end; gap: .5rem; padding: .8rem 1.25rem; border-top: 1px solid var(--line); background: var(--panel-2); }
@keyframes modal-in { from { opacity: 0; transform: translateY(8px) scale(.98); } }

/* ---------- next steps (completion panel) ---------- */
.next-steps-list { list-style: none; display: grid; gap: .3rem; margin: .5rem 0 0; padding: 0; }
.next-steps-list a { font-weight: 600; }
.next-steps-list .icon { margin-left: .15rem; }

/* ---------- page head + subnav ---------- */
.pagehead { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: 1rem; flex-wrap: wrap; }
.pagehead h1, .pagehead h2 { margin: 0; }
.pagehead-tabs { min-height: 36px; }   /* = .btn: a head with no button is as tall as one with */
/* An h2 row inside a page (title + its buttons), so sections stop borrowing .pagehead. */
.sectionhead { display: flex; align-items: center; justify-content: space-between; gap: .75rem; flex-wrap: wrap; margin: 1.25rem 0 .6rem; }
.sectionhead h2 { margin: 0; }
/* As a card's first child the card padding is already the top gap. */
.card > .sectionhead:first-child, .tab-panel > .sectionhead:first-child { margin-top: 0; }
/* Section tab strip. .subnav = separate PAGES under one sidebar entry (GSP); .tabs = panels of
   ONE page (_ui.html tabs(), wired by app.js). Same look on purpose. */
.subnav, .tabs { display: flex; flex-wrap: wrap; gap: .25rem; margin: 0 0 1.25rem; border-bottom: 1px solid var(--line); }
.subnav a, .tabs a { padding: .5rem .9rem; color: var(--muted); border-bottom: 2px solid transparent; font-size: var(--fs-base); white-space: nowrap; }
.subnav a:hover, .tabs a:hover { color: var(--ink); text-decoration: none; }
.subnav a:focus-visible, .tabs a:focus-visible { outline-offset: -2px; }
/* Text is --brand-strong (AA); the underline is a fill, so it stays --brand. */
.subnav a.on, .subnav a[aria-current="page"], .tabs a[aria-selected="true"] { color: var(--brand-strong); border-bottom-color: var(--brand); font-weight: 600; }
.tab-count { display: inline-block; min-width: 1.5em; margin-left: .3rem; padding: 0 .4em; border-radius: var(--r-pill); background: var(--slate-100); color: var(--muted); font-size: var(--fs-xs); font-weight: 600; text-align: center; font-variant-numeric: tabular-nums; }
.tabs a[aria-selected="true"] .tab-count { background: var(--brand-soft); color: var(--brand-strong); }
/* tabindex=-1 makes a panel a hash target, not a control: no ring round a whole section. */
.tab-panel:focus { outline: none; }

/* ---------- pills ---------- */
/* Bare .pill is a neutral tag. A TONE (class or data-tone, which JS pollers set) adds the
   leading dot so state is never colour alone. The dot is a border, not a background:
   backgrounds are dropped in forced-colors mode. Unknown tone = the neutral look. */
.pill { display: inline-block; padding: .1rem .55rem; border-radius: var(--r-pill); font-size: var(--fs-xs); font-weight: 500;
  line-height: 1.5; background: var(--neutral-bg); border: 1px solid var(--neutral-bd); color: var(--neutral-fg); }
/* nowrap: otherwise the dot can be orphaned on its own line in a narrow cell. */
.pill[class*="pill-"], .pill[data-tone] { white-space: nowrap; }
.pill[class*="pill-"]::before, .pill[data-tone]::before { content: ""; display: inline-block; vertical-align: middle; width: 0; height: 0; margin: -.15em .4em 0 0; border: 3px solid currentColor; border-radius: 50%; }
.pill-ok, .pill[data-tone="ok"] { background: var(--ok-bg); border-color: var(--ok-bd); color: var(--ok-fg); }
.pill-warn, .pill[data-tone="warn"] { background: var(--warn-bg); border-color: var(--warn-bd); color: var(--warn-fg); }
.pill-danger, .pill[data-tone="danger"] { background: var(--danger-bg); border-color: var(--danger-bd); color: var(--danger-fg); }
.pill-info, .pill[data-tone="info"] { background: var(--info-bg); border-color: var(--info-bd); color: var(--info-fg); }
/* A live pill keeps its server-rendered pill-* class; data-tone must win for EVERY tone, neutral included. */
.pill[data-tone="neutral"] { background: var(--neutral-bg); border-color: var(--neutral-bd); color: var(--neutral-fg); }
/* In-progress states breathe; the global reduced-motion guard stops it. */
.pill-info::before, .pill[data-tone="info"]::before { animation: pulse 1.6s ease-in-out infinite; }
.pill[data-tone]:not([data-tone="info"])::before { animation: none; }
@keyframes pulse { 50% { opacity: .35; } }
.pill-list { display: inline-flex; flex-wrap: wrap; gap: .25rem; }

/* ---------- row action menu ---------- */
/* _ui.html row_menu(). THREE states, and the order of trust matters:
     html:not(.js)            no script, or app.js never initialised (base.html takes .js back
                              off): every item is laid out inline in the cell. The safe default.
     html.js :not(.is-ready)  app.js is on its way: items pre-collapsed so rows do not flash.
     html.js .is-ready        kebab + [hidden] menu, opened as a .dropdown-fixed.
   A destructive action is therefore never behind a toggle that does nothing. */
.rowmenu { display: inline-block; vertical-align: middle; }
/* A row with no menu keeps the kebab's width (.btn-icon.btn-sm) so its buttons align. JS-only:
   without the kebab there is no slot to hold. */
.rowmenu-spacer { display: none; }
html.js .rowmenu-spacer { display: inline-block; vertical-align: middle; width: 28px; }
.rowmenu .dropdown a, .rowmenu .dropdown button { display: flex; align-items: center; min-height: 36px; }
html.js .rowmenu:not(.is-ready) .dropdown { display: none; }
html:not(.js) .rowmenu-toggle { display: none; }
html:not(.js) .rowmenu .dropdown {
  position: static; display: inline-flex; flex-wrap: wrap; justify-content: flex-end; align-items: center; gap: .15rem;
  min-width: 0; padding: 0; border: 0; background: none; box-shadow: none;
}
html:not(.js) .rowmenu .dropdown a, html:not(.js) .rowmenu .dropdown button { width: auto; min-height: 32px; padding: .2rem .5rem; font-size: var(--fs-sm); }
html:not(.js) .rowmenu .dropdown-sep { align-self: stretch; width: 1px; height: auto; margin: 0 .2rem; }

/* ---------- secrets (one-time keys, TOTP secret, recovery codes) ---------- */
.qr { width: 176px; height: 176px; background: #fff; padding: 10px; border: 1px solid var(--line); border-radius: var(--r-md); }
.secret {
  display: inline-block; margin-top: .6rem; padding: .5rem .7rem; border: 1px dashed var(--slate-400);
  border-radius: var(--r-md); background: var(--panel-2); font-size: var(--fs-base);
  letter-spacing: .12em; word-break: break-all; user-select: all;
}
.codes {
  list-style: none; padding: .85rem; margin: 0 0 1.1rem;
  display: grid; grid-template-columns: 1fr 1fr; gap: .4rem .85rem;
  border: 1px solid var(--line); border-radius: var(--r-md); background: var(--panel-2);
}
.codes li { text-align: center; }
.codes code { display: block; padding: .4rem; text-align: center; font-size: var(--fs-base); letter-spacing: .06em; user-select: all; }
/* _ui.html secret_box(): the value lives ONLY as the text of .secret; Copy points at it by id. */
.secret-box { margin: .6rem 0 1rem; text-align: left; }
.secret-label { display: block; margin-bottom: .25rem; font-size: var(--fs-sm); font-weight: 600; }
.secret-row { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; }
.secret-row .secret { flex: 1 1 14rem; min-width: 0; margin-top: 0; }
.secret-plain { letter-spacing: normal; }
.secret-ack { margin: .8rem 0; }
/* Held until the box is ticked (app.js). .btn already dims on aria-disabled; this covers a
   plain link used as the gate. */
a[data-secret-gate][aria-disabled="true"] { opacity: .55; cursor: not-allowed; pointer-events: none; }

/* ---------- skeleton (JSON-polled panes before the first response) ---------- */
.skeleton { display: block; min-height: 1em; border-radius: var(--r-sm); color: transparent;
  background: linear-gradient(90deg, var(--slate-100) 25%, var(--slate-50) 50%, var(--slate-100) 75%) 0 0 / 200% 100%;
  animation: shimmer 1.4s linear infinite; }
@keyframes shimmer { to { background-position: -200% 0; } }

/* ---------- state page (403 / 404 / 500) ---------- */
.state-page { text-align: center; }
.state-page h1 { margin: 0 0 .4rem; font-size: var(--fs-lg); }
.state-page p { margin: 0 auto; max-width: 46ch; color: var(--ink-2); }
.state-page .actions { justify-content: center; margin-top: 1.1rem; }
.state-icon { margin: 0 auto .7rem; }
.state-warn .state-icon { background: var(--warn-bg); color: var(--warn-fg); }
.state-danger .state-icon { background: var(--danger-bg); color: var(--danger-fg); }

/* ---------- steps (horizontal progress) ---------- */
/* State is the dot's glyph (check / x / number) and an sr-only word, not the colour alone. */
.steps { display: flex; flex-wrap: wrap; gap: .4rem 0; list-style: none; margin: 0 0 1rem; padding: 0; }
.step { display: flex; align-items: center; gap: .45rem; color: var(--muted); font-size: var(--fs-sm); }
.step + .step::before { content: ""; width: 1.5rem; height: 2px; margin: 0 .6rem; background: var(--slate-300); }
.step-dot { display: inline-grid; place-items: center; flex: none; width: 22px; height: 22px; border-radius: 50%;
  border: 2px solid var(--slate-300); background: var(--panel); font-size: var(--fs-xs); font-weight: 700; }
.step-done { color: var(--ink-2); }
.step-done .step-dot { border-color: var(--ok-fg); background: var(--ok-fg); color: var(--brand-ink); }
.step-done + .step::before { background: var(--ok); }
.step-current { color: var(--ink); font-weight: 600; }
.step-current .step-dot { border-color: var(--brand); color: var(--brand-strong); box-shadow: var(--ring); }
.step-failed { color: var(--danger-fg); font-weight: 600; }
.step-failed .step-dot { border-color: var(--danger-fg); background: var(--danger-fg); color: var(--brand-ink); }

/* ---------- tables ---------- */
/* `.grid` is the legacy name of the data table; `.table` is the alias going forward. */
.grid, .table { width: 100%; border-collapse: collapse; }
.grid th, .table th { text-align: left; font-size: var(--fs-xs); font-weight: 600; text-transform: uppercase; letter-spacing: .04em;
  color: var(--muted); padding: .6rem 1.1rem; border-bottom: 1px solid var(--line); background: var(--panel-2); }
.grid td, .table td { padding: .6rem 1.1rem; border-bottom: 1px solid var(--line); }
/* An .sr-only child (the "Actions" header, ext_link's hint) is position:absolute. With no
   positioned ancestor its containing block is <body>, so it escaped the card's scroller and,
   sitting at the far end of a wide table, stretched the whole PAGE sideways on a phone. */
.grid th, .table th, .grid td, .table td { position: relative; }
.grid tbody tr:last-child td, .table tbody tr:last-child td { border-bottom: 0; }
/* Rows tint, never lift. The pointer is promised only where a row-click target exists. */
.grid tbody tr:hover, .table tbody tr:hover, .grid tbody tr:focus-within, .table tbody tr:focus-within { background: var(--panel-2); }
/* tr[data-href] is opened by app.js ("row-nav"); the keyboard path is the <a> in its first cell. */
tr[data-href] { cursor: pointer; }
.grid.tight td { padding: .45rem .7rem; }
/* Two classes deep so it outranks `.grid th`'s left alignment. */
.grid .right, .table .right { text-align: right; font-variant-numeric: tabular-nums; }
/* Columns whose content has no sensible wrap point — a version, a date, a size, a button pair.
   Width alone does not stop them: the browser wraps whatever is narrowest, so a date breaks
   after the month while a long prose column keeps its line. */
.nowrap { white-space: nowrap; }
/* ⚠ PAIR `.wide` WITH `.nowrap`, ALWAYS. Holding six columns to one line raises the table's
   minimum width, and auto table layout pays for that out of whatever column can still wrap —
   measured on the releases table at a 1120px content width, the Requires cell went from 2 lines
   to FIVE. A minimum width turns that into a scrollbar in the card instead: below it the table
   scrolls, above it nothing changes. Only for tables that genuinely need the room. */
.grid.wide { min-width: 1240px; }

/* ---------- tiles (KPI) ---------- */
.tiles { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; margin-bottom: 1rem; }
.tile {
  display: flex; flex-direction: column; gap: .35rem; padding: 1.1rem 1.25rem;
  background: var(--glass); border: 1px solid var(--glass-bd); border-radius: var(--r-lg);
  box-shadow: var(--e1), var(--hilite), 0 0 0 1px rgba(15,23,42,.04); color: var(--ink);
  -webkit-backdrop-filter: var(--glass-fx); backdrop-filter: var(--glass-fx);
  transition: transform .3s var(--ease), box-shadow .3s var(--ease), border-color .3s var(--ease);
}
/* The ONLY hover lift in the product: a tile that is a link. */
a.tile:hover, a.tile:focus-visible { text-decoration: none; transform: translateY(-2px); border-color: var(--brand); box-shadow: var(--e2), var(--hilite); }
.tile-label { font-size: var(--fs-sm); text-transform: uppercase; letter-spacing: .05em; color: var(--muted); }
.tile-value { font-size: var(--fs-2xl); font-weight: 700; line-height: 1; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
.tile-sub { font-size: var(--fs-md); font-weight: 500; color: var(--muted); }
/* Alert emphasis is a bar as well as a colour. */
.tile-alert { border-color: var(--danger-bd); box-shadow: var(--e1), var(--hilite), inset 3px 0 0 var(--danger); }
.tile-alert .tile-value { color: var(--danger-fg); }

/* ---------- toasts ----------
   One notification style for the whole portal. Fixed to the viewport so a message can
   never reflow the page underneath it — the previous .flash sat inside <main> and pushed
   content down (worst on the login screen, where a one-line notice became a full-width
   band above the card). The markup therefore lives OUTSIDE <main>. Errors never auto-hide
   (app.js only times out toasts that carry data-autohide). */
.toasts {
  position: fixed; top: 12px; right: 12px; z-index: 60;
  display: flex; flex-direction: column; gap: .5rem;
  width: min(380px, calc(100vw - 24px));
  pointer-events: none;                 /* the empty column must not eat clicks */
}
body.app .toasts { top: calc(var(--topbar-h) + 12px); }
.toast {
  pointer-events: auto;
  display: grid; grid-template-columns: auto 1fr auto; align-items: start; gap: .6rem;
  padding: .7rem .8rem;
  background: var(--glass-strong); color: var(--ink);
  border: 1px solid var(--line); border-left: 3px solid var(--muted);
  border-radius: var(--r-md); box-shadow: var(--e2);
  -webkit-backdrop-filter: var(--glass-fx); backdrop-filter: var(--glass-fx);
  animation: toast-in .18s ease-out;
}
.toast-icon { width: 18px; height: 18px; margin-top: .1rem; flex: none; color: var(--muted); }
.toast-msg { min-width: 0; overflow-wrap: anywhere; }
.toast-close {
  flex: none; background: none; border: 0; padding: .15rem; margin: -.15rem -.15rem 0 0;
  color: var(--muted); cursor: pointer; border-radius: var(--r-sm); line-height: 0;
}
.toast-close svg { width: 15px; height: 15px; }
.toast-close:hover { color: var(--ink); background: var(--slate-100); }
.toast-close:focus-visible { outline-offset: 1px; }
.toast-success { border-left-color: var(--ok); }
.toast-success .toast-icon { color: var(--ok-fg); }
.toast-danger, .toast-error { border-left-color: var(--danger); }
.toast-danger .toast-icon, .toast-error .toast-icon { color: var(--danger); }
.toast-warning { border-left-color: var(--warn); }
.toast-warning .toast-icon { color: var(--warn); }
.toast-info { border-left-color: var(--info); }
.toast-info .toast-icon { color: var(--info); }
/* Motion is decoration here; the message must still appear and still leave. The global
   reduced-motion guard shortens these to ~0 but animationend still fires for app.js. */
.toast--out { animation: toast-out .16s ease-in forwards; }
@keyframes toast-in  { from { opacity: 0; transform: translateX(12px); } }
@keyframes toast-out { to   { opacity: 0; transform: translateX(12px); } }

/* ---------- tooltip ---------- */
/* CSS-only, for short labels. Clipped inside .card.nopad (overflow) — use title= there. */
[data-tip] { position: relative; }
[data-tip]:hover::after, [data-tip]:focus-visible::after {
  content: attr(data-tip); position: absolute; left: 50%; bottom: calc(100% + 6px); transform: translateX(-50%); z-index: 50;
  padding: .25rem .5rem; border-radius: var(--r-sm); background: var(--slate-900); color: var(--slate-50);
  font-size: var(--fs-xs); font-weight: 500; white-space: nowrap; pointer-events: none;
}

/* ---------- user cell (avatar + name + second line) ---------- */
.user-cell { display: inline-flex; align-items: center; gap: .6rem; min-width: 0; }
.user-cell-text { display: grid; min-width: 0; line-height: 1.3; }

/* ================= 5. UTILITIES ================= */
/* Last of the general layers so a utility beats a component at equal specificity. */
.muted { color: var(--muted); }
.small { font-size: var(--fs-sm); }
.text-danger { color: var(--danger-fg); }      /* after .muted on purpose: "muted small text-danger" is red */
.text-warn { color: var(--warn-fg); }
.text-ok { color: var(--ok-fg); }
.text-center { text-align: center; }
.right { text-align: right; }
.num { font-variant-numeric: tabular-nums; }
.mono { font-family: var(--mono); }
.break-all { word-break: break-all; }
.scroll-x { overflow-x: auto; }
.brand-line { text-align: center; }
.m-0 { margin: 0; }
.mt-0 { margin-top: 0; }
.mt-2 { margin-top: var(--sp-2); }
.mt-3 { margin-top: var(--sp-3); }
.mt-4 { margin-top: var(--sp-4); }
.mt-6 { margin-top: var(--sp-6); }
.mt-8 { margin-top: var(--sp-8); }
.mb-0 { margin-bottom: 0; }
.mb-4 { margin-bottom: var(--sp-4); }
.p-0 { padding: 0; }
.p-2 { padding: var(--sp-2); }
.p-3 { padding: var(--sp-3); }
.p-4 { padding: var(--sp-4); }
.gap-1 { gap: var(--sp-1); }
.gap-5 { gap: var(--sp-5); }
.w-full { width: 100%; max-width: none; }
.sr-only {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* ================= 6. AUTH SCREENS =================
   Sign-in, 2FA verify/enrol and recovery codes. These are the only pages an
   unauthenticated visitor ever sees, so they carry the whole first impression
   on their own — no top bar, no sidebar to lean on. Light and brand-tinted to
   match the console behind them rather than introducing a second look. */
body.auth::before {
  background:
    radial-gradient(80rem 40rem at 50% -18rem, var(--brand-soft) 0%, transparent 60%),
    linear-gradient(180deg, #fbfcfe 0%, var(--bg) 100%);
}
body.auth .centered { padding: 1.25rem; }
/* Markup is `.card.narrow.authcard`: width and the 6vh margin come from .card.narrow, which
   outranks this selector — so neither is restated here. */
.authcard { width: 100%; padding: 2rem 1.75rem 1.75rem; text-align: center; box-shadow: var(--e3), var(--hilite); }
.authcard .logo-img { height: 44px; margin: 0 auto .5rem; }
.authcard h1 { font-size: var(--fs-lg); margin: 0 0 .3rem; letter-spacing: -.01em; }
.authcard > .muted { font-size: var(--fs-base); margin: 0 0 1.4rem; line-height: 1.5; }
.authcard .tagline { font-size: var(--fs-sm); text-transform: uppercase; letter-spacing: .08em; }
/* Labels read far better left-aligned even inside a centred card. */
.authcard form { text-align: left; margin-top: .25rem; }
.authcard form label { margin-bottom: .9rem; font-weight: 600; color: var(--ink); }
.authcard form label .input { margin-top: .35rem; }
/* The rule above is for the legacy label-wraps-input markup, where the label's bottom margin
   was the gap between fields. With the _forms.html macro the label is a SIBLING of the input,
   so that gap moves to .field and the label loses it. */
.authcard form .field { margin: 0 0 .9rem; }
.authcard form .field > label { margin: 0; }
.authcard .btn.primary, .authcard .btn-primary { margin-top: .35rem; padding: .65rem 1rem; font-weight: 600; }
/* One-time code: wide, spaced and monospaced so a 6-digit code is read back at a
   glance. Not numeric-only — a recovery code (a1b2-c3d4) goes in this same box. */
.authcard .otp {
  text-align: center; font-family: var(--mono);
  font-size: var(--fs-xl); letter-spacing: .28em; text-indent: .28em; padding: .7rem .5rem;
}
.authcard .otp::placeholder { letter-spacing: .18em; font-size: var(--fs-md); opacity: .45; }
.authhint { margin: 1.1rem 0 0; font-size: var(--fs-sm); color: var(--muted); line-height: 1.55; }
/* enrolment */
.qrwrap { display: flex; justify-content: center; margin: .25rem 0 1rem; }
.authcard details { text-align: center; margin-bottom: 1.1rem; }
.authcard details summary { cursor: pointer; font-size: var(--fs-sm); color: var(--brand-strong); }
.authcard details summary::marker { color: var(--muted); }

/* ================= 7. RESPONSIVE + PREFERENCES ================= */
@media (max-width: 880px) {
  .hamburger { display: inline-flex; align-items: center; justify-content: center; min-width: 40px; min-height: 40px; }
  .welcome { display: none; }
  /* visibility, not just the transform: an off-screen drawer is still in the tab order, and
     the hamburger says aria-expanded="false". Hidden only AFTER the slide-out has finished. */
  .sidebar { transform: translateX(-100%); visibility: hidden; transition: transform .2s ease, visibility 0s .2s; box-shadow: var(--e2); }
  body.nav-open .sidebar { transform: translateX(0); visibility: visible; transition-delay: 0s; }
  .content { margin-left: 0; }
  .nav-scrim { display: none; position: fixed; inset: var(--topbar-h) 0 0 0; background: rgba(15,23,42,.35); z-index: 20; }
  body.nav-open .nav-scrim { display: block; }
  .tiles { grid-template-columns: repeat(2, 1fr); }
  .cols, .grid2 { grid-template-columns: 1fr; }
  /* The drawer is modal here: the page behind it must not scroll under the scrim. */
  body.nav-open { overflow: hidden; }
  .topbar-title { display: block; flex: 1; }
  .topright { margin-left: 0; }
  /* Touch: 40px minimum for the controls a thumb has to hit. */
  button.avatar { width: 40px; height: 40px; }
  .nav-item { min-height: 40px; }
  .search input { min-height: 40px; }
  .toast-close { padding: .5rem; margin: -.5rem -.5rem 0 0; }
  /* A table in a PADDED card has no scroller of its own (.card.nopad does): let the card be
     one, or the table pushes the whole page sideways. */
  .card:has(> table), .card:has(> form > table) { overflow-x: auto; }
  .checklist { columns: 1; }
}
@media (max-width: 560px) {
  .content { padding: 1rem; }
  .tiles { grid-template-columns: 1fr; }
  .toasts { left: 12px; right: 12px; width: auto; }
  .authcard { padding: 1.5rem 1.15rem 1.25rem; }
  .codes { grid-template-columns: 1fr; }
  /* Trail collapses to the immediate parent. */
  .crumbs li:not(:nth-last-child(2)) { display: none; }
  /* Spacing restated: `li + li::before` never matches a parent that is the FIRST item. */
  .crumbs li:nth-last-child(2)::before { content: "\2039"; margin-right: .35rem; color: var(--slate-400); }
  .modal-foot { flex-direction: column-reverse; }
  .modal-foot .btn { width: 100%; }
  /* Label over value: a max-content label column squeezes long values (paths, hostnames) into
     a sliver on a phone. */
  .kv { grid-template-columns: 1fr; gap: 0; }
  .kv dt { margin-top: .55rem; }
  .kv dt:first-child { margin-top: 0; }
  /* One swipeable line instead of three wrapped rows of tabs. */
  .subnav, .tabs { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; }
  .steps { flex-direction: column; align-items: flex-start; }
  .step + .step::before { display: none; }
  .secret-row .btn { width: 100%; }
}
/* Fingers, at any width: row-menu items and inline text buttons get a real target. */
@media (pointer: coarse) {
  .rowmenu .dropdown a, .rowmenu .dropdown button { min-height: 44px; }
  .rowmenu-toggle.btn-sm { width: 36px; min-height: 36px; }
  html.js .rowmenu-spacer { width: 36px; }
  .linkbtn::after { inset: -11px -4px; }
}

/* Every animation and transition, present and future. !important because this must beat
   component rules regardless of order; animationend/transitionend still fire. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important; animation-iteration-count: 1 !important;
    transition-duration: .01ms !important; scroll-behavior: auto !important;
  }
  a.tile:hover, a.tile:focus-visible, .btn:active { transform: none; }
}

/* Glass is a set of tokens, so switching it off is a token change, not per-component rules. */
@media (prefers-reduced-transparency: reduce) {
  :root { --glass: var(--panel); --glass-strong: var(--panel); --chrome: var(--panel); --glass-bd: var(--line); --glass-fx: none; }
  .modal::backdrop { -webkit-backdrop-filter: none; backdrop-filter: none; background: rgba(15,23,42,.6); }
}
@media (prefers-contrast: more) {
  :root {
    --glass: var(--panel); --glass-strong: var(--panel); --chrome: var(--panel); --glass-fx: none;
    --muted: var(--slate-700); --line: #7b8594; --glass-bd: #7b8594; --line-strong: var(--slate-900);
  }
  body::before { display: none; }
  /* Unclassed links only (prose, tables, crumbs). Component anchors — .btn, .nav-item, .tile —
     clear the underline on :hover alone, so a bare `a` rule underlined them at rest. */
  a:not([class]) { text-decoration: underline; }
}
/* Borders and text survive forced colours; backgrounds do not. The gaps are states drawn ONLY
   with a background: the active nav item / tab, the alert tile, and the focus ring colour. */
@media (forced-colors: active) {
  :where(a, button, input, select, textarea, summary, [tabindex]):focus-visible, .input:focus { outline-color: Highlight; }
  .nav-item.active, .nav-item[aria-current="page"] { outline: 2px solid Highlight; outline-offset: -2px; }
  .subnav a.on, .subnav a[aria-current="page"] { border-bottom-color: Highlight; }
  .tile-alert { border: 2px solid Mark; }
  .btn, .card, .tile, .toast, .modal, .dropdown, .callout { border-color: CanvasText; }
  .btn.is-busy, .btn[aria-busy="true"] { color: GrayText; }
  .skeleton { border: 1px dashed GrayText; }
  .tabs a[aria-selected="true"] { border-bottom-color: Highlight; }
  .meter { border: 1px solid CanvasText; }
  .meter-fill { background: Highlight; }
  .step-dot, .checklist, .avatar { border: 1px solid CanvasText; }
  .avatar-dot { background: Mark; }
}
@media print {
  .topbar, .sidebar, .nav-scrim, .toasts, .skip-link, .skip, .actions, .rowmenu, body::before { display: none; }
  /* Paper has no tabs: print every panel. */
  .tab-panel[hidden] { display: block !important; }
  .content { margin: 0; padding: 0; max-width: none; }
  .card, .tile { box-shadow: none; border-color: var(--slate-300); break-inside: avoid; }
}
