/* =============================================================================
   downpipes console: production design system (tokens + base primitives + shell)
   Single source of truth for the bespoke, zero-dependency design system, served
   same-origin as one <link> (no CDN, no web font, no @import of anything remote).

   Built from design/01-architecture/tokens.css (authoritative for the existing
   values), with two production additions called for by the build contract:
     1. The teal "verified" TRUST accent promoted into the shared semantic layer
        (--trust*), distinct from the indigo action accent, in BOTH themes, so
        trust telemetry reads visually distinct from primary action.
     2. The app-shell + foundation-component CSS (nav rail, context bar, brand
        mark) layered on the same token contract.

   Theme mechanism (design-system.md 10):
     - Light is the default in :root.
     - Dark applies via @media (prefers-color-scheme: dark) for the System
       setting AND via :root[data-theme="dark"] (the override wins).
     - :root[data-theme="light"] forces light under an OS-dark setting.
   The OWNER's locked decision makes OBSIDIAN (dark) the DEFAULT skin when the
   stored preference is unset; that default is applied by the single sanctioned
   pre-paint inline script in index.html (it writes data-theme="dark" when no
   preference is stored), so this token layer keeps the honest light/dark
   mechanism and the script chooses the default. System still follows the OS.

   House rules honoured: Australian English in comments, no AI attribution,
   precise wording. Contrast targets: WCAG 2.2 AA (4.5:1 body text, 3:1 large
   text / UI component boundaries and focus indicator) in BOTH themes.
   ========================================================================== */

/* -----------------------------------------------------------------------------
   1. Raw palette ramps (the only place raw colours live). Never reference these
   directly from a component; reference the semantic tokens in section 2.
   -------------------------------------------------------------------------- */
:root {
  /* Cool neutral ramp */
  --n-0:   #ffffff;
  --n-50:  #f6f8fa;
  --n-100: #eef1f5;
  --n-150: #e4e8ee;
  --n-200: #d8dee6;
  --n-300: #c0c8d4;
  --n-400: #9aa5b5;
  --n-450: #828ea0;  /* control-border, light: >=3:1 on canvas and surface */
  --n-500: #646f7f;  /* AA body text: 5.09:1 on --n-0, 4.79:1 on --bg */
  --n-550: #5b6575;
  --n-600: #515b6b;
  --n-700: #3c4350;
  --n-750: #2f3540;
  --n-800: #232831;
  --n-850: #1a1e25;
  --n-900: #14171d;
  --n-950: #0e1014;

  /* Accent ramp (formalised from the existing #2563eb seed). Indigo-blue. */
  --a-50:  #eef3ff;
  --a-100: #dbe6ff;
  --a-300: #93b1ff;
  --a-400: #6088fb;
  --a-500: #3b66f0;   /* primary accent, light theme */
  --a-600: #2952d8;   /* hover */
  --a-700: #1f40b0;   /* active */
  --a-900: #1a2f73;
  --a-300d: #a8c0ff;  /* desaturated/raised accent for dark surfaces */
  --a-400d: #84a4ff;

  /* Trust ramp (the teal "verified" accent, lifted from Obsidian and formalised
     into the shared layer). Deliberately a separate hue from the indigo action
     accent AND from the green ok-status, so trust telemetry is its own family.
     Teal sits between green and blue; verified against WCAG AA in both themes. */
  --t-50:   #e2f7f4;
  --t-400:  #2ba696;
  --t-500:  #0f8c7d;   /* trust accent, light theme (dot/border tone) */
  --t-600:  #0a6e62;   /* readable teal foreground on the light tint (>=4.5:1) */
  --t-150d: #0e302c;   /* subtle teal tint for a verified chip in dark */
  --t-300d: #4fd0bf;   /* raised teal foreground/accent for dark surfaces */

  /* Status ramps. Each carries a strong tone (text/icon on subtle bg), a
     mid tone (the dot/border), and subtle tints for badge backgrounds. */
  --ok-100:  #d8f3e3;  --ok-200:  #abe3c5;  --ok-500:  #1f9d57;  --ok-600:  #167a43;
  --ok-150d: #133626;  --ok-300d: #5fd699;

  --warn-100:#fdeecb;  --warn-200:#f4d98a;  --warn-500:#a8730b;  --warn-600:#8a5d06;
  --warn-150d:#3a2c0c; --warn-300d:#f0c057;

  --danger-100:#fbe0df; --danger-200:#f3b7b4; --danger-500:#cf2f2f; --danger-600:#a82424;
  --danger-150d:#3a1717; --danger-300d:#f08784;

  --info-100:#dcecfd;  --info-200:#aecff7;  --info-500:#1763c4;  --info-600:#1250a0;
  --info-150d:#132a45;  --info-300d:#74acf0;

  /* ---------------------------------------------------------------------------
     Scales that are theme-independent (spacing, type, radius, motion, z-index).
     Defined once here; never overridden per theme.
     ------------------------------------------------------------------------ */

  /* Spacing: 4px base, geometric-ish ramp. Every margin/padding/gap uses these. */
  --space-1:  4px;
  --space-2:  8px;
  --space-3:  12px;
  --space-4:  16px;
  --space-5:  24px;
  --space-6:  32px;
  --space-7:  40px;
  --space-8:  48px;
  --space-10: 64px;
  --space-12: 96px;

  /* Type: system stacks only (no external font). */
  --font-sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue",
               Arial, "Noto Sans", sans-serif;
  --font-mono: ui-monospace, "SF Mono", "SFMono-Regular", "Cascadia Code",
               "Roboto Mono", Menlo, Consolas, "Liberation Mono", monospace;

  /* Type scale (Enterprise consoles trend slightly small + airy line-height).
     REM-BASED (UIA-17): the browser font-size preference now scales the whole
     console (Primer/USWDS/GOV.UK/Carbon practice); at the default 16px root the
     computed sizes are identical to the previous px scale (12/13/14/15/18/22/
     28/34). The in-app text-size setting scales the root, composing with — never
     replacing — browser zoom. */
  --text-xs:   0.75rem;
  --text-sm:   0.8125rem;
  --text-base: 0.875rem;
  --text-md:   0.9375rem;
  --text-lg:   1.125rem;
  --text-xl:   1.375rem;
  --text-2xl:  1.75rem;
  --text-3xl:  2.125rem;

  --leading-tight: 1.2;
  --leading-snug:  1.35;
  --leading-base:  1.55;

  --weight-regular: 400;
  --weight-medium:  500;
  --weight-semibold:600;
  --weight-bold:    700;

  --tracking-tight: -0.012em;  /* large headings */
  --tracking-wide:  0.06em;     /* small uppercase section labels */

  /* Radius */
  --radius-sm:   4px;
  --radius:      8px;
  --radius-lg:   12px;
  --radius-full: 9999px;

  /* Borders */

  /* Control sizing (consistent input/button heights) */
  --control-h:    36px;
  --control-h-sm: 28px;
  --control-h-lg: 44px;   /* coarse-pointer / primary CTA */

  /* Layout */
  --rail-w:        240px;
  --rail-w-collapsed: 56px;
  --header-h:      56px;
  --measure-prose: 680px;   /* readable measure for forms/prose */
  --measure-wide:  1280px;  /* dense tables/dashboards */

  /* Motion: keep most UI motion under ~200ms. The slow in-out easing serves the
     long ambient aurora drift only (reduced-motion gated like everything else). */
  --dur-fast:    120ms;
  --dur:         180ms;
  --ease-out:    cubic-bezier(0.16, 0.84, 0.44, 1);
  --ease-in-out: cubic-bezier(0.45, 0, 0.2, 1);

  /* z-index ladder: base < sticky < drawer < modal < toast < palette < tooltip */
  --z-sticky:  100;
  --z-drawer:  200;
  --z-modal:   300;
  --z-toast:   400;
  --z-palette: 500;
  --z-tooltip: 600;
}

/* -----------------------------------------------------------------------------
   2. Semantic tokens: LIGHT (the light companion). Components reference only these.
   -------------------------------------------------------------------------- */
:root {
  color-scheme: light;

  /* Surfaces (lightness steps create hierarchy; borders stay hairline) */
  --bg:           var(--n-50);   /* app canvas */
  --bg-subtle:    var(--n-100);  /* recessed wells, table zebra */
  --surface:      var(--n-0);    /* cards, panels, inputs */
  --surface-raised: var(--n-0);  /* overlays (use shadow for lift) */
  --surface-hover:  var(--n-50);
  --surface-active: var(--n-100);
  --surface-inset:  var(--n-100);/* code blocks, recessed regions */

  /* Borders */
  --border:        var(--n-200);
  --border-subtle: var(--n-150);
  --border-strong: var(--n-300);
  --control-border: var(--n-450);  /* input/select boundary, >=3:1 (WCAG 1.4.11) */

  /* Text */
  --text:        var(--n-900);   /* primary, 14:1 on --surface */
  /* Secondary text and small section/group labels. Nudged one step darker (--n-550)
     so 12px labels keep a buffer above 4.5:1 (~5:1 on the canvas and on --bg-subtle), rather
     than sitting at exactly 4.50:1. There is deliberately NO third "subtle" text tone: the old
     --text-subtle failed AA wherever it touched text and is retired (audit UIA-02). */
  --text-muted:  var(--n-550);
  --text-link:   var(--a-600);
  /* Input/textarea placeholder: a dedicated readable tone (>=4.5:1 on the input surface in both
     themes). */
  --placeholder: var(--n-500);

  /* Accent (the indigo ACTION accent: primary buttons, active nav, focus) */
  --accent:        var(--a-500);
  --accent-hover:  var(--a-600);
  --accent-active: var(--a-700);
  --accent-fg:     #ffffff;        /* text on accent (>=4.5:1 on --a-500) */
  --accent-subtle-bg: var(--a-50);
  --accent-subtle-fg: var(--a-700);

  /* Trust (the teal VERIFIED accent: Access-verified chip, no-custody chip,
     audit chain-intact state). A third hue family, distinct from --accent
     (action) and from --ok (status). --trust-fg on --trust-bg clears 4.5:1;
     --trust-border clears 3:1. */
  --trust:        var(--t-500);   /* the verified dot/border tone */
  --trust-bg:     var(--t-50);    /* subtle teal tint for a verified chip */
  --trust-fg:     var(--t-600);   /* readable teal foreground (>=4.5:1 on --trust-bg) */
  /* The verified chip border must clear 3:1 as a UI boundary (WCAG 1.4.11) on BOTH the white
     surface and the chip tint. --t-400 was 3.0:1 on white but only 2.69:1 on the tint; --t-500
     clears both (audit UIA-21). */
  --trust-border: var(--t-500);

  /* Focus ring (>=3:1 against adjacent surfaces in this theme) */
  --ring:        var(--a-500);

  /* Status: dot/border tone, plus subtle badge bg + readable fg */
  --ok:          var(--ok-500);
  --ok-bg:       var(--ok-100);
  --ok-fg:       var(--ok-600);
  --ok-border:   var(--ok-200);

  --warn:        var(--warn-500);
  --warn-bg:     var(--warn-100);
  --warn-fg:     var(--warn-600);
  --warn-border: var(--warn-200);

  --danger:      var(--danger-500);
  --danger-bg:   var(--danger-100);
  --danger-fg:   var(--danger-600);
  --danger-border:var(--danger-200);
  --danger-solid-fg: #ffffff;      /* text on a solid danger button */
  /* Foreground on a solid --ok fill (the done-step tick markers). White on --ok-500 is 3.3:1 —
     fine for the tick GLYPH (1.4.11); never set running text on a solid ok fill in light. */
  --ok-solid-fg: #ffffff;
  /* Foreground on a solid --warn fill (the abandoned run-cell glyph). --warn is --warn-500
     (#a8730b), a dark amber in light theme, so white reads on it. Defined here rather than
     borrowing --danger-solid-fg, because --warn inverts between themes exactly as danger and
     ok do and a borrowed token would silently stop inverting with its own fill. */
  --warn-solid-fg: #ffffff;

  --info:        var(--info-500);
  --info-bg:     var(--info-100);
  --info-fg:     var(--info-600);
  --info-border: var(--info-200);

  --neutral:     var(--n-500);     /* idle/unknown dot; >=3:1 on surface AND bg-subtle (1.4.11) */
  --neutral-bg:  var(--n-100);
  --neutral-fg:  var(--n-600);

  /* Selection */
  --selection-bg: var(--a-100);
  --selection-fg: var(--n-900);

  /* Elevation (soft, diffuse; reserved for true overlays) */
  --shadow-sm: 0 1px 2px rgba(16, 22, 35, 0.06),
               0 1px 1px rgba(16, 22, 35, 0.04);
  --shadow:    0 4px 12px rgba(16, 22, 35, 0.08),
               0 1px 3px rgba(16, 22, 35, 0.06);
  --shadow-lg: 0 16px 40px rgba(16, 22, 35, 0.16),
               0 4px 12px rgba(16, 22, 35, 0.10);
  --shadow-focus-danger: 0 0 0 3px var(--danger-100);

  /* Backdrop for modals/palette */
  --backdrop: rgba(20, 23, 29, 0.45);

  /* Skeleton */
  --skeleton-base: var(--n-150);
  --skeleton-sheen: var(--n-100);

  /* The failed-cell hatch stripe (run strips). Theme-tuned so the inset x mark
     keeps >=3:1 over the worst-case stripe in BOTH themes (UIA-21). */
  --hatch: rgba(0, 0, 0, 0.42);
}

/* -----------------------------------------------------------------------------
   3. Semantic tokens: DARK (Obsidian, the default skin). Applied via
   prefers-color-scheme (the System setting) and the explicit override. Dark is
   NOT an inversion: accents desaturate, surfaces step up in lightness for
   elevation, shadows soften, text is near-white.
   -------------------------------------------------------------------------- */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;

    --bg:            var(--n-950);
    --bg-subtle:     var(--n-900);
    --surface:       var(--n-850);
    --surface-raised:var(--n-800);
    --surface-hover: var(--n-800);
    --surface-active:var(--n-750);
    --surface-inset: var(--n-900);

    --border:        var(--n-750);
    --border-subtle: var(--n-800);
    --border-strong: var(--n-600);
    --control-border: var(--n-500);  /* input/select boundary, >=3:1 in dark */

    --text:          var(--n-100);   /* near-white, not #fff */
    --text-muted:    var(--n-400);   /* >=4.5:1 on --surface */
    --text-link:     var(--a-300d);

    --accent:        var(--a-400d);
    --accent-hover:  var(--a-300d);
    --accent-active: var(--a-300);
    --accent-fg:     var(--n-950);    /* dark text on the lighter dark-accent */
    --accent-subtle-bg: #18233f;
    --accent-subtle-fg: var(--a-300d);

    --trust:        var(--t-300d);
    --trust-bg:     var(--t-150d);
    --trust-fg:     var(--t-300d);    /* light teal on the dark teal tint, >=4.5:1 */
    --trust-border: var(--t-400);     /* teal border, >=3:1 on the dark surface and the chip tint */

    --ring:          var(--a-400d);

    --ok:        var(--ok-300d);  --ok-bg: var(--ok-150d);  --ok-fg: var(--ok-300d);  --ok-border:#1d5238;
    --warn:      var(--warn-300d);--warn-bg:var(--warn-150d);--warn-fg:var(--warn-300d);--warn-border:#5a440f;
    --danger:    var(--danger-300d);--danger-bg:var(--danger-150d);--danger-fg:var(--danger-300d);--danger-border:#5e2422;
    /* The dark danger SOLID button uses the lighter dark-danger tone (--danger-300d #f08784) as its
       fill, so white text on it is only 2.47:1. Use a near-black foreground (--n-950) so the label
       clears 4.5:1 on the solid danger button (a11y fix). */
    --danger-solid-fg: var(--n-950);
    /* Near-black tick on the light dark-ok fill (--ok-300d): the ok-fg-on-ok pairing was 1.00:1
       (invisible). --n-950 on #5fd699 clears 4.5:1. */
    --ok-solid-fg: var(--n-950);
    /* Near-black glyph on the light dark-warn fill (--warn-300d, #f0c057); white would be ~1.7:1. */
    --warn-solid-fg: var(--n-950);
    --info:      var(--info-300d);--info-bg:var(--info-150d);--info-fg:var(--info-300d);--info-border:#1c3c63;

    --neutral:   var(--n-500);   --neutral-bg: var(--n-800); --neutral-fg: var(--n-300);

    /* Input/textarea placeholder: raise the dark placeholder tone to --n-400 so it clears ~4.5:1 on
       the dark input surface (the default --text-subtle dark tone fell to ~3.3:1). */
    --placeholder: var(--n-400);

    --selection-bg:#27365c;
    --selection-fg: var(--n-50);

    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.40);
    --shadow:    0 6px 18px rgba(0, 0, 0, 0.46),
                 0 2px 6px rgba(0, 0, 0, 0.38);
    --shadow-lg: 0 20px 48px rgba(0, 0, 0, 0.58),
                 0 6px 16px rgba(0, 0, 0, 0.46);
    /* Dark glow derived from the DARK danger tone (--danger-300d), not the light red (UIA-20). */
    --shadow-focus-danger: 0 0 0 3px rgba(240, 135, 132, 0.35);

    --backdrop:  rgba(0, 0, 0, 0.62);

    --skeleton-base: var(--n-800);
    --skeleton-sheen: var(--n-750);

    /* Lighter stripe in dark so the near-black x mark clears 3:1 over it (UIA-21). */
    --hatch: rgba(0, 0, 0, 0.28);
  }
}

/* Explicit override wins regardless of system setting. Duplicated rather than
   shared so the override is unconditional (an OS-light user can force dark, and
   the pre-paint script writes this attribute to make Obsidian the default). */
:root[data-theme="dark"] {
  color-scheme: dark;

  --bg:            var(--n-950);
  --bg-subtle:     var(--n-900);
  --surface:       var(--n-850);
  --surface-raised:var(--n-800);
  --surface-hover: var(--n-800);
  --surface-active:var(--n-750);
  --surface-inset: var(--n-900);

  --border:        var(--n-750);
  --border-subtle: var(--n-800);
  --border-strong: var(--n-600);
  --control-border: var(--n-500);

  --text:          var(--n-100);
  --text-muted:    var(--n-400);
  --text-link:     var(--a-300d);

  --accent:        var(--a-400d);
  --accent-hover:  var(--a-300d);
  --accent-active: var(--a-300);
  --accent-fg:     var(--n-950);
  --accent-subtle-bg: #18233f;
  --accent-subtle-fg: var(--a-300d);

  --trust:        var(--t-300d);
  --trust-bg:     var(--t-150d);
  --trust-fg:     var(--t-300d);
  --trust-border: var(--t-400);     /* teal border, >=3:1 on the dark surface and the chip tint */

  --ring:          var(--a-400d);

  --ok:        var(--ok-300d);  --ok-bg: var(--ok-150d);  --ok-fg: var(--ok-300d);  --ok-border:#1d5238;
  --warn:      var(--warn-300d);--warn-bg:var(--warn-150d);--warn-fg:var(--warn-300d);--warn-border:#5a440f;
  --danger:    var(--danger-300d);--danger-bg:var(--danger-150d);--danger-fg:var(--danger-300d);--danger-border:#5e2422;
  /* Near-black label on the lighter dark-danger solid fill, so the danger button clears 4.5:1
     (white was 2.47:1). Matches the prefers-color-scheme dark block above. */
  --danger-solid-fg: var(--n-950);
  /* Near-black tick on the light dark-ok fill; matches the prefers-color-scheme dark block. */
  --ok-solid-fg: var(--n-950);
  /* Near-black glyph on the light dark-warn fill; matches the prefers-color-scheme dark block. */
  --warn-solid-fg: var(--n-950);
  --info:      var(--info-300d);--info-bg:var(--info-150d);--info-fg:var(--info-300d);--info-border:#1c3c63;

  --neutral:   var(--n-500);   --neutral-bg: var(--n-800); --neutral-fg: var(--n-300);

  /* Dark placeholder raised to --n-400 (~4.5:1 on the dark input surface). */
  --placeholder: var(--n-400);

  --selection-bg:#27365c;
  --selection-fg: var(--n-50);

  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.40);
  --shadow:    0 6px 18px rgba(0, 0, 0, 0.46),
               0 2px 6px rgba(0, 0, 0, 0.38);
  --shadow-lg: 0 20px 48px rgba(0, 0, 0, 0.58),
               0 6px 16px rgba(0, 0, 0, 0.46);
  /* Dark glow derived from the DARK danger tone (--danger-300d), not the light red (UIA-20). */
  --shadow-focus-danger: 0 0 0 3px rgba(240, 135, 132, 0.35);

  --backdrop:  rgba(0, 0, 0, 0.62);

  --skeleton-base: var(--n-800);
  --skeleton-sheen: var(--n-750);

  /* Lighter stripe in dark so the near-black x mark clears 3:1 over it (UIA-21). */
  --hatch: rgba(0, 0, 0, 0.28);
}

/* Light override also wins, for an OS-dark user who forces light. */
:root[data-theme="light"] { color-scheme: light; }

/* =============================================================================
   4. Base + reset (minimal, opinionated). Self-contained; no normalize dep.
   ========================================================================== */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }
/* The HTML `hidden` attribute must win over a display utility. The browser's
   default [hidden]{display:none} is a UA rule, so any author display (e.g.
   .stack-sm { display:grid }) beats it and a hidden styled container stays
   visible. This author-origin !important restores hidden's intent everywhere,
   so a screen toggling el.hidden to swap variants (Destinations' R2/S3 blocks)
   actually swaps. JS that shows an element removes the attribute, so nothing
   that should be visible is suppressed. */
[hidden] { display: none !important; }

html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  font-family: var(--font-sans);
  line-height: var(--leading-base);
  tab-size: 2;
  /* Focus Not Obscured (WCAG 2.4.11/2.4.12): scrolled-to focus targets clear the
     sticky context bar and sticky table headers. */
  scroll-padding-top: calc(var(--header-h) + var(--space-3));
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-size: var(--text-base);
  font-weight: var(--weight-regular);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  font-synthesis: none;
}

::selection { background: var(--selection-bg); color: var(--selection-fg); }

img, svg, video, canvas { display: block; max-width: 100%; }
svg { fill: none; }

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

a { color: var(--text-link); text-decoration: none; }
a:hover { text-decoration: underline; }

h1, h2, h3, h4 { font-weight: var(--weight-semibold); line-height: var(--leading-tight); }
h1 { font-size: var(--text-2xl); letter-spacing: var(--tracking-tight); }
h2 { font-size: var(--text-xl); letter-spacing: var(--tracking-tight); }
h3 { font-size: var(--text-lg); }
h4 { font-size: var(--text-md); }

code, kbd, samp, pre { font-family: var(--font-mono); font-size: 0.92em; }

/* Tabular numerals on anything numeric/aligned. */
.tnum, td.num, .stat-value, .mono { font-variant-numeric: tabular-nums; }
.mono { font-family: var(--font-mono); }

/* Readable measures */
.measure { max-width: var(--measure-prose); }
.measure-wide { max-width: var(--measure-wide); }

/* Small utility: one step below the body size, matching --text-sm. Used for
   secondary values in session cards and similar compact read surfaces. */
.small { font-size: var(--text-sm); }

/* Visually hidden but available to assistive tech */
.visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%);
  white-space: nowrap; border: 0;
}

/* Skip link (keyboard) */
.skip-link {
  position: absolute; left: var(--space-2); top: -48px; z-index: var(--z-tooltip);
  background: var(--surface); color: var(--text); border: 1px solid var(--border);
  border-radius: var(--radius-sm); padding: var(--space-2) var(--space-3);
  box-shadow: var(--shadow); transition: top var(--dur) var(--ease-out);
}
.skip-link:focus-visible { top: var(--space-2); }

/* Universal visible focus. :focus-visible only, so mouse clicks stay clean.
   3:1 ring + offset; never remove without replacement. */
:where(a, button, input, select, textarea, summary, [tabindex], [role="button"],
       [role="option"], [role="tab"], [role="menuitem"]):focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* =============================================================================
   5. Base primitives. Enough to build the shell and screens directly. Each is a
   thin, token-driven class. More components are specced in design-system.md.
   ========================================================================== */

/* ---- Button --------------------------------------------------------------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center;
  gap: var(--space-2);
  height: var(--control-h);
  padding: 0 var(--space-4);
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  font-size: var(--text-base);
  font-weight: var(--weight-medium);
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
  user-select: none;
  transition: background var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out),
              box-shadow var(--dur-fast) var(--ease-out);
}
.btn:disabled, .btn[aria-disabled="true"] {
  opacity: 0.5; cursor: not-allowed; pointer-events: none;
}
/* A linklike button gated with aria-disabled dims the same way, so it can stay focusable and carry a
   reachable reason (a visually-hidden span) rather than a hover-only title. */
.linklike:disabled, .linklike[aria-disabled="true"] {
  opacity: 0.5; cursor: not-allowed; pointer-events: none;
}
.btn svg { width: 16px; height: 16px; flex: none; }

/* Variants */
.btn--primary {
  background: var(--accent); color: var(--accent-fg); border-color: var(--accent);
}
.btn--primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
.btn--primary:active { background: var(--accent-active); border-color: var(--accent-active); }

.btn--secondary {
  /* The boundary is load-bearing on same-colour surfaces, so it uses the >=3:1
     --control-border, not the decorative --border-strong (audit UIA-11). */
  background: var(--surface); color: var(--text); border-color: var(--control-border);
}
.btn--secondary:hover { background: var(--surface-hover); }
.btn--secondary:active { background: var(--surface-active); }

.btn--ghost { background: transparent; color: var(--text); border-color: transparent; }
.btn--ghost:hover { background: var(--surface-hover); }
.btn--ghost:active { background: var(--surface-active); }

.btn--danger {
  background: var(--danger); color: var(--danger-solid-fg); border-color: var(--danger);
  /* E5: the disabled -> enabled snap (type-to-confirm's Apply lighting up on a match) fades rather
     than cutting. Only opacity and border-color animate; no hue change, glow, scale or bounce. */
  transition: background var(--dur-fast) var(--ease-out), border-color 150ms var(--ease-out), color var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out), opacity 150ms var(--ease-out);
}
.btn--danger:hover { filter: brightness(0.94); }
.btn--danger:active { filter: brightness(0.88); }
.btn--danger:focus-visible { outline-color: var(--danger); box-shadow: var(--shadow-focus-danger); }

/* Sizes */
.btn--sm { height: var(--control-h-sm); padding: 0 var(--space-3); font-size: var(--text-sm); border-radius: var(--radius-sm); }
.btn--lg { height: var(--control-h-lg); padding: 0 var(--space-5); font-size: var(--text-md); }

/* Icon-only */
.btn--icon { width: var(--control-h); padding: 0; }
.btn--icon.btn--sm { width: var(--control-h-sm); }

/* Busy: reserve width, swap content. The spinner respects reduced motion below. */
.btn[data-busy="true"] { pointer-events: none; }
.btn[data-busy="true"] .btn__spinner { display: inline-block; }
.btn__spinner {
  display: none; width: 14px; height: 14px; flex: none;
  border: 2px solid currentColor; border-right-color: transparent;
  border-radius: var(--radius-full);
  animation: dp-spin 0.7s linear infinite;
}

/* ---- Input / textarea / select ------------------------------------------- */
.field { display: grid; gap: var(--space-2); }
.field__label {
  font-size: var(--text-sm); font-weight: var(--weight-medium); color: var(--text);
}
.field__hint { font-size: var(--text-xs); color: var(--text-muted); }
.field__doc { font-size: var(--text-xs); display: inline-flex; align-items: center; gap: var(--space-1); width: fit-content; }
.field__error {
  font-size: var(--text-xs); color: var(--danger-fg);
  display: flex; align-items: center; gap: var(--space-1);
}
/* Two fields side by side, collapsing to one column on narrow, at the same 640 breakpoint
   .restore-field-grid uses. Both tracks are minmax(0, 1fr) so NEITHER field can take the other's width.
   That is the whole point of the class rather than a taste: an `auto` second track is grown to its
   max-content before a 1fr track gets anything at all, and a .field whose min-width has been zeroed
   contributes nothing to hold its own track open, so the first column collapses. Measured on console
   d9bc3c5e on /access/roles: the add-mapping row was `1fr auto` with a whole .field in the auto track,
   and at every width from 375 to 962 the Group name column was 0 wide with its label, input, hint and
   Learn more link all 40.2px wide and painting outside the field, while the Role select took 205px of a
   375 viewport. Closed by default, so no as-delivered pass ever saw it. */
.field-2up { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--space-3); align-items: end; }
@media (max-width: 640px) { .field-2up { grid-template-columns: minmax(0, 1fr); } }

.input, .textarea, .select {
  width: 100%;
  height: var(--control-h);
  padding: 0 var(--space-3);
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--control-border);
  border-radius: var(--radius);
  font-size: var(--text-base);
  transition: border-color var(--dur-fast) var(--ease-out),
              box-shadow var(--dur-fast) var(--ease-out),
              background var(--dur-fast) var(--ease-out);
}
.textarea { height: auto; min-height: calc(var(--control-h) * 2); padding: var(--space-2) var(--space-3); resize: vertical; line-height: var(--leading-snug); }
.input::placeholder, .textarea::placeholder { color: var(--placeholder); }
.input:hover, .textarea:hover, .select:hover { border-color: var(--border-strong); }
.input:focus, .textarea:focus, .select:focus {
  outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-subtle-bg);
}
.input:disabled, .textarea:disabled, .select:disabled,
.input[aria-disabled="true"], .textarea[aria-disabled="true"], .select[aria-disabled="true"] {
  background: var(--bg-subtle); color: var(--text-muted); cursor: not-allowed;
}
.input[aria-invalid="true"], .textarea[aria-invalid="true"], .select[aria-invalid="true"] {
  border-color: var(--danger);
}
.input[aria-invalid="true"]:focus, .textarea[aria-invalid="true"]:focus {
  box-shadow: var(--shadow-focus-danger);
}

/* Native select chevron via inline SVG data-uri (the stroke matches the LIGHT
   --text-muted, --n-550 #5b6575 — keep in step if the token moves). Kept tiny
   and self-contained. */
.select {
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%235b6575' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--space-3) center;
  padding-right: var(--space-7);
}

/* Dark-theme chevron: the light data-uri hardcodes the light muted tone; in dark,
   --text-muted resolves to --n-400 (#9aa5b5). Override the background-image so
   the arrow matches --text-muted in dark (both the [data-theme] attribute and the
   OS-dark media query, to mirror the same two-block pattern the semantic tokens
   use). No other .select properties change; only the encoded stroke colour. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .select {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%239aa5b5' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
  }
}
:root[data-theme="dark"] .select {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%239aa5b5' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
}

/* ---- Identity-provider add-connection form (idp-connections.ts) -----------
   ONE consistent field grid so every row's columns line up. Earlier the form
   used several independent `auto-fit` grids, so a group holding a single field
   (Entra's lone tenant id, the client id, the secret) stretched full-width
   while paired rows showed two half-width columns — the fields did not align.
   A FIXED two-column track fixes that: a lone field sits half-width, aligned
   under the row above; it collapses to one column on narrow viewports. A field
   that genuinely wants the full width (a long URL, the secret, a textarea) opts
   in with .idp-span. */
.idp-form-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-4) var(--space-4);
  align-items: start;
}
.idp-form-grid > .idp-span { grid-column: 1 / -1; }
@media (max-width: 640px) {
  .idp-form-grid { grid-template-columns: 1fr; }
}

/* The "How to add <provider>" guide: a calm, bordered panel of numbered steps
   that renders inline when a provider is chosen. Not a stack of banners — one
   quiet surface (calm density, design-system 7a). */
.idp-guide {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-subtle);
  padding: var(--space-4);
  display: grid;
  gap: var(--space-4);
}
.idp-guide__title {
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  color: var(--text);
  margin: 0;
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
.idp-guide__section { display: grid; gap: var(--space-2); }
.idp-guide__sub {
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  color: var(--text-muted);
  margin: 0;
}

/* Numbered steps: a counter chip + the text, two-column so multi-line steps
   keep a clean hanging indent under the text, never under the number. */
.idp-steps {
  margin: 0;
  padding: 0;
  list-style: none;
  counter-reset: idp-step;
  display: grid;
  gap: var(--space-2);
}
.idp-steps > li {
  counter-increment: idp-step;
  display: grid;
  grid-template-columns: 1.5rem 1fr;
  gap: var(--space-2);
  align-items: start;
  font-size: var(--text-sm);
  color: var(--text);
  line-height: var(--leading-snug);
}
.idp-steps > li::before {
  content: counter(idp-step);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  flex: none;
  border-radius: var(--radius-full);
  background: var(--accent-subtle-bg);
  color: var(--accent);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  font-variant-numeric: tabular-nums;
}

/* Plain bullet list (gotchas / values-to-copy) — dotted, calm, indented to line
   up with the step text above it. */
.idp-list { margin: 0; padding-left: var(--space-4); display: grid; gap: var(--space-1); }
.idp-list > li { font-size: var(--text-sm); color: var(--text); line-height: var(--leading-snug); }

/* The redirect/callback URL callout — THE value the customer pastes into their
   IdP. A dashed, copyable highlight so it reads as "take this from here". The
   value updates live as the connection id is typed. */
.idp-redirect {
  display: grid;
  gap: var(--space-1);
  padding: var(--space-3);
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface);
}
.idp-redirect__label { font-size: var(--text-xs); color: var(--text-muted); }
.idp-redirect__row { display: flex; align-items: center; gap: var(--space-2); min-width: 0; }
.idp-redirect__value {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--text-sm);
}

/* ---- Checkbox / radio / switch (native, themed via accent-color) ---------- */
.checkbox-row { display: flex; align-items: flex-start; gap: var(--space-2); }
.checkbox-row input[type="checkbox"],
.checkbox-row input[type="radio"] {
  width: 16px; height: 16px; margin-top: 2px; accent-color: var(--accent); flex: none;
}
.checkbox-row label { font-size: var(--text-base); }

/* ---- Card / panel --------------------------------------------------------- */
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: var(--space-5);
}
.card--inset { background: var(--bg-subtle); }
.card__header {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-3); margin-bottom: var(--space-4);
}
.card__title { font-size: var(--text-md); font-weight: var(--weight-semibold); }
.card__desc { font-size: var(--text-sm); color: var(--text-muted); margin-top: var(--space-1); }

/* Caution / danger zone variants (Stripe/GitHub "danger zone" idiom) */
.card--warn   { border-color: var(--warn-border);   background: var(--warn-bg); }
.card--danger { border-color: var(--danger-border); }
.card--danger .card__title { color: var(--danger-fg); }

/* ---- Stat tile ------------------------------------------------------------ */
.stat {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-lg); padding: var(--space-4) var(--space-5);
  display: grid; gap: var(--space-1);
}
/* Mixed case (CALM/UIA-47): uppercase microlabels added texture noise and read
   harder for dyslexic operators; weight + colour carry the hierarchy instead. */
.stat__label {
  font-size: var(--text-xs); font-weight: var(--weight-semibold);
  color: var(--text-muted);
}
.stat__value { font-size: var(--text-2xl); font-weight: var(--weight-semibold); font-variant-numeric: tabular-nums; }
.stat__secondary { font-size: var(--text-sm); color: var(--text-muted); }

/* ---- Status dot (colour + shape, never colour alone) ---------------------- */
.dot {
  display: inline-block; width: 9px; height: 9px; flex: none;
  border-radius: var(--radius-full); background: var(--neutral);
  vertical-align: middle;
}
.dot--ok      { background: var(--ok); }
.dot--warn    { /* triangle for shape-redundancy */
  width: 0; height: 0; border-radius: 0; background: transparent;
  border-left: 5px solid transparent; border-right: 5px solid transparent;
  border-bottom: 9px solid var(--warn);
}
.dot--danger  { background: var(--danger); border-radius: 2px; } /* square */
.dot--neutral { background: transparent; border: 1.5px solid var(--neutral); } /* hollow */
.dot--info    { background: var(--info); }
.dot--trust   { background: var(--trust); } /* verified telemetry */

/* ---- Badge ---------------------------------------------------------------- */
.badge {
  display: inline-flex; align-items: center; gap: var(--space-1);
  min-height: 20px; padding: 0 var(--space-2);
  font-size: var(--text-xs); font-weight: var(--weight-medium);
  border-radius: var(--radius-full);
  background: var(--neutral-bg); color: var(--neutral-fg);
  border: 1px solid transparent;
  white-space: nowrap;
}
.badge--ok     { background: var(--ok-bg);     color: var(--ok-fg);     border-color: var(--ok-border); }
.badge--warn   { background: var(--warn-bg);   color: var(--warn-fg);   border-color: var(--warn-border); }
.badge--danger { background: var(--danger-bg); color: var(--danger-fg); border-color: var(--danger-border); }
.badge--info   { background: var(--info-bg);   color: var(--info-fg);   border-color: var(--info-border); }
.badge--accent { background: var(--accent-subtle-bg); color: var(--accent-subtle-fg); }
.badge--trust  { background: var(--trust-bg);  color: var(--trust-fg);  border-color: var(--trust-border); }

/* ---- Keycap (for the command-palette keycap and shortcut hints) ----------- */
.kbd {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 20px; min-height: 20px; padding: 0 var(--space-1);
  font-family: var(--font-sans); font-size: var(--text-xs); font-weight: var(--weight-medium);
  color: var(--text-muted); background: var(--bg-subtle);
  border: 1px solid var(--border); border-bottom-width: 2px;
  border-radius: var(--radius-sm);
}

/* ---- Code block (copyable, conceal/reveal lives in spec) ------------------ */
.code {
  font-family: var(--font-mono); font-size: var(--text-sm);
  background: var(--surface-inset); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius);
  padding: var(--space-3) var(--space-4);
  overflow-x: auto; white-space: pre; line-height: var(--leading-snug);
}

/* ---- Inline link-like button (used for entity names in tables) ------------ */
.linklike {
  background: none; border: none; padding: 0; cursor: pointer;
  color: var(--text-link); font: inherit; text-align: left;
}
.linklike:hover { text-decoration: underline; }

/* ---- Divider / section label --------------------------------------------- */
.section-label {
  font-size: var(--text-xs); font-weight: var(--weight-semibold);
  color: var(--text-muted);  /* 12px label: must clear 4.5:1, so muted not subtle */
}
.hr { border: none; border-top: 1px solid var(--border-subtle); margin: var(--space-5) 0; }

/* =============================================================================
   6. App shell (design-system.md 6.1, information-architecture.md 2). A CSS grid
   of three zones: nav rail, context bar, main. The rail collapses to an icon
   strip on narrow viewports and on demand; on Compact it becomes a slide-over.
   ========================================================================== */

.shell {
  display: grid;
  /* minmax(0, 1fr), never a bare 1fr. A bare `1fr` track is `minmax(auto, 1fr)`, and that `auto` FLOOR is
     the larger of its items' automatic minimum sizes, so ANY grid child that has not opted out with
     min-width:0 can push the content column wider than the viewport and scroll the whole page sideways.
     .main opted out (min-width:0 below); the context bar and the setup strip never did, so the bar's
     min-content (its widest unshrinkable item, the engine identity chip's nowrap host) became the column's
     floor: at 375px on an estate whose host is 46 characters the bar's min-content is 410px, so every
     chrome'd screen measured a uniform 35px horizontal overflow (scrollWidth 410 against clientWidth 375).
     Flooring the track at 0 lets the column be exactly the space available and hands the shrinking back to
     the bar's own wrap + the chip's ellipsis, which were already written for it. */
  grid-template-columns: var(--rail-w) minmax(0, 1fr);
  /* Three rows: header, an AUTO setup row (collapses to 0 when the setup strip is hidden), then main. The
     setup strip is a grid child with no area otherwise, so it auto-flowed to a trailing implicit row and
     rendered at the page BOTTOM (over the demo banner on a phone); giving it a named row seats it under the
     header where it belongs. The header track is auto-min (minmax below 1023) so a WRAPPED context bar grows
     the row instead of overflowing a fixed 56px and painting over the page h1. */
  grid-template-rows: var(--header-h) auto 1fr;
  grid-template-areas:
    "rail header"
    "rail setup"
    "rail main";
  min-height: 100vh;
  min-height: 100dvh;
}
.shell[data-rail="collapsed"] { grid-template-columns: var(--rail-w-collapsed) minmax(0, 1fr); }

/* Full-bleed mode: the onboarding wizard and the signed-out state run without the
   rail or the context bar (the one shell exception, information-architecture.md 2 /
   screen 10), to keep the operator on a single track. The shell stays mounted (the
   router and keyboard layer are singletons); the chrome is hidden and main spans
   the full width, centred on the prose measure by the screen itself. */
.shell[data-chrome="off"] {
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: 1fr;
  grid-template-areas: "main";
}
.shell[data-chrome="off"] .rail,
.shell[data-chrome="off"] .context-bar { display: none; }
.shell[data-chrome="off"] .main {
  padding: var(--space-8) var(--space-5);
  display: flex; justify-content: center; align-items: flex-start;
}
.shell[data-chrome="off"] .main__inner { width: 100%; }
/* The onboarding carousel and the passkey/register/signed-out screens are full-viewport
   surfaces (their root .ob-page is min-height:100vh and owns its own header, padding and
   centring). The default full-bleed main padding therefore reserved empty space ABOVE their
   header bar (pushing the logo down and to the right relative to the shell) and pushed the
   footer below the fold. Drop it for .ob-page screens so the page sits flush in the viewport. */
.shell[data-chrome="off"] .main:has(.ob-page) { padding: 0; display: block; }

/* ---- Nav rail ------------------------------------------------------------- */
.rail {
  grid-area: rail;
  display: flex; flex-direction: column;
  background: var(--bg-subtle);
  border-right: 1px solid var(--border);
  min-height: 0;            /* allow the nav region to scroll inside the rail */
  position: sticky; top: 0; height: 100vh; height: 100dvh;
}
.rail__brand {
  display: flex; align-items: center; gap: var(--space-2);
  height: var(--header-h); padding: 0 var(--space-4);
  border-bottom: 1px solid var(--border-subtle);
  flex: none;
}
.rail__nav {
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  padding: var(--space-3) var(--space-2);
  display: flex; flex-direction: column; gap: var(--space-1);
}
.rail__footer {
  flex: none; border-top: 1px solid var(--border-subtle);
  padding: var(--space-3) var(--space-2);
  display: flex; flex-direction: column; gap: var(--space-2);
}
.rail__group {
  display: flex; flex-direction: column; gap: var(--space-1);
  margin-top: var(--space-3);
}
.rail__group:first-child { margin-top: 0; }
.rail__group-label {
  padding: 0 var(--space-3); margin-bottom: var(--space-1);
  font-size: var(--text-xs); font-weight: var(--weight-semibold);
  color: var(--text-muted);
}

/* A nav item: an anchor styled as a row, with a leading icon and a label. The
   active item gets a 2px left accent bar plus a tinted background (never a loud
   pill); status is conveyed by aria-current="page" as well as the visual. */
.nav-item {
  position: relative;
  display: flex; align-items: center; gap: var(--space-3);
  height: var(--control-h);
  padding: 0 var(--space-3);
  border-radius: var(--radius);
  color: var(--text); font-size: var(--text-base); font-weight: var(--weight-medium);
  text-decoration: none;
  transition: background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.nav-item:hover { background: var(--surface-hover); text-decoration: none; }
.nav-item svg { width: 20px; height: 20px; flex: none; color: var(--text-muted); }
.nav-item__label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* The quiet "needs attention" count chip (only /credentials today): a small text pill pushed to the
   right of the label. It is shape + label, never colour alone — the integer is its content and the
   link carries an aria-label, so removing colour loses nothing. A muted warn tone (matching the
   --warn token palette) reads as a calm count, not an alarm. No animation, no ambient motion. */
.nav-item__count {
  margin-left: auto; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 18px; height: 18px; padding: 0 var(--space-1);
  font-size: var(--text-xs); font-weight: var(--weight-medium);
  line-height: 1;
  border-radius: var(--radius-full);
  background: var(--warn-bg); color: var(--warn-fg);
  border: 1px solid var(--warn-border);
}
/* On the active item the label/icon recolour to the accent; the count keeps its own warn tone so the
   "needs attention" reading is not lost when /credentials is the current screen. */
.nav-item[aria-current="page"] {
  background: var(--accent-subtle-bg); color: var(--accent-subtle-fg);
}
.nav-item[aria-current="page"] svg { color: var(--accent-subtle-fg); }
.nav-item[aria-current="page"]::before {
  content: ""; position: absolute; left: 0; top: 6px; bottom: 6px;
  width: 2px; border-radius: var(--radius-full); background: var(--accent);
}

/* Collapsed rail: hide labels + group labels, centre the icons. The accessible
   name survives because the label stays in the DOM (visually-hidden), and a
   tooltip mirrors it on hover/focus (wired in the shell). */
.shell[data-rail="collapsed"] .rail__brand-name,
.shell[data-rail="collapsed"] .rail__group-label,
.shell[data-rail="collapsed"] .nav-item__label,
.shell[data-rail="collapsed"] .nav-item__count {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.shell[data-rail="collapsed"] .nav-item { justify-content: center; padding: 0; }
.shell[data-rail="collapsed"] .rail__brand { justify-content: center; padding: 0; }

/* ---- Context bar (header) ------------------------------------------------- */
.context-bar {
  grid-area: header;
  display: flex; align-items: center; gap: var(--space-3);
  height: var(--header-h);
  padding: 0 var(--space-4);
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  position: sticky; top: 0; z-index: var(--z-sticky);
}
.context-bar__title { font-size: var(--text-md); font-weight: var(--weight-semibold); }
.context-bar__spacer { flex: 1 1 auto; }

/* The command-palette trigger: a faux search field with a keycap. */
.palette-trigger {
  display: flex; align-items: center; gap: var(--space-2);
  height: var(--control-h-sm); min-width: 220px;
  padding: 0 var(--space-3);
  background: var(--bg-subtle); color: var(--text-muted);
  border: 1px solid var(--border); border-radius: var(--radius);
  font-size: var(--text-sm); cursor: text;
}
.palette-trigger svg { width: 14px; height: 14px; flex: none; }
.palette-trigger__hint { margin-left: auto; }

/* The engine identity chip in the context bar (host + health dot). */
.engine-chip {
  display: inline-flex; align-items: center; gap: var(--space-2);
  height: var(--control-h-sm); padding: 0 var(--space-3);
  background: var(--bg-subtle); border: 1px solid var(--border);
  border-radius: var(--radius); font-size: var(--text-sm); color: var(--text);
  max-width: 280px;
}
.engine-chip__host { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The "Update available" chip in the context bar: present ONLY when a verified release genuinely
   carries a newer engine or console component (the shell keeps it hidden otherwise, including
   while the verdict loads). A real button that deep-links to the Updates section on /licence, so
   it reads as an interactive pill in the accent-subtle treatment the count/Active chips use:
   shape + icon + label, never colour alone. Calm: no animation beyond the standard hover
   transition; the focus ring comes from the universal :focus-visible rule. */
.update-chip {
  display: inline-flex; align-items: center; gap: var(--space-2);
  height: var(--control-h-sm); padding: 0 var(--space-3);
  background: var(--accent-subtle-bg); color: var(--accent-subtle-fg);
  border: 1px solid transparent; border-radius: var(--radius-full);
  font-size: var(--text-sm); font-weight: var(--weight-medium);
  cursor: pointer; flex: none; white-space: nowrap;
  transition: border-color var(--dur-fast) var(--ease-out);
}
.update-chip:hover { border-color: var(--accent); }
.update-chip svg { flex: none; }
/* No version named by the channel: the empty span leaves the flex row so it adds no stray gap. */

/* ---- Main content region -------------------------------------------------- */
.main {
  grid-area: main;
  min-width: 0;            /* let wide tables scroll inside, not blow out the grid */
  padding: var(--space-6);
}
.main__inner { margin: 0 auto; }            /* centred; width capped by .measure* */
.page-header { margin-bottom: var(--space-5); }
.page-header__title { font-size: var(--text-2xl); }
/* The referrer-aware Back affordance (navigation design): one quiet line above the
   h1, only on child/sub pages. A text button (CSP-safe, no inline handler), muted
   until hover, with a leading chevron. */
.page-header__crumb { margin-bottom: var(--space-2); }
.page-header__back {
  display: inline-flex; align-items: center; gap: var(--space-1);
  border: none; background: none; padding: 0; cursor: pointer;
  font: inherit; font-size: var(--text-sm); color: var(--text-muted);
  border-radius: var(--radius-sm);
}
.page-header__back:hover { color: var(--text); }
.page-header__back:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
.page-header__back svg { flex: none; }
.page-header__desc { color: var(--text-muted); margin-top: var(--space-1); }

/* The rail toggle is hidden by default; shown on medium and below to open the
   slide-over / collapse. */
.rail-toggle { display: none; }

/* ---- Mobile slide-over rail ----------------------------------------------- */
.rail-scrim {
  display: none; position: fixed; inset: 0;
  background: var(--backdrop); z-index: var(--z-drawer);
}

/* =============================================================================
   7. Responsive shell (information-architecture.md design-system 4.8). Container
   queries are preferred for components; viewport media queries drive the shell.
   ========================================================================== */

/* Medium and below: the rail can collapse to an icon strip; show the toggle. */
@media (max-width: 1023px) {
  .rail-toggle { display: inline-flex; }
  /* The context bar wraps to 2–3 rows in this band (§20); let the header grid track GROW to fit it (minmax
     floor = the one-line height) so the wrapped bar never overflows a fixed 56px row and paints over the
     page h1. Mirrors the 3-row template above. */
  .shell { grid-template-rows: minmax(var(--header-h), auto) auto 1fr; }
}

/* Compact: the rail becomes a focus-trapped slide-over (opened via the toggle).
   The shell collapses to a single content column; 44px touch targets. */
@media (max-width: 767px) {
  .shell {
    /* minmax(0, 1fr) for the same reason as the wide template above: the single content column must be
       exactly the viewport, never the widest chrome item's min-content. */
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas:
      "header"
      "setup"
      "main";
  }
  .rail {
    position: fixed; left: 0; top: 0; bottom: 0; height: 100dvh;
    width: min(86vw, var(--rail-w)); z-index: calc(var(--z-drawer) + 1);
    transform: translateX(-100%);
    transition: transform var(--dur) var(--ease-out);
    box-shadow: var(--shadow-lg);
  }
  .shell[data-rail="open"] .rail { transform: none; }
  .shell[data-rail="open"] .rail-scrim { display: block; }
  .main { padding: var(--space-4); }
  .nav-item, .btn, .engine-chip { min-height: var(--control-h-lg); }

  /* Reflow (WCAG 1.4.10): no horizontal overflow at 320px. The context bar WRAPS instead of
     forcing a single non-wrapping row, and grows vertically as needed. */
  .context-bar {
    flex-wrap: wrap;
    height: auto;
    min-height: var(--header-h);
    padding: var(--space-2) var(--space-3);
    row-gap: var(--space-2);
  }
  /* The page title may shrink and ellipsis rather than push the row wider than the viewport. */
  .context-bar__title {
    min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  /* The command-palette trigger collapses to an ICON-ONLY button: drop the 220px search-field
     min-width and hide the text label + the keycap hint, so it no longer forces overflow. The
     button keeps its aria-label, so it is still an accessible "open command palette" control. */
  .palette-trigger {
    min-width: 0;
    width: var(--control-h-lg);
    min-height: var(--control-h-lg);
    justify-content: center;
    padding: 0;
  }
  .palette-trigger__label,
  .palette-trigger__hint { display: none; }
  /* The update chip collapses to an ICON-ONLY pill (the palette-trigger idiom): the text label and
     version hide; the button's aria-label still names the fact and the version in full. */
  .update-chip {
    min-width: var(--control-h-lg);
    min-height: var(--control-h-lg);
    justify-content: center;
    padding: 0;
  }
  .update-chip__label,
  /* The engine chip may shrink (its host already ellipsises) so it never overflows the row. */
  .engine-chip { min-width: 0; max-width: 100%; }
}

/* Ultra: content stays capped at the wide measure, centred, never edge-to-edge.
   The .measure / .measure-wide utilities on .main__inner already cap width; this
   just keeps generous side gutters on very wide screens. */
@media (min-width: 1600px) {
  .main { padding-left: var(--space-8); padding-right: var(--space-8); }
}

/* =============================================================================
   8. Motion + animation keyframes (all gated on reduced-motion below).
   ========================================================================== */
@keyframes dp-spin { to { transform: rotate(360deg); } }
@keyframes dp-shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } }
@keyframes dp-fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes dp-rise { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
/* The public tour's beat model (2026-07-02): the rail variant shifts the console left so the story
   column never covers content, and a try-it beat's spotlight cutout pulses a soft accent invitation
   ring around the REAL control. The ring keyframe restates the spotlight's own dim wash (the cutout
   paints the dim as a 200vmax shadow), or the dim would flicker with each pulse. Gated on the
   effective motion preference via [data-motion] (the pre-paint attribute the app sets). */
@media (min-width: 1024px) {
  body.dp-tour-rail .main { padding-right: 376px; }
  /* The drawer overlay is fixed and full-width, so it never saw the rail's reserved column and the run
     chapters' subjects were laid out behind the guide panel: measured at 1024, the run-detail dialog spans
     232 to 792 while the rail occupies 664 to 1008, and the panel covered 28% of run-seal and 20% of
     run-segments. The dialog is centred in its overlay, so reserving the same column here re-centres it in
     what is left. */
  body.dp-tour-rail .overlay--drawer { padding-right: 376px; }
}
@keyframes dp-tour-invite {
  0%, 100% { box-shadow: 0 0 0 200vmax rgba(3, 7, 14, 0.44), 0 0 0 0 color-mix(in srgb, var(--accent) 60%, transparent); }
  50%      { box-shadow: 0 0 0 200vmax rgba(3, 7, 14, 0.44), 0 0 0 12px color-mix(in srgb, var(--accent) 0%, transparent); }
}
:root[data-motion="full"] .tour-spot--invite { animation: dp-tour-invite 1.7s var(--ease-out) infinite; }

/* The tour welcome's two walk actions (Governance Tour / Engineering Tour): equal-width columns on a wide
   card so each is a normal, centred button rather than a full-card bar, stacking to a single column on a
   narrow card so they never clip off a phone. Buttons stretch to a shared height (grid default) regardless
   of label length. The demoted linklike Free Explore control sits outside this grid (persona-fork.ts). */
.tour-fork-actions {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-3);
  width: 100%;
}
@media (max-width: 560px) {
  .tour-fork-actions { grid-template-columns: 1fr; }
}

.skeleton {
  background: linear-gradient(90deg, var(--skeleton-base) 25%, var(--skeleton-sheen) 37%, var(--skeleton-base) 63%);
  background-size: 200% 100%;
  border-radius: var(--radius-sm);
  animation: dp-shimmer 1.4s ease-in-out infinite;
}

/* =============================================================================
   9. prefers-reduced-motion: HARD gate. Disable non-essential motion; keep
   information visible (skeletons fall back to a static tint, spinners freeze
   into a static ring, transitions collapse to instant). Never hide info.
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
  .skeleton {
    background: var(--skeleton-base);
    animation: none;
  }
  .btn__spinner { animation: none; opacity: 0.6; }
}

/* =============================================================================
   9.5 Higher-order components (console task 2/2): the primitives the screens
   compose. Built only from the semantic tokens above. Overlays (dialog, drawer,
   toast) are the only shadowed surfaces; flat surfaces use border + lightness.
   ========================================================================== */

/* ---- Overlay engine: backdrop + dialog (modal) + drawer ------------------- */
.overlay {
  position: fixed; inset: 0; z-index: var(--z-modal);
  display: flex; background: var(--backdrop);
  animation: dp-fade-in var(--dur-fast) var(--ease-out);
}
.overlay--modal { align-items: center; justify-content: center; padding: var(--space-5); }
/* The "drawer" variant now renders CENTRED, like a modal (owner: no right-side panels — every
   wizard, form and detail view opens in the middle of the page). Kept as a distinct variant only
   so existing call sites need not change; geometry mirrors --modal. */
.overlay--drawer { align-items: center; justify-content: center; padding: var(--space-5); }

.dialog {
  background: var(--surface-raised); color: var(--text);
  border: 1px solid var(--border); box-shadow: var(--shadow-lg);
  display: flex; flex-direction: column; min-height: 0;
  outline: none;
}
/* Belt-and-braces (UIA-26): when focus falls back onto the surface itself (a
   dialog with no focusable child), the keyboard user still sees a ring. */
.dialog:focus-visible { outline: 2px solid var(--ring); outline-offset: -2px; }
.dialog--modal {
  border-radius: var(--radius-lg);
  width: min(560px, 100%); max-height: calc(100dvh - var(--space-8));
  animation: dp-rise var(--dur) var(--ease-out);
}
.dialog--drawer {
  border-radius: var(--radius-lg);
  width: min(560px, 100%); max-height: calc(100dvh - var(--space-8));
  animation: dp-rise var(--dur) var(--ease-out);
}
@keyframes dp-drawer-in { from { transform: translateX(16px); opacity: 0.6; } to { transform: none; opacity: 1; } }

.dialog__header {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-3);
  padding: var(--space-4) var(--space-5); border-bottom: 1px solid var(--border-subtle);
  flex: none;
}
.dialog__title { font-size: var(--text-md); font-weight: var(--weight-semibold); }
/* R17: the dialog header's optional severity chip (dialog.ts dialogSurface). */
.dialog__heading { display: flex; align-items: center; gap: var(--space-2); min-width: 0; }
.dialog__severity { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; flex: none; border-radius: var(--radius); border: 1px solid transparent; }
.dialog__severity--warn { background: var(--warn-bg); color: var(--warn-fg); border-color: var(--warn-border); }
.dialog__severity--danger { background: var(--danger-bg); color: var(--danger-fg); border-color: var(--danger-border); }
.dialog__body { padding: var(--space-5); overflow-y: auto; flex: 1 1 auto; min-height: 0; }
.dialog__footer { padding: var(--space-4) var(--space-5); border-top: 1px solid var(--border-subtle); flex: none; }
.dialog__actions { display: flex; justify-content: flex-end; gap: var(--space-2); }

/* ---- Toast region --------------------------------------------------------- */
.toast-region {
  position: fixed; left: 50%; transform: translateX(-50%); bottom: var(--space-6); z-index: var(--z-toast);
  display: flex; flex-direction: column; align-items: center; gap: var(--space-2);
  max-width: min(420px, calc(100vw - var(--space-8))); pointer-events: none;
}
.toast-region--assertive { bottom: auto; top: var(--space-4); }
.toast {
  pointer-events: auto;
  display: flex; align-items: center; gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  background: var(--surface-raised); color: var(--text);
  border: 1px solid var(--border); border-left-width: 3px; border-radius: var(--radius);
  box-shadow: var(--shadow); font-size: var(--text-sm);
  animation: dp-rise var(--dur) var(--ease-out);
}
.toast--success { border-left-color: var(--ok); }
.toast--info { border-left-color: var(--info); }
.toast--warn { border-left-color: var(--warn); }
.toast__icon { display: inline-flex; flex: none; }
.toast--success .toast__icon { color: var(--ok); }
.toast--info .toast__icon { color: var(--info); }
.toast--warn .toast__icon { color: var(--warn); }
.toast__msg { flex: 1 1 auto; }
.toast__action { flex: none; }
.toast__close { flex: none; }

/* ---- Trust chips (verified telemetry: a third hue family) ----------------- */
.trust-row { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-bottom: var(--space-5); }
.trust-chip {
  display: inline-flex; align-items: center; gap: var(--space-2);
  height: 24px; padding: 0 var(--space-3);
  border: 1px solid var(--border); border-radius: var(--radius-full);
  font-size: var(--text-sm); font-weight: var(--weight-medium);
  background: var(--bg-subtle); color: var(--text);
  /* Never wrap to a second line (the fixed 24px height meant a wrap overflowed the chip), and allow the
     chip to shrink inside a cramped top bar so the label ellipsises rather than pushing the row wider. */
  white-space: nowrap; min-width: 0; max-width: 100%;
}
.trust-chip__icon { display: inline-flex; flex: none; }
/* The label ellipsises when the top bar is tight; the email rides in its own span so it can be dropped
   entirely on a narrow viewport (the verdict word + the title tooltip still convey the full state). */
.trust-chip__label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Bound the email so a long federated identity ellipsises inside the chip rather than stretching the top
   bar and, through it, the shell main region past the viewport (B20 / G-P1-145 U9). inline-block + a
   max-width + nowrap is what makes text-overflow actually fire; the full address stays in the chip title. */
.trust-chip__email { display: inline-block; max-width: min(22vw, 280px); vertical-align: bottom; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Drop the verdict chip's email across the whole docked-but-cramped band (≤1023, not just ≤768): in 769–1023
   the bar already wraps and chrome.ts renders a separate mono .account-chip__id with the SAME email, so the
   address would otherwise read twice in the one chip. The verdict word + the title tooltip still convey state. */
@media (max-width: 1023px) {
  .trust-chip__email { display: none; }
}
.trust-chip--trust { background: var(--trust-bg); color: var(--trust-fg); border-color: var(--trust-border); }
.trust-chip--warn { background: var(--warn-bg); color: var(--warn-fg); border-color: var(--warn-border); }
.trust-chip--neutral { background: var(--neutral-bg); color: var(--neutral-fg); border-color: var(--border); }

/* ---- Tables --------------------------------------------------------------- */
/* position:relative so the scroller is also the CONTAINING BLOCK for anything absolutely positioned inside
   it. Without that, an absolutely positioned descendant takes the nearest positioned ancestor ABOVE the
   scroller (.async-region is position:relative), and a box is only clipped by ancestors between it and its
   containing block, so the scroller does not clip it and its static position out at the far edge of a wide
   table widens the whole document. That is exactly what /access/roles does at a 1062 viewport on console
   d9bc3c5e: the members table is 989px inside a 758px scroller, the Actions column's screen-reader-only
   reason span (".. You cannot remove the only Owner") is laid out at x=1208, and the document scrolls
   sideways by 147px with no element painting outside its parent, because the containment rule excludes
   out-of-flow children and is right to. It is the only width it happens at: below 1024 the table
   card-stacks, and from 1101 the content column is wide enough to hold the table. */
.dp-table-wrap { position: relative; overflow-x: auto; border: 1px solid var(--border); border-radius: var(--radius-lg); }
.dp-table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); }
.dp-table th, .dp-table td { text-align: left; padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--border-subtle); vertical-align: middle; }
.dp-table thead th {
  position: sticky; top: 0; background: var(--bg-subtle); z-index: 1;
  font-weight: var(--weight-semibold); color: var(--text-muted);
  font-size: var(--text-xs);
}
.dp-table tbody tr:last-child td { border-bottom: none; }
.dp-table td.num, .dp-table th.num { text-align: right; font-variant-numeric: tabular-nums; }
.dp-table__row--activatable { cursor: pointer; }
.dp-table__row--activatable:hover { background: var(--surface-hover); }
.dp-table__row--activatable:focus-visible { outline: 2px solid var(--ring); outline-offset: -2px; }
.dp-table__empty { color: var(--text-muted); text-align: center; padding: var(--space-6); }
.filter-toolbar { display: flex; align-items: center; gap: var(--space-3); margin-bottom: var(--space-3); }
.filter-toolbar .input { max-width: 360px; }
.filter-toolbar__count { white-space: nowrap; }

/* ---- Stat grid (Overview tiles) ------------------------------------------- */
.stat-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: var(--space-4); }
/* Dimension-true skeletons (CALM-06): the placeholder matches what replaces it,
   so a load never reflows the page. Tile = stat padding + label + value +
   status line (~108px); row = the comfortable table row pitch (49px, matching
   data-table.ts rowHeightComfortable — keep these two in step). */
.skeleton-tile { height: 108px; border-radius: var(--radius-lg); }
.skeleton-block { display: flex; flex-direction: column; gap: var(--space-2); }
.skeleton-row { height: 49px; border-radius: var(--radius); }

/* ---- Fleet list ----------------------------------------------------------- */
.fleet-list { list-style: none; display: flex; flex-direction: column; }
.fleet-list__row { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); padding: var(--space-2) 0; border-bottom: 1px solid var(--border-subtle); }
.fleet-list__row:last-child { border-bottom: none; }

/* ---- Block error + inline outcome (two-channel error rendering) ----------- */
.block-error, .inline-outcome { margin-top: var(--space-4); }
.block-error__head, .inline-outcome__head { display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-2); }
.block-error__icon { color: var(--warn); display: inline-flex; }
.inline-outcome__icon { color: var(--info); display: inline-flex; }
.block-error__title, .inline-outcome__title { font-size: var(--text-md); font-weight: var(--weight-semibold); }
.block-error__detail, .inline-outcome__detail { color: var(--text); }

/* ---- Empty state ---------------------------------------------------------- */
.empty-state { text-align: center; }
.empty-state__diagram { margin-bottom: var(--space-3); color: var(--text-muted); }
.empty-state__title { font-size: var(--text-md); font-weight: var(--weight-semibold); }
.empty-state__body { color: var(--text-muted); margin-top: var(--space-2); }

/* ---- Banner --------------------------------------------------------------- */
.banner {
  display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-3) var(--space-4); border-radius: var(--radius);
  border: 1px solid var(--border); margin-bottom: var(--space-4);
}
.banner__icon { flex: none; display: inline-flex; }
.banner__msg { flex: 1 1 auto; font-size: var(--text-sm); }
.banner__action, .banner__close { flex: none; }
.banner--info { background: var(--info-bg); color: var(--info-fg); border-color: var(--info-border); }
.banner--info .banner__icon { color: var(--info); }
.banner--warn { background: var(--warn-bg); color: var(--warn-fg); border-color: var(--warn-border); }
.banner--warn .banner__icon { color: var(--warn); }
.banner--danger { background: var(--danger-bg); color: var(--danger-fg); border-color: var(--danger-border); }
.banner--danger .banner__icon { color: var(--danger); }

/* G-P0-096 (banner-below-the-fold): the control-plane recovery host (components/recovery-banner.ts) is
   inserted as the FIRST child of <body>, ahead of the mounted shell, so it reads above the chrome in normal
   document flow -- but app-shell.ts:243 moves focus to .main on every route render with preventScroll:false
   (the standard SPA a11y pattern: land keyboard/screen-reader focus on the new screen and let the browser
   scroll it into view), and a plain static-flow element ahead of .main gets scrolled straight off the top of
   the viewport by that same browser-native scroll. Measured: a fresh boot on a latched plane left
   window.scrollY at 111 (the banner's own rendered height) with the banner fully off-screen above the fold,
   even though it is the very first thing in the DOM. Sticky keeps it pinned to the top of the viewport
   through that scroll (it is already at its sticky offset the moment the page has any scrollable content, so
   it never leaves y=0) without taking the element out of flow the way `fixed` would, so nothing below it
   needs a compensating top-padding hack. z-index sits one above the sticky context-bar/rail (the chrome this
   banner must always read above) and stays well under drawer/modal/toast/palette/tooltip, so an open
   reconcile modal or a confirmation toast is never hidden under it -- the same z-ladder the tour's persistent
   demo banner already follows (lib/demo/banner.ts). */
.cp-recovery-host { position: sticky; top: 0; z-index: calc(var(--z-sticky) + 1); }
.cp-recovery-host:empty { display: none; }

/* ---- Code block + key field (conceal/reveal hygiene) ---------------------- */
.code-block { position: relative; margin: var(--space-2) 0; }
.code-block__pre { margin: 0; padding-right: var(--space-10); }
.code-block__copy { position: absolute; top: var(--space-2); right: var(--space-2); }
/* The "Copied" confirmation uses the readable OK FOREGROUND, not the mid-tone --ok dot colour
   (which was ~3.1:1 in light); --ok-fg clears 4.5:1 on the surface in both themes. */
.copy-btn--done { color: var(--ok-fg); }
.command-with-value { margin: var(--space-2) 0 var(--space-3); }
.key-field { margin: var(--space-2) 0; }
.key-field__label { display: block; margin-bottom: var(--space-1); }
.key-field__row { display: flex; align-items: center; gap: var(--space-2); background: var(--surface-inset); border: 1px solid var(--border); border-radius: var(--radius); padding: var(--space-2) var(--space-3); }
.key-field__value { flex: 1 1 auto; overflow-x: auto; white-space: nowrap; }

/* ---- Drawer + section + kv rows ------------------------------------------- */
.drawer-section { margin-bottom: var(--space-5); }
.drawer-section__title { font-size: var(--text-md); font-weight: var(--weight-semibold); margin-bottom: var(--space-3); }
.drawer-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.drawer-actions__danger { color: var(--danger-fg); margin-left: auto; }
/* Label/value rows in a drawer/detail/config section. A two-column grid — a stable
   label column + one fluid value column — with BOTH columns left-aligned and
   top-aligned, values wrapping cleanly. This is the orderly read of the website
   .map-table and the in-tree .run-facts; it replaces the old flex + text-align:right
   + word-break, which flung every value to a ragged right edge and split long lists
   mid-word (the "oddly positioned" complaint). One rule re-aligns every kvRow caller
   (the downpipe drawer, runs, map, licence) at once. */
.kv-row { display: grid; grid-template-columns: minmax(8rem, 11rem) 1fr; gap: var(--space-2) var(--space-4); align-items: start; padding: var(--space-2) 0; border-bottom: 1px solid var(--border-subtle); }
.kv-row:last-child { border-bottom: none; }
.kv-row__label { align-self: start; font-size: var(--text-sm); color: var(--text-muted); font-weight: var(--weight-medium); }
.kv-row__value { text-align: left; overflow-wrap: anywhere; min-width: 0; }
/* The same label-and-value reading WITHOUT the row rule, for a pair inside a card that already has its own
   separators, and wrapping rather than overhanging when the value will not fit beside the label.
   flex-wrap, not a grid with a media query, because the room this pair has depends on whether ITS OWN
   container is laid out one column or two, which the viewport width does not say: measured on console
   d9bc3c5e on /access/fallback, the "Cloudflare Access" trust badge painted 2.6px outside its value at a
   390 viewport and 50.6px at 890, where the session grid is two-up and each pair is narrower than it is on
   a phone. A badge is white-space:nowrap by design (a pill that wraps mid-phrase reads as two badges), so
   the pair has to give it a whole line rather than the badge give up its shape. */
.kv-pair { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-3); align-items: baseline; }
.kv-pair > .section-label { flex: 0 0 auto; min-width: 8rem; }
.kv-pair__value { flex: 1 1 14rem; min-width: 0; text-align: left; overflow-wrap: anywhere; }
/* The label column is a CONSISTENT width (not per-row max-content), so every value lines up
   into one column down the section — the orderly read the website .map-table gives. */

/* Destination cards laid two-up where the viewport allows, so the screen uses its width
   rather than one card per row (owner feedback). Collapses to one column when narrow. */
.dest-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(28rem, 1fr)); gap: var(--space-5); align-items: start; }

/* ---- Run strip (history timeline) ----------------------------------------- */
/* Status is conveyed by a NON-COLOUR channel as well as hue (WCAG 1.4.1): a failed cell carries a
   diagonal hatch and an inset X mark; an in-flight cell carries a dashed, hollow treatment. So an
   operator who cannot distinguish the red/blue/green fills can still tell failed and in-flight
   apart from a successful run by shape/texture, not colour alone. */
/* Cell sizing meets Target Size (Minimum) (WCAG 2.5.8): two of the strips are interactive
   (restore-flow, sources drawer), so cells are 20x24 with a 6px gap — a 24px circle centred on
   each cell clears its neighbours and their circles. Do not shrink below this on interactive
   strips. */
.run-strip { list-style: none; display: flex; flex-wrap: wrap; gap: 6px; }
.run-cell {
  position: relative;
  width: 20px; height: 24px; border-radius: 2px;
  background: var(--neutral); cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  transition: opacity var(--dur-fast) var(--ease-out);
}
.run-cell--ok { background: var(--ok); }
.run-cell--danger { background: var(--danger); }
.run-cell--info { background: var(--info); }
.run-cell--warn { background: var(--warn); }
.run-cell:focus-visible { outline: 2px solid var(--ring); outline-offset: 1px; }
/* E7: hovering/focusing one cell (run-context.ts setFocus) dims every OTHER cell to a subtle 88%,
   the calm sibling-dim idiom the map's live-flow hover-focus uses, scaled to a small status strip.
   The popover reuses .info-tip__pop untouched. */
.run-strip--focused .run-cell { opacity: 0.88; }
.run-strip--focused .run-cell--lit { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .run-cell { transition: none; } }

/* Failed: a diagonal hatch overlay (texture, not hue) plus a small inset X glyph. */
.run-cell--danger {
  background-image: repeating-linear-gradient(
    45deg,
    transparent 0, transparent 2px,
    var(--hatch) 2px, var(--hatch) 4px
  );
}
/* In-flight: a hollow, dashed treatment so it reads as "in progress" regardless of hue. */
.run-cell--inflight {
  background: transparent;
  border: 1.5px dashed var(--info);
}
/* Abandoned: the engine never got a verdict and reclaimed the run. It is neither a success nor a
   diagnosed failure nor still running, so it gets its own texture rather than borrowing one. A
   VERTICAL hatch, distinct from the failed cell's 45-degree hatch at a glance and in monochrome,
   plus its own inset glyph in the markup. Without this the state had no texture at all and read
   by hue alone, which is the one thing this strip is built not to do. */
.run-cell--warn {
  background-image: repeating-linear-gradient(
    90deg,
    transparent 0, transparent 2px,
    var(--hatch) 2px, var(--hatch) 4px
  );
}

/* The inset status mark (an X for failed). Sized to the small cell, high-contrast, decorative
   (the cell's aria-label carries the textual status, so the mark is aria-hidden in the markup). */
.run-cell__mark {
  font-size: 0.6875rem; font-weight: var(--weight-bold);
  line-height: 1; color: var(--danger-solid-fg);
  pointer-events: none;
}
.run-cell--inflight .run-cell__mark { color: var(--info); }
.run-cell--warn .run-cell__mark { color: var(--warn-solid-fg); }
.run-status-cell { display: inline-flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.run-status-cell__err { color: var(--danger-fg); }

/* ---- Restore calendar (point-in-time discovery, restore-flow/date-picker.ts) ----------------------- */
/* The "Browse runs to pick one" / "Browse by date" pair sit side by side, not stacked. */
.restore-flow__browse-row { display: flex; gap: var(--space-3); flex-wrap: wrap; }

.restore-calendar { display: grid; gap: var(--space-3); }
.restore-calendar__nav { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); }
.restore-calendar__month { margin: 0; font-size: var(--text-base); font-weight: var(--weight-medium); text-align: center; flex: 1; }

.restore-calendar__grid { display: flex; flex-direction: column; gap: 4px; }
.restore-calendar__week { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; }
.restore-calendar__weekdays { margin-bottom: 2px; }
.restore-calendar__weekday { text-align: center; font-size: var(--text-xs); color: var(--text-muted); font-weight: var(--weight-medium); }

/* Target Size (Minimum) (WCAG 2.5.8): the day grid is interactive (activating a day reveals that day's
   times), so cells stay at least 28px square, matching the run-strip's own interactive-cell floor. */
.cal-day {
  position: relative;
  aspect-ratio: 1 / 1; min-height: 28px;
  display: flex; align-items: center; justify-content: center;
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  border: 1px solid transparent;
}
.cal-day--blank { visibility: hidden; }
button.cal-day { cursor: pointer; }
.cal-day__num { pointer-events: none; }
.cal-day:focus-visible { outline: 2px solid var(--ring); outline-offset: 1px; }

/* available: the one positive restore affordance. The same tinted-badge combo (bg/fg/border) already
   verified elsewhere (badge--ok, stepper__item--done) -- never a solid --ok fill with running text on
   it (--ok-solid-fg only clears 3.3:1 in light, fine for a glyph, not for the day number). */
.cal-day--available { background: var(--ok-bg); color: var(--ok-fg); border-color: var(--ok-border); font-weight: var(--weight-medium); }
button.cal-day--available:hover { filter: brightness(0.96); }

/* failed-only: the tinted danger badge combo PLUS the same diagonal-hatch texture run-cell--danger uses
   (a non-colour cue, WCAG 1.4.1), so a day where nothing succeeded reads as its own distinct state and
   is never mistaken for "available". */
.cal-day--failed-only {
  background: var(--danger-bg); color: var(--danger-fg); border-color: var(--danger-border);
  background-image: repeating-linear-gradient(45deg, transparent 0, transparent 3px, var(--hatch) 3px, var(--hatch) 6px);
}

/* empty: the plain, unmarked default. An ordinary day the ring genuinely covers with nothing on it --
   deliberately styled like an everyday calendar cell, not muted, so it never reads as a limit. */
.cal-day--empty { color: var(--text); }

/* beyond-ring: a DISCOVERY limit of the recent-run history shown here, never a "gone" or "blocked"
   claim (see recovery-calendar.ts's header). Muted text plus a dashed outline -- a THIRD, distinct
   non-colour treatment from both the plain "empty" default and the failed-only hatch -- and reduced
   opacity; never a strike-through or a red/blocked hue, which would misread as unrecoverable. */
.cal-day--beyond-ring { color: var(--text-muted); opacity: 0.6; border-style: dashed; border-color: var(--border-subtle); }

.cal-day--selected { outline: 2px solid var(--ring); outline-offset: 1px; }

.restore-calendar__honesty { display: grid; gap: var(--space-2); padding: var(--space-3); border: 1px solid var(--border-subtle); border-radius: var(--radius); }

@media (prefers-reduced-motion: reduce) { button.cal-day--available:hover { filter: none; } }

/* ---- Forms (modal forms, create downpipe, restore) ------------------------ */
.form-stack { display: flex; flex-direction: column; gap: var(--space-4); }
.secrets-repeater { display: flex; flex-direction: column; gap: var(--space-3); }
.secrets-repeater__row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-3); }

/* ---- Restore plan review -------------------------------------------------- */
.restore-plan { margin-top: var(--space-5); }
.restore-stats { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: var(--space-3); margin: var(--space-4) 0; }
.restore-stat { background: var(--bg-subtle); border: 1px solid var(--border-subtle); border-radius: var(--radius); padding: var(--space-3); display: grid; gap: var(--space-1); }
.restore-stat__label { font-size: var(--text-xs); }
.restore-stat__value { font-size: var(--text-lg); font-weight: var(--weight-semibold); }
.restore-receipt { background: var(--ok-bg); border-color: var(--ok-border); }
.skipped-list { list-style: none; display: flex; flex-direction: column; gap: var(--space-1); }

/* ---- Dual-control approvals ----------------------------------------------- */
.approval-list { display: flex; flex-direction: column; gap: var(--space-4); }
/* R13: centre the pending-approval list in the console's standard prose measure. */
.approval-inbox { max-width: var(--measure-prose); margin-inline: auto; }
.approval-inbox__footnote { margin-top: var(--space-4); text-align: center; }
.approval-cues { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: var(--space-2); margin: var(--space-2) 0; }
.approval-cue { display: flex; flex-direction: column; }
.approval-cue__value { font-weight: var(--weight-medium); }
.approval-reason { margin: var(--space-2) 0; }
.approval-actions { display: flex; gap: var(--space-2); margin-top: var(--space-3); }
/* R6: the redirect-target chip (a short DANGER badge) + its trailing plain-text detail. */
.approval-target-redirect { display: inline-flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }

/* ---- Key ceremony: key-distribution panel + fingerprints + posture --------
   The "where each key goes" centrepiece. ONE refined instrument panel: three
   lanes split by hairlines, with NO tonal fills. The old teal/blue/green slabs
   read as decorative status boxes, which §1 ("semantic palette appears only to
   convey state, never to decorate") and §7a ("a SECTION is a heading + whitespace,
   not a card; rhythm comes from space, not borders and colour everywhere") both
   forbid. Structure now comes from one frame + hairline dividers + type weight;
   colour appears exactly ONCE (the trust accent on the lane that stays with the
   operator, carried by its icon chip only); and the vendor lane's "Nothing" is
   the quiet hero. The same classes style the Keys-screen legend (keys.ts), so the
   two surfaces read as one system. */
.custody-legend {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0;
  margin: var(--space-3) 0 var(--space-6);
  border: 1px solid var(--border); border-radius: var(--radius-lg);
  background: var(--surface); overflow: hidden;
}
.legend-col {
  border: none; background: transparent; border-radius: 0;
  padding: var(--space-5);
  display: flex; flex-direction: column; gap: var(--space-4);
}
.legend-col + .legend-col { border-left: 1px solid var(--border-subtle); }
/* The old tonal variants are retired to a flat surface; the trust lane keeps a
   single accent, carried by its icon chip only (below), never a full fill. */
.legend-col--trust, .legend-col--info, .legend-col--ok { background: transparent; border-color: transparent; }
.legend-col__title {
  display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-3);
  font-size: var(--text-sm); font-weight: var(--weight-semibold); color: var(--text); margin: 0;
}
/* The lane glyph sits in a quiet monochrome chip (a 28-30px square with a hairline,
   per the research brief), NOT a brand-coloured circle. */
.legend-col__title svg {
  width: 36px; height: 36px; padding: 8px; box-sizing: border-box; flex: none;
  border-radius: 10px; background: var(--bg-subtle);
  border: 1px solid var(--border-subtle); color: var(--text-muted);
}
/* The single trust accent on the page: the lane whose contents stay with the
   operator. The vendor lane is also .legend-col--trust but is the LAST child, so
   :first-child scopes the accent to the "stays in this browser" lane only. */
.legend-col--trust:first-child .legend-col__title svg {
  color: var(--trust); background: var(--trust-bg); border-color: var(--trust-border);
}
.legend-col__list { list-style: none; display: flex; flex-direction: column; gap: var(--space-3); font-size: var(--text-sm); }
/* Stack the filename above its caption ONLY for the onboarding pattern (a file row
   carries a .field__hint caption). The Keys-screen legend uses label + an inline
   "Download" link with no caption, so :has() leaves its inline layout untouched. */
.legend-col__list li:has(.field__hint) { display: flex; flex-direction: column; gap: 3px; }
.legend-col__list code.mono { font-size: var(--text-xs); color: var(--text); }
.legend-col__list .field__hint { font-size: var(--text-xs); line-height: var(--leading-snug); }
@media (max-width: 760px) {
  .custody-legend { grid-template-columns: 1fr; }
  .legend-col + .legend-col { border-left: none; border-top: 1px solid var(--border-subtle); }
}

/* ---- Offline break-glass custody step (blueprint 7.3) -------------------- */
/* The tiered storage menu, the per-tier panels, the per-custodian share rows and the
   inline notes. No animation is introduced here; the only transient is the shared
   .ob-spinner / .btn__spinner busy state, already reduced-motion gated above. Status is
   conveyed by the tone border/background AND a glyph AND a label, never colour alone. */
.custody-step { margin-top: var(--space-5); }
.custody-menu { display: flex; flex-direction: column; gap: var(--space-2); margin: var(--space-3) 0; }
.custody-menu__item { display: flex; align-items: flex-start; gap: var(--space-3); border: 1px solid var(--border); border-radius: var(--radius); padding: var(--space-3); cursor: pointer; }
.custody-menu__item:hover { border-color: var(--border-strong, var(--border)); }
.custody-menu__item:focus-within { outline: 2px solid var(--ring); outline-offset: 2px; }
.custody-menu__item input[type="radio"] { margin-top: 0.2rem; flex: none; }
.custody-menu__body { display: flex; flex-direction: column; gap: var(--space-1); }
.custody-menu__title { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.custody-panel { margin-top: var(--space-3); }
.custody-tier { border: 1px solid var(--border-subtle); border-radius: var(--radius); padding: var(--space-4); }
.custody-out { margin-top: var(--space-3); display: flex; flex-direction: column; gap: var(--space-2); }
.custody-note { border: 1px solid var(--border); border-radius: var(--radius); padding: var(--space-2) var(--space-3); margin-top: var(--space-2); }
.custody-note--info { border-color: var(--info-border); background: var(--info-bg); }
.custody-note--ok { border-color: var(--ok-border); background: var(--ok-bg); }
.custody-note--warn { border-color: var(--warn-border); background: var(--warn-bg); }
.custody-note--trust { border-color: var(--trust-border); background: var(--trust-bg); }
.custody-note--neutral { border-color: var(--border); background: var(--neutral-bg); }
.custody-note--danger { border-color: var(--danger-border); background: var(--danger-bg); }
.custody-share-dls { display: flex; flex-direction: column; gap: var(--space-2); }
.custody-share-dl { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; padding: var(--space-2); border: 1px solid var(--border-subtle); border-radius: var(--radius); }
.custody-share-dl__title { flex: 1 1 auto; min-width: 12rem; }
.custody-share-row { margin-bottom: var(--space-2); }
.custody-qr-chunks { display: flex; flex-direction: column; gap: var(--space-2); margin: var(--space-2) 0; }
.custody-qr-chunk { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }

.fingerprints { display: flex; flex-direction: column; gap: var(--space-2); }
.fp-row { display: flex; align-items: center; gap: var(--space-2); }
.fp-row__label { flex: none; min-width: 6rem; }
.fp-row__value { flex: 1 1 auto; overflow-x: auto; white-space: nowrap; }

/* ---- info-tip: the accessible "(i)" tooltip (components/info-tip.ts) -------
   A small, subdued, keyboard-reachable trigger with a popover anchored above it.
   The popover is shown by toggling [hidden] from script; the only motion is a
   fade that the global prefers-reduced-motion gate already collapses to instant. */
.info-tip__wrap { position: relative; display: inline-flex; vertical-align: middle; }
.info-tip {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.15rem; height: 1.15rem; padding: 0; margin: 0;
  border: none; background: none; color: var(--text-muted);
  border-radius: var(--radius-full); cursor: help; line-height: 0;
}
.info-tip:hover { color: var(--text); }
.info-tip:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; color: var(--text); }
.info-tip__pop {
  position: absolute; bottom: calc(100% + var(--space-2)); left: 50%;
  transform: translateX(-50%);
  z-index: var(--z-tooltip);
  width: max-content; max-width: 18rem;
  padding: var(--space-2) var(--space-3);
  background: var(--surface-raised); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  box-shadow: var(--shadow); font-size: var(--text-xs);
  font-weight: var(--weight-regular); line-height: var(--leading-snug);
  white-space: normal; text-align: left;
}
.info-tip__pop[hidden] { display: none; }

.recovery-actions, .settings-actions, .row-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.posture-grid { display: grid; grid-template-columns: 1fr; gap: var(--space-2); }
.posture-row { display: flex; flex-direction: column; gap: var(--space-1); padding: var(--space-2) 0; border-bottom: 1px solid var(--border-subtle); }
.posture-row:last-child { border-bottom: none; }

/* ---- Access verifier + setup ---------------------------------------------- */
.verifier-verdict { margin: var(--space-3) 0; }
.setup-steps { padding-left: var(--space-5); display: flex; flex-direction: column; gap: var(--space-2); }
.setup-steps__item { padding-left: var(--space-1); }

/* ---- Settings: theme control + connection -------------------------------- */
.theme-control { display: inline-flex; gap: var(--space-2); }
.theme-option {
  display: inline-flex; align-items: center; gap: var(--space-2);
  height: var(--control-h); padding: 0 var(--space-3);
  background: var(--surface); color: var(--text);
  border: 1px solid var(--control-border); border-radius: var(--radius);
  font-size: var(--text-sm); cursor: pointer;
}
/* Selected state for the Settings radiogroups (theme, rain, accessibility). The tint
   alone is too faint to read on the dark surface, so the cue is carried by a doubled
   accent edge (border + inset ring → reads as a clear 2px ring, no layout shift) and a
   medium-weight label, matching the .stepper__item--current idiom. */
.theme-option--active {
  background: var(--accent-subtle-bg); color: var(--accent-subtle-fg);
  border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent);
  font-weight: var(--weight-medium);
}
.conn-row { display: flex; align-items: flex-end; gap: var(--space-2); }
.conn-row .field { flex: 1 1 auto; }

/* ---- Onboarding wizard + stepper + readiness ------------------------------ */
.wizard { max-width: var(--measure-prose); margin: 0 auto; display: flex; flex-direction: column; gap: var(--space-5); }
.wizard__header { display: flex; flex-direction: column; gap: var(--space-3); align-items: flex-start; }
.wizard__sub { max-width: 52ch; }
.wizard__body { width: 100%; }
.wizard__nav { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-4); }
.stepper { list-style: none; display: flex; flex-wrap: wrap; gap: var(--space-2); }
.stepper__item { display: inline-flex; align-items: center; gap: var(--space-2); padding: var(--space-1) var(--space-3); border: 1px solid var(--border); border-radius: var(--radius-full); font-size: var(--text-sm); color: var(--text-muted); }
.stepper__item--done { border-color: var(--ok-border); color: var(--ok-fg); background: var(--ok-bg); }
.stepper__item--current { border-color: var(--accent); color: var(--accent-subtle-fg); background: var(--accent-subtle-bg); font-weight: var(--weight-medium); }
.stepper__marker {
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; border-radius: var(--radius-full);
  background: var(--surface); border: 1px solid currentColor; font-size: var(--text-xs);
}
.stepper__item--done .stepper__marker { background: var(--ok); color: var(--ok-solid-fg); border-color: var(--ok); }
.readiness-list { display: flex; flex-direction: column; gap: var(--space-2); }
.readiness-item { padding: var(--space-1) 0; }
.readiness-manual { padding-left: var(--space-5); display: flex; flex-direction: column; gap: var(--space-2); color: var(--text-muted); font-size: var(--text-sm); }
.configure-poll { display: flex; flex-direction: column; gap: var(--space-2); }

/* ---- Disclosure body (advanced sections) ---------------------------------- */
.disclosure { border: 1px solid var(--border-subtle); border-radius: var(--radius); padding: var(--space-2) var(--space-3); }
.disclosure > summary { cursor: pointer; font-size: var(--text-sm); font-weight: var(--weight-medium); }
.disclosure__body { display: flex; flex-direction: column; gap: var(--space-3); padding-top: var(--space-3); }

/* ---- Page header row (title + right-aligned actions) ----------------------- */
.page-header__row { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); flex-wrap: wrap; }
.page-header__actions { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }

/* ---- Misc layout wrappers (kept thin; most rely on inline gaps) ------------ */
.approval-card { margin: 0; }
.audit-verify:empty { display: none; }
.audit-verify { margin-bottom: var(--space-3); }
.confirm-impact { font-weight: var(--weight-medium); }
.restore-confirm { border-top: 1px solid var(--border-subtle); padding-top: var(--space-4); }
.conn-verdict { min-height: var(--control-h); }
.conn-row__copy { flex: none; }
.stat__value-row { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }

/* =============================================================================
   9.6 B2 shared components (console task 2/2): the reusable, Obsidian-led
   components the screens consume. Built only from the semantic tokens above, so
   each themes for free in Obsidian and the light companion. Contrast is a gate
   (WCAG 2.2 AA): status is hue + shape + label, focus rings clear 3:1, control
   boundaries use --control-border.
   ========================================================================== */

/* ---- Stat tile additions (label/value/secondary + status line + spark) ----- */
.stat__value { font-size: var(--text-2xl); font-weight: var(--weight-semibold); font-variant-numeric: tabular-nums; line-height: 1.1; letter-spacing: var(--tracking-tight); }
.stat__statusline { display: inline-flex; align-items: center; gap: var(--space-2); font-size: var(--text-sm); font-weight: var(--weight-medium); color: var(--text-muted); }
.stat__statusline .dot { flex: none; }
.stat__unknown { font-size: var(--text-md); font-weight: var(--weight-medium); color: var(--text-muted); }
.stat__value-skeleton { display: block; width: 60%; height: 28px; border-radius: var(--radius-sm); }
.stat__spark { margin-top: var(--space-1); color: var(--accent); }
/* A tile's secondary carries whatever the engine names: an operator's email, a bucket, a key id. Those are
   ONE unbroken token with no space to wrap at. .stat is a grid, and a grid item's automatic minimum size is
   its own min-content, so that token sets the floor for the tile's single track and the track grows past the
   tile: the text paints over the card edge and the label and value stretch with it (they share the track).
   Measured on the tour overview at 36af9bc: "Newest: a restore request by priya.nair@northwind.example."
   painted 32px OUTSIDE the Last audited card at 1440, and pushed the document 8px wider than a 1024
   viewport. overflow-wrap:anywhere (not break-word) both wraps the token AND lowers min-content so the
   track fits, which is why §20c uses anywhere; min-width:0 drops the automatic floor as well, the same
   pairing as .kv-row__value and .dp-name--cell. Prose secondaries are unaffected: anywhere breaks a word
   only when it cannot fit a line on its own. */
.stat > *, .stat__value-row > * { min-width: 0; }
.stat__label, .stat__value, .stat__secondary { overflow-wrap: anywhere; }
/* An interactive (clickable) stat tile: a real button that keeps the tile look and
   gains a hover/active affordance + the focus ring. */
.stat--button { text-align: left; cursor: pointer; font: inherit; color: var(--text); width: 100%; transition: border-color var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out); }
.stat--button:hover { border-color: var(--border-strong); background: var(--surface-hover); }
.stat--button:active { background: var(--surface-active); }

/* ---- Sparkline / mini-bars (inline SVG; currentColor + semantic tokens) ----- */
.spark-wrap { display: inline-flex; align-items: center; line-height: 0; }
.spark { display: block; overflow: visible; }
.spark--ok { color: var(--ok); }
.spark--warn { color: var(--warn); }
.spark--danger { color: var(--danger); }
.spark--info { color: var(--info); }
.spark--trust { color: var(--trust); }
.spark--muted { color: var(--text-muted); }
.spark--accent { color: var(--accent); }

/* ---- Data table: toolbar (filter + facets + count + density) ---------------- */
.data-table { display: flex; flex-direction: column; }
.dt-toolbar { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; margin-bottom: var(--space-3); }
.dt-filter-wrap { position: relative; flex: 1 1 260px; min-width: 200px; max-width: 380px; display: flex; align-items: center; }
.dt-filter-wrap svg { position: absolute; left: var(--space-3); width: 16px; height: 16px; color: var(--text-muted); pointer-events: none; }
.dt-filter { padding-left: var(--space-7); }
.dt-facets { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.dt-facet {
  display: inline-flex; align-items: center; gap: var(--space-2);
  height: var(--control-h-sm); padding: 0 var(--space-3);
  border: 1px solid var(--control-border); border-radius: var(--radius-full);
  background: var(--surface); color: var(--text); font-size: var(--text-sm);
  font-weight: var(--weight-medium); cursor: pointer; white-space: nowrap;
  transition: background var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.dt-facet:hover { background: var(--surface-hover); }
.dt-facet[aria-pressed="true"] { background: var(--accent-subtle-bg); color: var(--accent-subtle-fg); border-color: var(--accent-subtle-fg); }
.dt-facet .dot { flex: none; }
.dt-toolbar__right { margin-left: auto; display: flex; align-items: center; gap: var(--space-3); }
.dt-count { white-space: nowrap; font-variant-numeric: tabular-nums; }

/* Density segmented control */
.dt-density { display: inline-flex; border: 1px solid var(--control-border); border-radius: var(--radius); overflow: hidden; height: var(--control-h-sm); }
.dt-density__btn { height: 100%; padding: 0 var(--space-3); background: var(--surface); color: var(--text-muted); border: none; font-size: var(--text-sm); cursor: pointer; }
.dt-density__btn + .dt-density__btn { border-left: 1px solid var(--border); }
.dt-density__btn[aria-pressed="true"] { background: var(--accent-subtle-bg); color: var(--accent-subtle-fg); }
.dt-density__btn:focus-visible { outline: 2px solid var(--ring); outline-offset: -2px; }

/* Data table: sortable header control + caret (aria-sort drives the caret) */
.dt-th--sortable { padding: 0; }
.dt-sort {
  display: inline-flex; align-items: center; gap: var(--space-1);
  width: 100%; height: 100%; padding: var(--space-3) var(--space-4);
  background: transparent; border: none; cursor: pointer; color: inherit;
  font: inherit; font-size: var(--text-xs); font-weight: var(--weight-semibold);
  text-align: left;
}
.dp-table th.num .dt-sort { justify-content: flex-end; }
.dt-sort:hover { color: var(--text); }
/* Resting caret at 0.8 (UIA-10): 0.4 blended the muted tone to ~1.7-2.2:1 — the
   sortable affordance was below the 3:1 non-text floor. */
.dt-sort__caret { display: inline-block; width: 0; height: 0; border-left: 4px solid transparent; border-right: 4px solid transparent; opacity: 0.8; }
th[aria-sort="ascending"] .dt-sort__caret { border-bottom: 5px solid currentColor; opacity: 1; }
th[aria-sort="descending"] .dt-sort__caret { border-top: 5px solid currentColor; opacity: 1; }

/* Data table: selection (checkbox column + selected row) */
.dt-col-check { width: 40px; text-align: center; }
.dt-check { width: 16px; height: 16px; accent-color: var(--accent); cursor: pointer; vertical-align: middle; }
.dt-check:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
.dp-table tbody tr[aria-selected="true"] { background: var(--accent-subtle-bg); }
.dp-table tbody tr[aria-selected="true"]:hover { background: var(--accent-subtle-bg); }

/* Data table: bulk-action bar (appears on selection; a polite live region) */
.dt-bulkbar {
  display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-2) var(--space-4); margin-bottom: var(--space-3);
  /* Border on --accent (not --accent-ring): the ring tone was 2.95:1 against the
     tint in light — a hairline miss of the 3:1 boundary floor (UIA-21). */
  background: var(--accent-subtle-bg); border: 1px solid var(--accent);
  border-radius: var(--radius); animation: dp-rise var(--dur) var(--ease-out);
}
.dt-bulkbar__n { font-size: var(--text-sm); font-weight: var(--weight-semibold); color: var(--accent-subtle-fg); }
.dt-bulkbar__actions { margin-left: auto; display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.dt-bulkbar__clear { color: var(--text-muted); }

/* Data table: filtered-to-empty (distinct from true-empty so data never seems lost) */
.dt-filtered-empty { text-align: center; }

/* Data table: windowing viewport (large lists). The viewport scrolls; spacer rows
   keep the scrollbar honest while only the visible window renders. */
.dt-windowed-wrap { width: 100%; }
.dt-viewport { width: 100%; }
.dt-table--windowed { table-layout: fixed; }
.dt-spacer td { border: none !important; padding: 0 !important; }

/* ---- Wizard / stepper additions (link + blocked + label) -------------------- */
.stepper__label { white-space: nowrap; }
.stepper__link {
  display: inline-flex; align-items: center; gap: var(--space-2);
  background: none; border: none; padding: 0; margin: 0; cursor: pointer;
  color: inherit; font: inherit; font-size: var(--text-sm);
}
.stepper__link:hover { text-decoration: underline; }
.stepper__link:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; border-radius: var(--radius-sm); }
.stepper__blocked { display: inline-flex; align-items: center; gap: var(--space-2); opacity: 0.6; cursor: not-allowed; }
.stepper__item--blocked { border-color: var(--border); color: var(--text-muted); }
.stepper__item--blocked .stepper__marker { color: var(--text-muted); }
.stepper__item--upcoming { color: var(--text-muted); }
.wizard__stepper { width: 100%; }

/* ---- Verdict / trust surface (honest states; hue + shape + label) ----------- */
.verdict {
  border: 1px solid var(--border); border-radius: var(--radius-lg);
  padding: var(--space-4) var(--space-5); background: var(--surface);
  display: flex; flex-direction: column; gap: var(--space-2);
}
.verdict__head { display: flex; align-items: flex-start; gap: var(--space-3); }
.verdict__icon { flex: none; display: inline-flex; margin-top: 1px; }
.verdict__headtext { min-width: 0; }
.verdict__title { font-size: var(--text-md); font-weight: var(--weight-semibold); }
.verdict__body { color: var(--text); font-size: var(--text-sm); }
.verdict__caveat { margin-top: 0; }
.verdict__actions { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-2); }
/* The verified (teal trust) state: the only teal family, distinct from action + ok. */
.verdict--trust { border-color: var(--trust-border); background: var(--trust-bg); }
.verdict--trust .verdict__icon, .verdict--trust .verdict__title { color: var(--trust-fg); }
/* Cautious + honest states. */
.verdict--warn { border-color: var(--warn-border); background: var(--warn-bg); }
.verdict--warn .verdict__icon, .verdict--warn .verdict__title { color: var(--warn-fg); }
.verdict--danger { border-color: var(--danger-border); background: var(--danger-bg); }
.verdict--danger .verdict__icon, .verdict--danger .verdict__title { color: var(--danger-fg); }
.verdict--ok { border-color: var(--ok-border); background: var(--ok-bg); }
.verdict--ok .verdict__icon, .verdict--ok .verdict__title { color: var(--ok-fg); }
.verdict--info { border-color: var(--info-border); background: var(--info-bg); }
.verdict--info .verdict__icon, .verdict--info .verdict__title { color: var(--info-fg); }
.verdict--neutral { border-color: var(--border); background: var(--bg-subtle); }
.verdict--neutral .verdict__icon { color: var(--text-muted); }

/* ---- Detail drawer (rich, deep-linkable; over the shared overlay engine) ---- */
.detail-drawer__content { display: flex; flex-direction: column; gap: var(--space-5); }
.detail-drawer__subhead { display: flex; flex-direction: column; gap: var(--space-2); }
.detail-drawer__badges { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.detail-drawer__meta { font-size: var(--text-sm); color: var(--text-muted); }

/* Underline tablist (role=tablist; the active tab is underlined; roving tabindex).
   Named for the detail drawer, which is no longer a consumer: its own tabset was
   removed on 2026-08-08 for having had no call site. The live consumers are the
   three ROUTE tablists that build their own markup on this pattern, /access
   (access-security.ts), /notifications (notifications.ts) and /keys
   (keys/shared.ts). The class names are kept as they are because those three name
   them, and renaming CSS a hundred call sites read is not what this change is. */
.drawer-tablist { display: flex; gap: var(--space-1); border-bottom: 1px solid var(--border); }
.drawer-tab {
  position: relative; height: var(--control-h); padding: 0 var(--space-3);
  background: transparent; border: none; cursor: pointer;
  color: var(--text-muted); font: inherit; font-size: var(--text-sm); font-weight: var(--weight-medium);
}
.drawer-tab:hover { color: var(--text); }
.drawer-tab[aria-selected="true"] { color: var(--text); }
.drawer-tab[aria-selected="true"]::after {
  content: ""; position: absolute; left: var(--space-2); right: var(--space-2); bottom: -1px;
  height: 2px; border-radius: var(--radius-full); background: var(--accent);
}
.drawer-tab:focus-visible { outline: 2px solid var(--ring); outline-offset: -2px; border-radius: var(--radius-sm); }
.drawer-tabpanel { outline: none; }
.drawer-tabpanel:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; border-radius: var(--radius-sm); }
/* WCAG 2.4.7 / 2.4.11 (C1-03): area tabpanel focus ring; negative offset keeps the ring inside
   the panel boundary so it is not clipped by the container's overflow. Token matches drawer-tab. */
.area-panel:focus-visible { outline: 2px solid var(--ring); outline-offset: -2px; }

/* Compact: dialogs and drawers go full width; tables stay scrollable. */
@media (max-width: 767px) {
  .overlay--drawer { align-items: stretch; }
  .dialog--drawer { width: 100vw; }
  .dialog--modal { width: 100%; }
  .secrets-repeater__row { grid-template-columns: 1fr; }
  .toast-region { left: var(--space-3); right: var(--space-3); transform: none; max-width: none; align-items: stretch; }
  /* Data table toolbar stacks; the filter and the right cluster take full width so
     nothing overflows at 320px (WCAG 1.4.10 reflow). */
  .dt-toolbar { flex-direction: column; align-items: stretch; }
  .dt-filter-wrap { max-width: none; }
  .dt-toolbar__right { margin-left: 0; justify-content: space-between; }
  .dt-bulkbar { flex-wrap: wrap; }
  .dt-bulkbar__actions { margin-left: 0; width: 100%; }
}

/* =============================================================================
   9b. Sources + downpipes screen (screen 2): the source-type pills, the
   create/edit drawer's segmented source-type control + Secrets repeater, the
   schedule preset picker, the live KV cost projection with its bill-shock warn
   state, the destination clarity block, the freshness/next-run cues, and the
   bulk partial-success summary. All composed from the semantic token layer so
   they theme for free in Obsidian (dark default) and the light companion;
   status is hue + shape + label, never colour alone. Built to the screen-2
   wireframe (design/02-areas/sources-downpipes/wireframe.html).
   ========================================================================== */

/* Source-type pill: a small, uppercase, hue-coded chip carrying a glyph + the
   type word (KV / R2 / Secrets / D1), so the source layer reads at a glance.
   The label text is always present (not colour alone). */
.source-pill {
  display: inline-flex; align-items: center; gap: var(--space-1);
  min-height: 20px; padding: 0 var(--space-2);
  border-radius: var(--radius-sm);
  font-size: 0.6875rem; font-weight: var(--weight-semibold);
  letter-spacing: 0.02em; text-transform: uppercase;
  border: 1px solid transparent; white-space: nowrap;
}
.source-pill svg { width: 13px; height: 13px; flex: none; }
.source-pill--kv      { background: var(--accent-subtle-bg); color: var(--accent-subtle-fg); }
.source-pill--r2      { background: var(--info-bg);  color: var(--info-fg);  border-color: var(--info-border); }
.source-pill--d1      { background: var(--neutral-bg); color: var(--neutral-fg); border-color: var(--border); }
.source-pill--secrets { background: var(--warn-bg);  color: var(--warn-fg);  border-color: var(--warn-border); }
.source-pill--cf-config { background: var(--trust-bg); color: var(--trust-fg); border-color: var(--trust-border); }
.source-pill--workers { background: var(--accent-subtle-bg); color: var(--accent-subtle-fg); border-color: var(--border); }

/* The name cell: the link-like name on top, the binding (or secret count) in
   muted monospace beneath. */
.dp-name { display: flex; flex-direction: column; gap: 2px; line-height: var(--leading-tight); }
.dp-name__sub { font-size: var(--text-xs); color: var(--text-muted); }

/* The downpipes Name cell caps the identity + binding so a maximal-length name (config.name is 1 to 256
   chars) ellipsises inside the cell rather than ballooning the row and shoving the page-header primary
   controls off-screen (G-P1-145). The full text stays available via each element's title attribute, and the
   row still opens the detail drawer where the name is the heading. min-width:0 lets the flex children shrink;
   overflow-wrap:anywhere lets a long unbroken token wrap on the card-stack (below), never spill the card. */
.dp-name--cell { min-width: 0; }
.dp-name--cell .dp-name__label,
.dp-name--cell .dp-name__sub { overflow-wrap: anywhere; }
/* Wide table layout only (the complement of the =<1023 card-stack in §20c): single-line, capped, ellipsised
   so the eight-column row cannot be widened past the viewport by the name. Below 1024 the name is the card
   title and wraps (it has the room), so the ellipsis is not applied there. */
@media (min-width: 1024px) {
  .dp-name--cell .dp-name__label,
  .dp-name--cell .dp-name__sub {
    display: block;
    max-width: 20rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}

/* A short, inline run error reason under the last-run cell (coarse, never a
   stack). Hue + an alert glyph + the reason text. */
.run-reason { display: inline-flex; align-items: center; gap: var(--space-1); margin-top: 2px; font-size: var(--text-xs); color: var(--danger-fg); }
.run-reason svg { width: 11px; height: 11px; flex: none; }

/* The segmented source-type control in the create/edit drawer (a real
   radiogroup of buttons; the pressed one lifts onto the surface). */
.type-seg {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-1);
  padding: var(--space-1); background: var(--bg-subtle);
  border: 1px solid var(--border); border-radius: var(--radius);
}
.type-seg__btn {
  height: 32px; border: none; background: transparent; color: var(--text-muted);
  border-radius: var(--radius-sm); font-size: var(--text-sm); font-weight: var(--weight-medium);
  cursor: pointer; display: inline-flex; align-items: center; justify-content: center; gap: var(--space-1);
}
.type-seg__btn svg { width: 14px; height: 14px; }
.type-seg__btn[aria-checked="true"] { background: var(--surface); color: var(--text); box-shadow: var(--shadow-sm); }
.type-seg__btn:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }

/* The Secrets repeater row gains a trailing delete button column (the base
   .secrets-repeater / __row already exist; this adds the delete affordance). */
.secrets-repeater__row { grid-template-columns: 1fr 1fr auto; align-items: end; }
.secrets-repeater__del { height: var(--control-h); align-self: end; }

/* The schedule preset picker: friendly cadence presets as radio-style cards. */
.sched-presets { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-2); }
.sched-preset {
  display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border-strong); border-radius: var(--radius);
  background: var(--surface); cursor: pointer; text-align: left;
}
.sched-preset:hover { background: var(--surface-hover); }
.sched-preset[aria-checked="true"] { border-color: var(--accent); background: var(--accent-subtle-bg); }
.sched-preset[aria-checked="true"] .sched-preset__title { color: var(--accent-subtle-fg); }
.sched-preset:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
.sched-preset__title { font-size: var(--text-sm); font-weight: var(--weight-medium); color: var(--text); }
.sched-preset__sub { font-size: 0.6875rem; color: var(--text-muted); }
.sched-nextrun { font-size: var(--text-sm); color: var(--text-muted); margin-top: var(--space-2); display: flex; align-items: center; gap: var(--space-2); }
.sched-nextrun svg { width: 14px; height: 14px; flex: none; color: var(--text-muted); }
.sched-nextrun strong { color: var(--text); font-weight: var(--weight-medium); }

/* The live KV cost projection line. Neutral (within budget) by default; the
   bill-shock state flips to the warn tone with an alert glyph and the one-click
   "Use daily" steer. Tabular numerals for the read count. It never hard-blocks. */
.cost-line {
  display: flex; align-items: flex-start; gap: var(--space-2);
  padding: var(--space-3) var(--space-4); margin-top: var(--space-3);
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--bg-subtle); font-size: var(--text-sm); color: var(--text);
}
.cost-line svg { width: 16px; height: 16px; flex: none; margin-top: 2px; color: var(--text-muted); }
.cost-line__body { flex: 1 1 auto; min-width: 0; }
.cost-line__num { font-weight: var(--weight-semibold); font-variant-numeric: tabular-nums; }
.cost-line__detail { margin-top: 2px; color: var(--text-muted); }
.cost-line__steer { margin-top: var(--space-2); }
.cost-line--warn { background: var(--warn-bg); border-color: var(--warn-border); color: var(--warn-fg); }
.cost-line--warn svg { color: var(--warn); }
.cost-line--warn .cost-line__detail { color: var(--warn-fg); }

/* The destination clarity block: informational presence + residency, never a
   credential form (no custody). A row with an icon, the destination label and
   the region, and a reassurance note. */
.dest-block { border: 1px solid var(--border); border-radius: var(--radius); padding: var(--space-4); display: flex; flex-direction: column; gap: var(--space-3); }
.dest-block__opt { display: flex; gap: var(--space-3); align-items: flex-start; }
.dest-block__ic { width: 32px; height: 32px; border-radius: var(--radius-sm); background: var(--accent-subtle-bg); color: var(--accent-subtle-fg); display: flex; align-items: center; justify-content: center; flex: none; }
.dest-block__ic svg { width: 18px; height: 18px; }
.dest-block__title { font-size: var(--text-sm); font-weight: var(--weight-medium); display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.dest-block__desc { font-size: var(--text-xs); color: var(--text-muted); margin-top: 2px; }
.dest-block__note { font-size: var(--text-xs); color: var(--text-muted); display: flex; align-items: flex-start; gap: var(--space-2); }
.dest-block__note svg { width: 14px; height: 14px; flex: none; margin-top: 1px; color: var(--trust); }
.dest-block--warn { border-color: var(--warn-border); background: var(--warn-bg); }
.dest-block--warn .dest-block__note { color: var(--warn-fg); }
.dest-block--warn .dest-block__note svg { color: var(--warn); }

/* The freshness banner inside the detail drawer (a stale or failed downpipe
   leads with an honest, hue + shape + label summary). */
.freshness { display: flex; gap: var(--space-2); align-items: flex-start; padding: var(--space-3) var(--space-4); border-radius: var(--radius); border: 1px solid var(--border-subtle); background: var(--bg-subtle); font-size: var(--text-sm); }
.freshness__dot { margin-top: 4px; flex: none; }
.freshness--warn { border-color: var(--warn-border); background: var(--warn-bg); }
.freshness--danger { border-color: var(--danger-border); background: var(--danger-bg); }
.freshness--ok { border-color: var(--ok-border); background: var(--ok-bg); }

/* The bulk partial-success summary (no transaction; each item is its own
   request). Per-item failures are real, focusable text rows. */
.bulk-summary { display: flex; flex-direction: column; gap: var(--space-3); }
.bulk-summary__head { font-size: var(--text-sm); color: var(--text); }
.bulk-summary__list { display: flex; flex-direction: column; gap: var(--space-2); font-size: var(--text-sm); }
.bulk-summary__item { display: flex; align-items: center; gap: var(--space-2); }
.bulk-summary__item .mono { color: var(--text); }
.bulk-summary__reason { color: var(--text-muted); }
.bulk-progress { font-size: var(--text-sm); color: var(--text-muted); display: flex; align-items: center; gap: var(--space-2); }

/* The bulk-create (import a list) textarea is monospace, one binding per line. */
.import-list { min-height: calc(var(--control-h) * 4); font-family: var(--font-mono); font-size: var(--text-sm); }

/* A small inline cell-level error/marker on a row after a failed optimistic
   write (enable/disable), kept on the row rather than a global toast. */
.cell-error { font-size: var(--text-xs); color: var(--danger-fg); display: inline-flex; align-items: center; gap: var(--space-1); }

/* The danger zone inside the create/edit drawer / detail footer is handled by
   the existing .drawer-actions__danger; nothing new needed there. */

/* Reflow: at 320px the segmented control and presets collapse to two columns so
   the touch targets stay >= the control height and nothing overflows. */
@media (max-width: 560px) {
  .type-seg { grid-template-columns: repeat(2, 1fr); }
  .sched-presets { grid-template-columns: repeat(2, 1fr); }
  .secrets-repeater__row { grid-template-columns: 1fr 1fr; }
  .secrets-repeater__del { grid-column: 1 / -1; justify-self: start; }
}

/* =============================================================================
   9.7 Restore-flow screen (console task 2/2, screen: restore-flow). The elevated
   composites the restore hero + run context need that the B1 layer did not yet
   carry: the two-column workspace, the rich run-detail facts, the restore-flow
   stepper, the two-distinct target-choice cards, the dry-run review hero (impact
   banner + figure grid + resolved-destinations + integrity line), and the receipt
   (clean + honest-partial). Built ONLY from the semantic tokens above, so each
   themes for free in Obsidian and the light companion; status is hue + shape +
   label; focus rings clear 3:1; the layout reflows to 320px.
   ========================================================================== */

/* The two-column workspace: run context (left) + the restore flow (right). The
   flow is the primary column, so it leads on a single column at narrow widths. */
.restore-workspace { display: grid; grid-template-columns: minmax(0, 360px) minmax(0, 1fr); gap: var(--space-5); align-items: start; }
/* R5: the context rail travels with the scroll rather than stranding a void once the flow column
   (plan + confirm + receipt) grows taller than the run-detail card beside it. top clears the
   sticky context-bar header (--header-h) plus a comfortable gap; align-items:start on the grid
   (above) already sizes this item's containing block to the full (taller) row, which is what
   gives position:sticky real room to travel. Reset to static in the stacked layout below, where a
   single column gives sticky nothing meaningful to do. */
.restore-workspace__context { display: block; min-width: 0; position: sticky; top: calc(var(--header-h) + var(--space-5)); }
.restore-workspace__flow { min-width: 0; }
/* R1: the runless landing (no run chosen yet) has no context column, so the lone flow child would
   otherwise auto-place into this grid's narrow 360px track (the "cramped" bug); --solo drops to a
   single full-width column. The flow itself renders at a comfortable reading measure within it
   (below); the recency band spans the full solo width beneath it (R1/R2a). */
.restore-workspace--solo { grid-template-columns: minmax(0, 1fr); }
.restore-workspace--solo .restore-workspace__flow { max-width: 880px; width: 100%; margin-inline: auto; }
/* R2a: a defensive overflow guard so a mis-sized child (a long run id, a long binding name) can
   never paint under a sibling column; min-width:0 lets a grid/flex item shrink below its content's
   natural width instead of forcing the track wider than its share. */
.restore-workspace__recency { min-width: 0; }
/* R9: the compact mobile identity strip (downpipe id + run id + sealed status). A grid sibling
   (not nested in the flow or context column) so `order` can place it first on narrow widths; hidden
   above 1100px (the desktop run-detail card already carries this) and hidden until its data
   resolves (.is-ready), so it never flashes an empty box while the history fetch is in flight. */
.restore-identity-strip { display: none; min-width: 0; }
@media (max-width: 1100px) {
  .restore-workspace { grid-template-columns: 1fr; }
  /* The flow comes first on a stacked layout; the run context follows it. Sticky has nothing
     meaningful to do in a single column (the row height collapses to each item's own content), so
     it is reset to static rather than left to a no-op default. */
  .restore-identity-strip { order: 0; }
  .restore-workspace__flow { order: 1; }
  .restore-workspace__context { order: 2; position: static; }
  .restore-identity-strip.is-ready {
    display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap;
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--border-subtle); border-radius: var(--radius);
    background: var(--bg-subtle);
    font-size: var(--text-sm);
    margin-bottom: var(--space-3);
  }
}

/* Run-detail card: head row + a definition-list of run facts. */
.restore-detail__head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); flex-wrap: wrap; }
.run-facts { display: grid; grid-template-columns: max-content 1fr; gap: var(--space-2) var(--space-4); font-size: var(--text-sm); margin: 0; }
.run-facts dt { color: var(--text-muted); }
.run-facts dd { color: var(--text); margin: 0; overflow-wrap: anywhere; }
.run-id-cell { display: inline-flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }

/* Run-status timeline legend (the .run-strip cells themselves are B1). */
.run-strip__legend { display: flex; gap: var(--space-4); margin-top: var(--space-3); flex-wrap: wrap; }
.run-strip__legenditem { display: inline-flex; align-items: center; gap: var(--space-1); font-size: var(--text-xs); }

/* Drill (recoverability) panel result + action row. */
.drill-result { display: grid; gap: var(--space-2); }
.drill-actions { margin-top: var(--space-3); }

/* The restore-flow stepper: pick -> review -> confirm -> apply -> receipt. Status
   is a marker SHAPE (a tick for done, a number otherwise) + the hue + the label,
   never colour alone (WCAG 1.4.1). */
.restore-steps { list-style: none; display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; margin: 0; padding: 0; }
.restore-step { display: inline-flex; align-items: center; gap: var(--space-2); font-size: var(--text-sm); color: var(--text-muted); }
.restore-step--current { color: var(--text); font-weight: var(--weight-medium); }
.restore-step__num {
  display: grid; place-items: center; width: 22px; height: 22px; border-radius: var(--radius-full);
  font-size: var(--text-xs); font-weight: var(--weight-semibold);
  border: 1px solid var(--border-strong); color: var(--text-muted); background: var(--surface);
}
.restore-step--done .restore-step__num { background: var(--ok); color: var(--ok-solid-fg); border-color: var(--ok); }
.restore-step--current .restore-step__num { background: var(--accent); color: var(--accent-fg); border-color: var(--accent); }
/* R8: the connector between two steps. position:relative + overflow:hidden give the E1 fill
   (.restore-step__sep--fill::after) a contained box to reveal inside. */
.restore-step__sep { position: relative; overflow: hidden; flex: 1 1 18px; min-width: 12px; max-width: 24px; height: 1px; background: var(--border-strong); list-style: none; }
/* E1: the ONE connector that just crossed into "done" this paint (restoreStepper marks it via its
   optional prev arg) reveals left-to-right in --ok over 220ms, a one-shot acknowledgement of the
   step just completed. clip-path (not a scale/transform) does the reveal, so this is a plain wipe,
   never a pop. Only the single segment just crossed gets it, and only on a genuine forward step, so
   a same-step re-render and the Change / re-arm reset back to step 1 never animate. Reduced motion
   collapses the duration to near-zero via the global reduced-motion gate. */
.restore-step__sep--fill::after { content: ""; position: absolute; inset: 0; background: var(--ok); clip-path: inset(0 100% 0 0); animation: restore-step-fill 220ms var(--ease-out) both; }
@keyframes restore-step-fill { to { clip-path: inset(0 0 0 0); } }
/* Defensive: the DOM-last step never carries a trailing separator (restoreStepper already omits
   it), but this guarantees it at the CSS layer too. */
.restore-step:last-child .restore-step__sep { display: none; }
/* Evenly distribute the six steps across the full width on desktop: each non-last step grows to an
   equal share and its connector fills the remaining space in that share, so the markers sit on an
   even rhythm across the canvas rather than bunching at the left (the old max-width:24px connector).
   Scoped to >=641px so the wrapping mobile layout below (connectors hidden, natural width) is left
   untouched. */
@media (min-width: 641px) {
  .restore-steps { width: 100%; flex-wrap: nowrap; gap: 0; }
  .restore-step:not(:last-child) { flex: 1 1 0; min-width: 0; }
  /* gap on the GROUP, not margin-left on the connector. A margin on a flex item that is itself flexing to
     fill (flex: 1 1 auto) is added AFTER the free space is distributed, so the connector paints past its
     own row: measured by spec/visual/route-set-containment.spec.ts at 1.3px past the right at 670 and
     12.1px at 1280 on /restore/run-ledger-0009. gap is accounted for when the free space is worked out, so
     the same spacing costs the row nothing. min-width:0 lets the connector give way first if it still runs
     short, rather than the row growing. */
  .restore-step:not(:last-child) .restore-step-group { display: flex; width: 100%; min-width: 0; gap: var(--space-1); }
  .restore-step:not(:last-child) .restore-step__sep { flex: 1 1 auto; max-width: none; min-width: 0; }
  .restore-step__label { white-space: nowrap; }
}
/* The 768-1023 DOCKED DEAD ZONE (the same band the account chip already names): the rail is back in the
   grid at 240px, so the flow column is NARROWER here than it is at 640 with the rail as a slide-over (464px
   against 608px at 640), yet the >=641 block above pins the stepper flex-wrap:nowrap with nowrap labels.
   The stepper's 605px min-content then spilled the card and scrolled /restore sideways by 109px at 768
   (scrollWidth 877). Let it wrap in this band exactly as it does below 641, and drop the connector that
   would dangle at a wrapped row's edge for the same reason the <=640 rule below drops it. */
@media (min-width: 768px) and (max-width: 1023px) {
  .restore-steps { flex-wrap: wrap; gap: var(--space-2); }
  .restore-step:not(:last-child) { flex: 0 1 auto; }
  .restore-step:not(:last-child) .restore-step-group { display: inline-flex; width: auto; }
  .restore-step__sep { display: none; }
}

/* R8: suppress the dangling trailing connector at a WRAPPED row's right edge. Flexbox exposes no
   "last item in this visual row" selector, so a byte-perfect fix is not possible in CSS alone; the
   six steps reliably wrap onto more than one line below this width (narrower than that, the
   connector reliably dangles at a row break), so the honest, low-risk mitigation is to drop it
   there. The marker + label sequence still reads clearly without it. */
@media (max-width: 640px) {
  .restore-step__sep { display: none; }
}

/* R4: the collapsed one-line summary of the picked run + target ("Restoring run-x to original
   bindings"), replacing the pick-run form once a plan builds successfully. A labelled region (not
   bare prose), so it carries correct semantics without claiming a heading (design-system 7a). */
.restore-pick-summary {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-3);
  flex-wrap: wrap;
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--border); border-radius: var(--radius-lg);
  background: var(--bg-subtle);
  min-width: 0;
}
.restore-pick-summary__text { color: var(--text); min-width: 0; overflow-wrap: anywhere; }

.restore-flow__build { margin-top: var(--space-4); }
/* R12: the "pick a run to restore" modal list. */
.run-picker { display: flex; flex-direction: column; gap: var(--space-2); max-height: 60vh; overflow-y: auto; }
.run-picker__row { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); width: 100%; font: inherit; color: var(--text); text-align: left; padding: var(--space-3) var(--space-4); border: 1px solid var(--border-subtle); border-radius: var(--radius); background: var(--surface); cursor: pointer; transition: border-color var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out); }
.run-picker__row:hover { border-color: var(--accent); background: var(--accent-subtle-bg); }
.run-picker__row:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
.run-picker__main { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }
.run-picker__run { overflow-wrap: anywhere; }
.run-picker__dp { font-size: var(--text-sm); color: var(--text-muted); overflow-wrap: anywhere; }
.run-picker__meta { display: flex; flex-direction: column; align-items: flex-end; gap: var(--space-1); flex: none; }
.run-picker__age { white-space: nowrap; }
/* R16: disclosure sub-heading + two-up field grid (collapses to one column on narrow). */
.restore-subhead { margin: 0; font-size: var(--text-sm); font-weight: var(--weight-semibold); color: var(--text-muted); }
.restore-field-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); align-items: start; }
.restore-field-grid > .restore-field-grid__span { grid-column: 1 / -1; }
@media (max-width: 640px) { .restore-field-grid { grid-template-columns: 1fr; } }

/* R5: the context rail's live journey summary (journey.ts): current step, plan hash once one
   exists, and the approval state, a few calm rows above the run-detail card. */
.restore-journey { padding-bottom: var(--space-4); margin-bottom: var(--space-4); border-bottom: 1px solid var(--border-subtle); }
.restore-journey__title {
  font-size: var(--text-xs); font-weight: var(--weight-semibold); color: var(--text-muted);
  text-transform: uppercase; letter-spacing: var(--tracking-wide); margin: 0 0 var(--space-2);
}
.restore-journey__facts { display: grid; grid-template-columns: max-content 1fr; gap: var(--space-1) var(--space-3); font-size: var(--text-sm); margin: 0; }
.restore-journey__facts dt { color: var(--text-muted); }
.restore-journey__value { color: var(--text); margin: 0; font-weight: var(--weight-medium); }
.restore-journey__hash { margin-top: var(--space-2); font-size: var(--text-xs); display: flex; align-items: center; gap: var(--space-1); flex-wrap: wrap; }

/* The impact summary banner (the review hero): an unmissable, real-text statement,
   hue + icon + words, escalating for a non-latest or redirected restore. */
.impact { border-radius: var(--radius-lg); padding: var(--space-4) var(--space-5); border: 1px solid; display: grid; gap: var(--space-2); margin-bottom: var(--space-4); }
.impact--warn { background: var(--warn-bg); border-color: var(--warn-border); }
.impact--danger { background: var(--danger-bg); border-color: var(--danger-border); }
.impact__head { display: flex; align-items: center; gap: var(--space-2); font-weight: var(--weight-semibold); font-size: var(--text-md); }
.impact--warn .impact__head { color: var(--warn-fg); }
.impact--danger .impact__head { color: var(--danger-fg); }
.impact__head svg { flex: none; }
.impact__line { font-size: var(--text-base); color: var(--text); margin: 0; }
.impact__escalate { font-size: var(--text-sm); font-weight: var(--weight-medium); margin: 0; }
.impact--warn .impact__escalate { color: var(--warn-fg); }
.impact--danger .impact__escalate { color: var(--danger-fg); }

/* Plan figure grid (tabular numerals; the finance-grade read). */
/* auto-fit with a real floor, not four fixed tracks. The room these tiles have depends on whether the
   restore WORKSPACE is one column or two, which the viewport width does not say: at a 768 viewport the
   workspace is two-up and the flow column is at its narrowest, and four tracks holding a label, a figure
   like "184,220" at --text-xl, and a sub-line did not fit. It read as clean only because .stack grew to
   hold them and pushed the whole flow column out instead. min(9rem, 100%) is the same idiom as
   .ob-ceremony's minmax(min(420px, 100%), 1fr): the floor applies while there is room for it and gets out
   of the way when there is not, so the row lays four-up on a wide flow column and two-up or one-up on a
   narrow one, with no width named anywhere that has to stay in step with the workspace's own breakpoints.
   Each tile's own single column is minmax(0, 1fr) for the reason .stack is. */
.restore-figs { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(9rem, 100%), 1fr)); gap: var(--space-3); margin: var(--space-4) 0; }
.restore-fig { background: var(--bg-subtle); border: 1px solid var(--border-subtle); border-radius: var(--radius); padding: var(--space-3) var(--space-4); display: grid; grid-template-columns: minmax(0, 1fr); gap: 2px; }
.restore-fig__label { font-size: var(--text-xs); color: var(--text-muted); font-weight: var(--weight-medium); }
.restore-fig__value { font-size: var(--text-xl); font-weight: var(--weight-semibold); font-variant-numeric: tabular-nums; }
/* R14: the value + optional status-dot row inside a figure tile (shared.ts figure()). */
.restore-fig__value-row { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.restore-fig__sub { font-size: var(--text-xs); color: var(--text-muted); }
@media (max-width: 640px) { .restore-figs { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
/* R14: scheduled-restore-test recency tiles (recency.ts), grouped by state. Each tile carries a
   tone-coloured left edge as a visual echo of the same tone its badge chip already states. */
.rt-tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: var(--space-3); }
.rt-tile { display: grid; gap: var(--space-2); min-width: 0; background: var(--surface); border: 1px solid var(--border-subtle); border-left: 3px solid var(--border-subtle); border-radius: var(--radius); padding: var(--space-3) var(--space-4); }
.rt-tile--danger { border-left-color: var(--danger); }
.rt-tile--warn { border-left-color: var(--warn); }
.rt-tile--neutral { border-left-color: var(--neutral); }
.rt-tile--ok { border-left-color: var(--ok); }
.rt-tile__name { font-weight: var(--weight-semibold); color: var(--text); overflow-wrap: anywhere; }
@media (max-width: 480px) { .rt-tiles { grid-template-columns: 1fr; } }

/* Resolved-destinations sample + the integrity line. */
.restore-sample { margin: var(--space-4) 0; }
.restore-sample__title { display: block; margin-bottom: var(--space-2); }
.restore-plan__integrity { display: flex; align-items: center; gap: var(--space-2); margin-top: var(--space-4); }
.restore-plan__integrity svg { flex: none; color: var(--trust); }
.restore-plan__loadnote { margin-bottom: var(--space-3); }

/* Confirm section: the plan-hash cue + the apply row. */
.restore-confirm__hash { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; margin-bottom: var(--space-3); }
/* E2: the plan-hash seal tick, a small --ok mark of confidence beside the (always readable) hash. */
.restore-confirm__seal { color: var(--ok-fg); flex: none; }
.restore-confirm__apply { margin-top: var(--space-3); }
.restore-request { margin-top: var(--space-4); }

/* Receipt: clean (ok, the trust/ok family) and honest-partial (warn). The actor
   line (the F2 fix) sits in the body; the apply actor satisfies the gate. */
.restore-receipt { margin-top: var(--space-4); border-radius: var(--radius-lg); overflow: hidden; }
.restore-receipt--clean { border-color: var(--ok-border); }
.restore-receipt--partial { border-color: var(--warn-border); }
.restore-receipt__head { display: flex; align-items: center; gap: var(--space-3); font-weight: var(--weight-semibold); margin: calc(var(--space-5) * -1) calc(var(--space-5) * -1) var(--space-4); padding: var(--space-4) var(--space-5); }
.restore-receipt--clean .restore-receipt__head { background: var(--ok-bg); color: var(--ok-fg); }
.restore-receipt--partial .restore-receipt__head { background: var(--warn-bg); color: var(--warn-fg); }
.restore-receipt__head svg { flex: none; }
/* R7: a typographic step-up from the plain card__title (text-md), so the receipt reads as the
   payoff moment rather than another mid-flow panel. */
.restore-receipt__title { font-size: var(--text-lg); margin: 0; }
.restore-receipt__line { color: var(--text); margin: 0 0 var(--space-3); }
/* R7: records + bytes as headline figures, the same tabular-numeral idiom the plan review's
   .restore-fig uses (design consistency between the promise and the payoff). */
.restore-receipt__headline { display: flex; gap: var(--space-6); flex-wrap: wrap; margin: 0 0 var(--space-4); }
.restore-receipt__figure { display: flex; flex-direction: column; gap: 2px; }
.restore-receipt__figure-value { font-size: var(--text-xl); font-weight: var(--weight-semibold); font-variant-numeric: tabular-nums; }
.restore-receipt__figure-label { font-size: var(--text-xs); color: var(--text-muted); }
/* R7: the maker/checker identity pair, its own block behind a hairline, in normal text weight
   (not one muted caption) so the dual-control attribution is prominent, not an afterthought. */
.restore-receipt__identity { border-top: 1px solid var(--border-subtle); margin-top: var(--space-3); padding-top: var(--space-3); display: grid; gap: var(--space-1); }
.restore-receipt__identity-row { display: flex; align-items: baseline; gap: var(--space-2); margin: 0; color: var(--text); font-weight: var(--weight-medium); }
.restore-receipt__identity-row svg { flex: none; color: var(--trust); align-self: center; }
.restore-receipt__identity-note { margin: var(--space-1) 0 0; }
/* R7: the closing actions (Copy summary / Retry / View run) sit behind their own hairline, reading
   as the deliberate final step rather than a trailing row. */
.restore-receipt__actions { display: flex; gap: var(--space-2); flex-wrap: wrap; margin-top: var(--space-4); padding-top: var(--space-4); border-top: 1px solid var(--border-subtle); }

/* R4 step 4 / R7: once every step above the receipt collapses on apply success (confirm.ts), the
   outer plan/confirm card's own chrome (border, padding, background from the base .card rule) is
   redundant with the receipt's OWN full card styling nested inside it; dropping it stops the
   receipt reading as "a card nested inside another card" and lets it stand as the sole payoff. The
   confirm section's own border-top + padding-top (.restore-confirm, above) stays: with everything
   else in the card hidden, that hairline now sits directly above the receipt, which is exactly the
   "hairline separation" R7 asks for. */
.restore-plan--collapsed { border: none; padding: 0; background: transparent; box-shadow: none; }

/* The two-distinct target-choice cards (the pivotal blast-radius control). The
   redirect option is visibly walled off (danger treatment) from the calm default.
   :has() drives the selected ring; a focus-within ring keeps keyboard parity. */
.target-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-3); }
@media (max-width: 640px) { .target-grid { grid-template-columns: 1fr; } }
.target-opt { display: grid; gap: var(--space-1); border: 1px solid var(--border-strong); border-radius: var(--radius-lg); padding: var(--space-4); cursor: pointer; background: var(--surface); transition: border-color var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out); }
.target-opt:hover { border-color: var(--accent); }
.target-opt input[type="radio"] { position: absolute; opacity: 0; pointer-events: none; }
.target-opt__top { display: flex; align-items: center; gap: var(--space-2); font-weight: var(--weight-semibold); }
.target-opt__top svg { flex: none; }
.target-opt__desc { font-size: var(--text-sm); color: var(--text-muted); }
.target-opt__binding { margin-top: var(--space-2); }
.target-opt:has(input:checked) { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-subtle-bg); background: var(--accent-subtle-bg); }
.target-opt:has(input:checked) .target-opt__desc { color: var(--text); }
.target-opt--danger .target-opt__top { color: var(--danger-fg); }
.target-opt--danger:has(input:checked) { border-color: var(--danger); box-shadow: var(--shadow-focus-danger); background: var(--danger-bg); }
.target-opt:focus-within { outline: 2px solid var(--ring); outline-offset: 2px; }
.target-opt--danger:focus-within { outline-color: var(--danger); }

/* Reflow: at 320px the figure grid and target cards drop to one column already
   above; the receipt head's negative-margin bleed stays inside the card padding. */
@media (max-width: 380px) {
  .restore-figs { grid-template-columns: 1fr; }
  .restore-steps { gap: var(--space-1); }
}

/* =============================================================================
   9.9 Onboarding wizard + key ceremony (screens/onboarding-ceremony.ts; IA
   screen 10). The full-bleed first-run chrome: a slim top bar (brand + the
   persistent no-custody line + the live theme control), a step rail, and the
   centred content column. Built only from the semantic tokens, so it themes for
   free (Obsidian default + light companion). Status is hue + SHAPE + label;
   focus-visible is the global ring; reflows to a single column on Compact and to
   320px. The wizard runs WITHOUT the app shell (the one shell exception), so this
   composition supplies its own page chrome.
   ========================================================================== */
.ob-page { min-height: 100vh; display: flex; flex-direction: column; }
.ob-skip { position: absolute; left: var(--space-2); top: -48px; z-index: var(--z-tooltip); background: var(--surface); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: var(--space-2) var(--space-3); box-shadow: var(--shadow); transition: top var(--dur) var(--ease-out); }
.ob-skip:focus-visible { top: var(--space-2); }

/* Top bar: brand on the left, the no-custody line pushed to the centre/right, the
   theme control on the far right. Sticky so it stays as the body scrolls. */
.ob-top { display: flex; align-items: center; gap: var(--space-4); padding: var(--space-3) var(--space-5); border-bottom: 1px solid var(--border-subtle); background: var(--bg); position: sticky; top: 0; z-index: var(--z-sticky); }
.ob-top__custody { display: flex; align-items: center; gap: var(--space-2); font-size: var(--text-sm); color: var(--text-muted); margin-left: auto; min-width: 0; }
.ob-top__custody svg { color: var(--trust); flex: none; }

/* Live theme control (System / Light / Dark), the segmented pattern. */
.ob-theme { display: inline-flex; border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; background: var(--surface); flex: none; }
.ob-theme__btn { height: var(--control-h-sm); padding: 0 var(--space-2); border: none; background: transparent; color: var(--text-muted); font-size: var(--text-xs); font-weight: var(--weight-medium); cursor: pointer; display: inline-flex; align-items: center; gap: 4px; border-right: 1px solid var(--border); }
.ob-theme__btn:last-child { border-right: none; }
.ob-theme__btn svg { width: 14px; height: 14px; }
.ob-theme__btn[aria-pressed="true"] { background: var(--accent-subtle-bg); color: var(--accent-subtle-fg); }
.ob-theme__btn:focus-visible { outline: 2px solid var(--ring); outline-offset: -2px; }

/* Body: the step rail + the content column, capped and centred. */
.ob-body { flex: 1 1 auto; display: grid; grid-template-columns: 260px minmax(0, 1fr); gap: var(--space-8); width: 100%; margin: 0 auto; padding: var(--space-8) clamp(var(--space-6), 4vw, var(--space-12)) var(--space-12); }

/* Step rail (ordered list; aria-current="step"; sticky on desktop). */
.ob-rail { align-self: start; position: sticky; top: calc(var(--header-h) + var(--space-5)); }
.ob-steps { list-style: none; display: flex; flex-direction: column; gap: 2px; padding: 0; }
.ob-steps__heading { font-size: var(--text-xs); font-weight: var(--weight-semibold); text-transform: uppercase; letter-spacing: var(--tracking-wide); color: var(--text-muted); margin-bottom: var(--space-3); }
/* The step is boxless: a thin connector runs marker-to-marker (a real stepper, not
   a stack of bordered rows), and the current step is distinguished by an accent ring
   + weight, not a heavy surface box. */
.ob-step { position: relative; display: flex; align-items: flex-start; gap: var(--space-3); padding: var(--space-2) var(--space-3); border-radius: var(--radius); color: var(--text-muted); border: 1px solid transparent; background: transparent; }
.ob-step:not(:last-child)::after { content: ""; position: absolute; left: 24px; top: 34px; height: calc(100% - 22px); width: 1.5px; background: var(--border-subtle); }
.ob-step--done:not(:last-child)::after { background: var(--ok); opacity: 0.45; }
.ob-step--current { background: transparent; border-color: transparent; color: var(--text); }
.ob-step--current .ob-step__label { font-weight: var(--weight-semibold); }
.ob-step--done { color: var(--text); }
.ob-step--upcoming { color: var(--text-muted); }
.ob-step__marker { position: relative; z-index: 1; width: 25px; height: 25px; flex: none; border-radius: var(--radius-full); display: inline-flex; align-items: center; justify-content: center; font-size: var(--text-xs); font-weight: var(--weight-semibold); border: 1.5px solid var(--border-strong); color: var(--text-muted); background: var(--surface); }
.ob-step--done .ob-step__marker { background: var(--ok); border-color: var(--ok); color: var(--ok-solid-fg); }
.ob-step--current .ob-step__marker { background: var(--accent); border-color: var(--accent); color: var(--accent-fg); box-shadow: 0 0 0 4px var(--accent-subtle-bg); }
.ob-step__body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.ob-step__label { font-size: var(--text-sm); font-weight: var(--weight-medium); }
.ob-step__meta { font-size: var(--text-xs); color: var(--text-muted); }
.ob-step__link { display: flex; align-items: flex-start; gap: var(--space-3); color: inherit; text-decoration: none; border-radius: var(--radius); }
.ob-step__link:hover { text-decoration: none; }
.ob-step__link:hover .ob-step__label { text-decoration: underline; }
.ob-step__link:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }

/* Content column. */
.ob-content { min-width: 0; }
/* Suppress the focus ring ONLY for pointer/programmatic focus (wizard.ts steps forward via
   body.focus()); keyboard focus is preserved by the :focus-visible companion, satisfying
   WCAG 2.4.11 (Focus Not Obscured) and 2.4.12. A11Y-01 fix. */
.ob-content:focus:not(:focus-visible) { outline: none; }
/* The eyebrow: a small, tracked, muted label with a short accent rule, the
   convention premium consoles use to anchor a step heading without shouting. */
.ob-kicker {
  display: inline-flex; align-items: center; gap: var(--space-2);
  font-size: var(--text-xs); font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wide); color: var(--text-muted);
  margin-bottom: var(--space-3);
}
.ob-kicker::before { content: ""; width: 16px; height: 2px; border-radius: 2px; background: var(--accent); }
/* The step title: a confident display size with display-grade negative tracking
   (system-ui is already optically sized, so a moderate -0.02em, not Geist's -0.06em). */
.ob-title { font-size: 2rem; line-height: 1.08; letter-spacing: -0.02em; font-weight: var(--weight-semibold); margin-bottom: var(--space-4); }
.ob-lede { color: var(--text-muted); font-size: var(--text-md); margin-bottom: var(--space-6); max-width: 60ch; }
.ob-actions { display: flex; gap: var(--space-3); align-items: center; margin-top: var(--space-5); flex-wrap: wrap; }
.ob-actions__spacer { flex: 1 1 auto; }
.ob-verdict { margin-top: var(--space-4); display: flex; flex-direction: column; gap: var(--space-3); }
.ob-poll__line { color: var(--text-muted); }

/* The key-ceremony statement of intent. It is a type-led SECTION on the canvas,
   not a bordered card (§7a: a section is a heading + whitespace, not a box). The
   lede carries the weight through size and a generous measure; gravitas comes from
   restraint, never a slab. The .ob-gravitas element no longer carries the .card
   class (set in onboarding-ceremony.ts), so there is no border/fill/padding to
   undo here. */
.ob-gravitas { max-width: 62ch; margin-bottom: var(--space-6); }
.ob-gravitas__lede { font-size: var(--text-lg); line-height: var(--leading-base); color: var(--text); font-weight: var(--weight-regular); }
.ob-gravitas .field__hint { margin-top: var(--space-3); }

/* The "Nothing" cell in the vendor lane: the quiet hero of the page. Rendered as a
   confident, bright statement (the research brief's "absence as a confident
   punchline", never a red cross), at the display weight the eye lands on. */
.ob-nothing { font-size: var(--text-2xl); line-height: 1; letter-spacing: -0.01em; font-weight: var(--weight-semibold); color: var(--text); margin: 0; }

/* Post-ceremony success heading + the files-downloaded manifest. The check sits in
   a subtle ok-tinted chip (matching the entry's icon-chip language), and the file
   list reads as a tidy ledger: hairline-divided rows, the filename column fixed so
   the destination badges align, one badge per row. */
.ob-success-head { display: flex; align-items: center; gap: var(--space-3); margin-bottom: var(--space-1); }
.ob-success-head__icon {
  color: var(--ok); flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; border-radius: var(--radius-full);
  background: var(--ok-bg); border: 1px solid var(--ok-border);
}
.ob-filelist { list-style: none; padding: 0; margin: var(--space-4) 0 0; display: flex; flex-direction: column; }
.ob-filelist__row { display: flex; align-items: center; gap: var(--space-3); min-height: 44px; padding: var(--space-2) 0; font-size: var(--text-sm); border-top: 1px solid var(--border-subtle); }
.ob-filelist__row:first-child { border-top: none; }
.ob-filelist__name { font-size: var(--text-sm); font-weight: var(--weight-medium); flex: 0 0 auto; min-width: 9.5rem; }
.ob-filelist__spacer { flex: 1 1 auto; }
@media (max-width: 520px) {
  .ob-filelist__row { flex-wrap: wrap; min-height: 0; }
  .ob-filelist__name { min-width: 0; }
}

/* The source -> engine -> destination teaching diagram (true-empty first-run state). */
.ob-flow { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; padding: var(--space-4); background: var(--bg-subtle); border-radius: var(--radius); margin: var(--space-4) 0; }
.ob-flow__node { display: flex; flex-direction: column; align-items: center; gap: var(--space-1); text-align: center; flex: 1 1 0; min-width: 80px; }
.ob-flow__node svg { color: var(--accent); }
.ob-flow__label { font-size: var(--text-xs); color: var(--text-muted); }
.ob-flow__arrow { color: var(--text-muted); flex: none; }

/* The create-first-downpipe form grid (two columns, collapsing on Compact). */
.ob-form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-3); margin-top: var(--space-3); }

/* A standalone spinner for the in-flow "generating / running" status lines (the
   in-button busy state uses .btn__spinner). Reduced-motion freezes it below. */
.ob-spinner { display: inline-block; width: 14px; height: 14px; flex: none; border: 2px solid currentColor; border-right-color: transparent; border-radius: var(--radius-full); animation: dp-spin 0.7s linear infinite; }

@media (prefers-reduced-motion: reduce) {
  .ob-spinner { animation: none; opacity: 0.6; }
}

/* Reflow: collapse the wizard to a single column on Compact; the step rail goes
   horizontal and scrollable, showing only the current step's detail to save space. */
@media (max-width: 860px) {
  .ob-body { grid-template-columns: 1fr; gap: var(--space-5); padding: var(--space-5) var(--space-4) var(--space-10); }
  .ob-rail { position: static; }
  .ob-steps { flex-direction: row; overflow-x: auto; gap: var(--space-2); padding-bottom: var(--space-2); }
  .ob-steps__heading { display: none; }
  .ob-step { flex: none; }
  .ob-step::after { display: none; } /* the marker-to-marker connector is vertical-only */
  .ob-step__body { display: none; }
  .ob-step--current .ob-step__body { display: flex; }
  .ob-form-grid { grid-template-columns: 1fr; }
}
@media (max-width: 520px) {
  .ob-top__custody-text { display: none; }
  .ob-theme__label { display: none; }
}

/* =============================================================================
   10. Print: the recovery sheet and audit/drill exports print clean. Force a
   light, ink-frugal palette regardless of theme. Self-contained so the sheet
   prints without the app stylesheet. The shell chrome is not printed.
   ========================================================================== */
@media print {
  :root, :root[data-theme="dark"] {
    color-scheme: light;
    --bg: #ffffff; --surface: #ffffff; --bg-subtle: #ffffff; --surface-inset: #ffffff;
    --surface-raised: #ffffff; --surface-hover: #ffffff; --surface-active: #ffffff;
    --border: #999999; --border-subtle: #bbbbbb; --border-strong: #666666; --control-border: #666666;
    --text-link: #000000; --accent: #333333; --accent-fg: #ffffff;
    --accent-subtle-bg: #ffffff; --accent-subtle-fg: #000000; --ring: #000000;
    /* The status families remap to ink-safe values (UIA-12): printing from the
       dark default otherwise kept dark tints (near-black chips) or, with
       background stripping, pale status text on white at ~1.5-2:1. Hue is kept
       (colour printers) but every fg clears AA on white and on its tint. */
    --ok: #14683c; --ok-bg: #ffffff; --ok-fg: #14683c; --ok-border: #14683c; --ok-solid-fg: #ffffff;
    --warn: #7a5205; --warn-bg: #ffffff; --warn-fg: #7a5205; --warn-border: #7a5205;
    --danger: #a82424; --danger-bg: #ffffff; --danger-fg: #a82424; --danger-border: #a82424; --danger-solid-fg: #ffffff;
    --info: #1250a0; --info-bg: #ffffff; --info-fg: #1250a0; --info-border: #1250a0;
    --trust: #0a5750; --trust-bg: #ffffff; --trust-fg: #0a5750; --trust-border: #0a5750;
    --neutral: #555555; --neutral-bg: #ffffff; --neutral-fg: #333333;
    --shadow-sm: none; --shadow: none; --shadow-lg: none;
  }
  body { background: #ffffff; color: #000000; }
  /* Fixed/overlay surfaces repeat on every printed page: never print them. */
  .no-print, .rail, .context-bar, .toast-region, .overlay, .rail-scrim { display: none !important; }
  .shell { display: block; }
  .main { padding: 0; }
  .card { border: 1px solid #999999; box-shadow: none; }
  /* Keep logical units intact across page breaks. */
  .card, .verdict, .stat, .dp-table tr, .kv-row, .posture-row { break-inside: avoid; }
  a { color: #000000; text-decoration: underline; }
  /* Print the destination of real hyperlinks in prose (not in-app buttons). */
  a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 0.85em; color: #333333; }
}

/* =============================================================================
   11. Missing structural classes (adversarial-review finding 2026-06-07).
   The three confirmed-missing groups (switch, stack/stack-sm, async-region)
   plus one further gap (confirm-body) found by the completeness sweep.
   All built only from the semantic token layer so both themes apply for free.
   ========================================================================== */

/* ---- Enable / disable switch (A) ------------------------------------------
   Markup: <span class="switch">
             <input type="checkbox">
             <span class="track"><span class="thumb"></span></span>
           </span>
   The input is visually hidden so the track/thumb provide the full visual
   control. The thumb sits inside .track, so the :checked sibling combinator
   targets .track and the descendant combinator targets .thumb inside it.
   Contrast gates (WCAG 1.4.11, 3:1 for UI component boundaries):
     Off-state border: --control-border.
       Light: --n-450 (#828ea0) on --n-0 canvas, 3.29:1. PASS.
       Dark:  --n-500 (#646f7f) on --n-850 (#1a1e25), 4.10:1. PASS.
     On-state fill: --accent (the indigo action ramp).
       Light: --a-500 (#3b66f0) on --n-0 canvas, 4.70:1. PASS.
       Dark:  --a-400d (#84a4ff) on --n-850 (#1a1e25), 5.55:1. PASS.
     Thumb fill: --surface (white / near-black), visually distinct shape.
   Shape redundancy: the thumb position (left vs right) distinguishes the
   checked state independent of colour, satisfying WCAG 1.4.1 and 1.4.11.
   -------------------------------------------------------------------------- */
.switch {
  position: relative;
  display: inline-flex;
  width: 36px;
  height: 20px;
  flex: none;
}
.switch input {
  position: absolute;
  inset: 0;
  opacity: 0;
  margin: 0;
  cursor: pointer;
  width: 100%;
  height: 100%;
  z-index: 1;
}
.switch .track {
  position: relative;
  width: 36px;
  height: 20px;
  border-radius: var(--radius-full);
  background: var(--neutral-bg);
  border: 1px solid var(--control-border);
  transition: background var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out);
}
.switch .thumb {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 14px;
  height: 14px;
  border-radius: var(--radius-full);
  background: var(--surface);
  box-shadow: var(--shadow-sm);
  transition: transform var(--dur-fast) var(--ease-out);
  pointer-events: none;
}
.switch input:checked ~ .track {
  background: var(--accent);
  border-color: var(--accent);
}
.switch input:checked ~ .track .thumb {
  transform: translateX(16px);
}
.switch input:focus-visible ~ .track {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
}

/* ---- Vertical-rhythm utilities (B) ----------------------------------------
   Used as the sole layout class on the primary step regions of the restore
   hero flow. .stack-sm is the tighter sibling. No specificity fights: these
   are additive gap utilities only.
   -------------------------------------------------------------------------- */
/* minmax(0, 1fr), never the implicit `auto` column these used to get. A single-column grid with no
   template declares one IMPLICIT track sized `auto`, and an auto track's floor is the widest min-content
   among its items. A stack holding a <pre> (white-space:pre, so its min-content is the longest line), a
   data table, or any un-shrinkable control therefore grows PAST the stack's own box, every sibling stretches
   with it because they share the track, and an inner overflow-x:auto scroller never engages because the
   scroller is already as wide as its content. min-width:0 further down (section 20) cannot reach this: it
   lowers the ITEM's floor, not the TRACK's.
   Measured on console d9bc3c5e over the console's full declared route set at 17 widths derived from this
   stylesheet: the wrangler stanza in the "Prefer no token" disclosure ran 845.1px past its div.stack-sm on
   /sources/advanced and /sources/add at 375 and was still 155.1px past at 1600, and gave the document a
   816px horizontal scrollbar at 375; and the restore flow column's three children ran 145.9px past their
   div.stack at 390, with the document scrolling sideways at five of the seventeen widths.
   Zeroing the track floor is the idiom this repository already uses at .shell, .restore-workspace, the cost
   grids, .idp-grid and the Access setup wizard's step body. Visually identical where nothing overflows: an
   auto track in a definite-width container already stretches to fill it. */
.stack    { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-5); }
.stack-sm { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-3); }

/* ---- Async-region refetch indicator (C) -----------------------------------
   Any host that mounts .is-refetching during a background poll gets a slim
   indeterminate strip so polling is always visible without blanking content.
   Redesigned 2026-06-12 (walkthrough: the old hard-looping 40% gradient read
   as a flashing glitch, not as loading): a faint rounded TRACK plus a COMET -
   a rounded indigo-to-teal segment that glides end to end on an eased
   ping-pong, so there is no restart pop and the motion reads as deliberate.
   Reduced-motion users get a static 2px accent strip (no animation).
   -------------------------------------------------------------------------- */
.async-region { position: relative; }

.async-region.is-refetching::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 2px;
  border-radius: var(--radius-full);
  background: color-mix(in srgb, var(--accent) 16%, transparent);
}

.async-region.is-refetching::after {
  content: "";
  position: absolute;
  top: 0; left: 0;
  height: 2px;
  width: 18%;
  border-radius: var(--radius-full);
  background: linear-gradient(90deg, var(--accent), var(--trust));
  box-shadow: 0 0 8px color-mix(in srgb, var(--accent) 55%, transparent);
  animation: dp-refetch-sweep 1.5s cubic-bezier(0.65, 0, 0.35, 1) infinite alternate;
}

/* The comet travels its track minus its own width; translateX percentages are
   relative to the comet itself (18% of the track), so the far stop is
   (100 - 18) / 18 of its own width. */
@keyframes dp-refetch-sweep {
  from { transform: translateX(0); }
  to { transform: translateX(455.5%); }
}

@media (prefers-reduced-motion: reduce) {
  .async-region.is-refetching::before {
    background: color-mix(in srgb, var(--accent) 70%, transparent);
  }
  .async-region.is-refetching::after { display: none; }
}

/* ---- Confirm-body (completeness-sweep addition) ---------------------------
   The wrapper div inside a confirm dialog that holds the impact sentence, any
   extra nodes, and the typed-confirmation field. Needs a vertical stack layout
   so each child flows top-to-bottom with consistent gap; no inline style was
   present on this element.
   -------------------------------------------------------------------------- */
.confirm-body { display: flex; flex-direction: column; gap: var(--space-3); }

/* =============================================================================
   12. Topology map + cost calculator (the /map and /costs centrepieces).
   Built only from the semantic + trust token layer above, so both Obsidian
   (dark default) and the light companion apply with no per-theme overrides.
   Class hooks are taken verbatim from components/topology.ts and
   screens/{map,costs}.ts. House rules: status reads by HUE + GLYPH + LABEL
   together, never colour alone; "unknown"/"disabled" are first-class neutral
   states (never a stale green); the in-flight flowing dash is gated under the
   prefers-reduced-motion block (static under reduced motion).

   Status -> token mapping (the single mapping the whole map obeys, matching
   presentStatus() in topology.ts):
     healthy  -> trust  (the teal verified family: --trust / --trust-bg / etc.)
     stale    -> warn   (amber: --warn / --warn-bg / --warn-border)
     failed   -> danger (--danger / --danger-bg / --danger-border)
     disabled -> neutral (--neutral / --neutral-bg / --neutral-fg)
     unknown  -> neutral (--neutral ...; shape, not hue, separates it from disabled)
   ========================================================================== */

/* ---- Topology: the component root + the screen chrome around it ------------ */
.topo { display: block; }
.topo-screen { display: block; }
.topo__body { display: block; }

/* The figure holds the inline SVG enhancement. It is a recessed instrument
   panel: a subtle inset surface, a hairline frame and generous padding so the
   bipartite flow reads as a single calm diagram rather than loose shapes. The
   SVG scales to the figure width (preserveAspectRatio keeps the aspect).

   A faint vertical channel backdrop ties the two columns into one deliberate
   bipartite diagram: a single soft trust-tinted band runs down the middle (where
   the pipes converge) over the inset surface, so the source/destination banks
   read as two sides of one channel rather than two unrelated stacks. It is
   restrained (very low-opacity tints of existing tokens, fading to transparent at
   the edges) and purely decorative, so it stays a calm instrument backdrop and
   never competes with the pipes or the node text in either theme. */
.topo__figure {
  margin: 0;
  padding: var(--space-5);
  background:
    radial-gradient(120% 78% at 50% 50%,
      color-mix(in srgb, var(--trust) 7%, transparent) 0%,
      transparent 58%),
    linear-gradient(90deg,
      transparent 0%,
      color-mix(in srgb, var(--surface) 60%, transparent) 38%,
      color-mix(in srgb, var(--surface) 60%, transparent) 62%,
      transparent 100%),
    var(--surface-inset);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  overflow-x: auto;          /* never clip a wide map; scroll inside the frame */
}
.topo-svg {
  display: block;
  width: 100%;
  height: auto;
  max-width: 100%;
  /* A sensible floor so a small map is still legible at a glance, not shrunk to
     a thumbnail; the channel + node boxes keep their proportions. */
  min-width: 520px;
}

/* The cap notice + the table heading sit beneath the figure with breathing room. */
.topo__cap-notice { margin-top: var(--space-3); }
.topo__table { display: block; margin-top: var(--space-6); }
.topo__table-title {
  font-size: var(--text-md); font-weight: var(--weight-semibold);
  margin-bottom: var(--space-3);
}

/* The empty state (the component's own calm explanation, never a blank canvas). */
.topo__empty { display: grid; gap: var(--space-2); text-align: center; padding: var(--space-7) var(--space-5); }
.topo__empty-title { font-size: var(--text-md); font-weight: var(--weight-semibold); }

/* ---- Topology SVG: edges (curved connectors) ------------------------------
   The connector reads as a smooth pipe behind the nodes. Three bounded
   thickness steps (--w1/--w2/--w3) reinforce throughput; the NUMBER in the
   label and table is the source of truth, so the steps stay restrained. The
   dash style (solid/dashed/dotted) is shape redundancy alongside the hue, set
   inline by the component as a fallback and refined here. */
.topo-svg__edges { color: var(--border-strong); }
.topo-edge__line {
  stroke: currentColor;
  stroke-width: 1.5px;
  stroke-linecap: round;
  fill: none;
  transition: stroke var(--dur-fast) var(--ease-out),
              stroke-width var(--dur-fast) var(--ease-out);
}

/* Three thickness steps (bounded; spec section 3). */
.topo-edge--w1 .topo-edge__line { stroke-width: 1.5px; }
.topo-edge--w2 .topo-edge__line { stroke-width: 2.5px; }
.topo-edge--w3 .topo-edge__line { stroke-width: 4px; }

/* Dash style (shape redundancy; the inline stroke-dasharray fallback holds even
   before this lands, these keep it consistent with the rest of the system). */
.topo-edge--solid  .topo-edge__line { stroke-dasharray: none; }
.topo-edge--dashed .topo-edge__line { stroke-dasharray: 7 5; }
.topo-edge--dotted .topo-edge__line { stroke-dasharray: 1.5 5; }

/* Status hue per edge (the join glyph, the label and the line all inherit from
   the edge group via currentColor, so one colour decl tones the whole edge). */
.topo-edge--trust   { color: var(--trust); }
.topo-edge--warn    { color: var(--warn); }
.topo-edge--danger  { color: var(--danger); }
.topo-edge--neutral { color: var(--neutral); }

/* The healthy (trust) edges are the load-bearing read, so give them a touch
   more presence than the neutral idle ones without shouting. Beyond hue +
   thickness they get subtle DEPTH: a soft glow + a faint drop-shadow lifts the
   live pipe off the recessed panel so the healthy flow reads as the figure, not
   a flat line. This uses the CSS drop-shadow() filter FUNCTION (not an SVG
   filter referenced by id), so it needs no <defs> and no component wiring, and
   it degrades cleanly: a browser without filter support simply shows the plain
   coloured stroke. The glow is a tinted halo (the trust hue, low alpha) plus a
   tiny dark offset shadow for separation; restrained so it stays a security-
   grade instrument, not gaudy, and the stroke itself still carries the colour so
   contrast does not depend on the glow. */
.topo-edge--trust .topo-edge__line {
  stroke: var(--trust);
  filter:
    drop-shadow(0 0 3px color-mix(in srgb, var(--trust) 55%, transparent))
    drop-shadow(0 1px 1px rgba(0, 0, 0, 0.22));
}
.topo-edge--neutral .topo-edge__line { opacity: 0.85; }

/* The join glyph + label group at the curve midpoint (decorative; aria-hidden).
   The glyph inherits the edge hue; the label is muted so the diagram is not a
   wall of coloured text. The label sits on a faint plate via paint-order so it
   stays legible where it crosses a connector. */
.topo-edge__marker { color: inherit; }
.topo-edge__marker svg { color: inherit; }
.topo-edge__label {
  fill: var(--text-muted);
  font-family: var(--font-sans);
  font-size: 0.6875rem;
  font-weight: var(--weight-medium);
  text-anchor: middle;
  paint-order: stroke;
  stroke: var(--surface-inset);
  stroke-width: 3px;
  stroke-linejoin: round;
}

/* The in-flight indication. The flowing dash overlay animates ONLY when motion
   is allowed (gated in the reduced-motion block below); the static branch draws
   a dot + a "running" TEXT label so the state always reads by glyph + label +
   hue. The running marks take the trust hue (an in-flight healthy/stale/unknown
   run is progress, not an alarm). */
.topo-edge__running { color: var(--trust); }
.topo-edge__running-dot { fill: var(--trust); }
.topo-edge__running-label {
  fill: var(--trust-fg);
  font-family: var(--font-sans);
  font-size: 0.625rem;
  font-weight: var(--weight-semibold);
  text-anchor: middle;
}
.topo-edge__flow {
  stroke: var(--trust);
  stroke-width: 2.5px;
  stroke-linecap: round;
  fill: none;
  opacity: 0.9;
  /* The flowing dash: a long dash marching along the pipe. Gated below. */
  stroke-dasharray: 6 10;
  animation: dp-topo-flow 0.9s linear infinite;
}

/* The IDLE ambient drift (the SVG twin of the canvas sheen): a slow, dim
   long-dash overlay in the edge's own hue (currentColor: the edge group's tone
   class sets it) drifting toward the destination, so a watched idle route reads
   alive between runs - never the trust hue, never fast, never bright; the
   in-flight dash above stays the exclusive "running now" signal. Gated with
   the same media/data-motion rules as every other animation. */
.topo-edge__ambient {
  stroke: currentColor;
  stroke-width: 2px;
  stroke-linecap: round;
  fill: none;
  opacity: 0.32;
  stroke-dasharray: 16 30;
  animation: dp-topo-ambient 6.5s linear infinite;
}

/* The transparent hit path (sighted pointer/keyboard panning target; not the
   AT-operable surface). No paint; just a focus ring when it holds roving focus.
   The ring clears 3:1 against the inset panel in BOTH themes (it is --ring, the
   indigo action accent, the same indicator the rest of the system uses). */
.topo-edge__hit { cursor: pointer; outline: none; }
.topo-edge__hit:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 1px;
  border-radius: var(--radius-sm);
}
/* Lift the whole edge on hover/focus of its hit target so the active pipe reads. */
.topo-edge:hover .topo-edge__line,
.topo-edge:focus-within .topo-edge__line { stroke-width: 4px; }

/* ---- Topology SVG: structural layers (pure <g> containers) ----------------
   The node layer + each roving node group are structural wrappers; their
   children (nodes, glyphs, text) carry the paint. They inherit the colour
   cascade so a future hue change flows down without per-layer overrides. */
.topo-svg__nodes,
.topo-svg__nodegroup { color: inherit; }

/* ---- Topology SVG: group (kind cluster) labels ---------------------------- */
.topo-svg__groups { color: var(--text-muted); }
/* Each group wrapper just positions its label child (set via the SVG x/y); it
   inherits the muted group colour so the label reads as a quiet section header. */
.topo-svg__group { color: inherit; }
.topo-svg__group-label {
  fill: var(--text-muted);
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  /* Centre the caption over its column (buildGroupLabel sets x to the column centre) so the source and
     destination labels are placed consistently above their nodes. */
  text-anchor: middle;
}

/* ---- Topology SVG: nodes + kind glyphs ------------------------------------ */
.topo-node { cursor: default; }
.topo-node[tabindex] { cursor: pointer; }
/* The node boxes are raised surfaces, not flat rects on the panel. They fill
   with the RAISED surface token (a step up in lightness in dark, so the box
   reads as lifted off the inset panel) and gain a soft drop-shadow for
   elevation. SVG rects take no box-shadow, so the lift uses the CSS
   drop-shadow() filter FUNCTION (no <defs>, no component wiring, degrades to the
   plain bordered box where unsupported). The hairline --border-strong stroke
   stays as the >=3:1 boundary so the box edge never depends on the shadow for
   contrast. */
.topo-node__box {
  fill: var(--surface-raised);
  stroke: var(--border-strong);
  stroke-width: 1px;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.20));
  transition: fill var(--dur-fast) var(--ease-out),
              stroke var(--dur-fast) var(--ease-out);
}
/* The kind glyph + the two text lines. The glyph and the secondary line are
   muted; the name is the primary text, so each node has a clear two-line
   hierarchy (name, then "KIND - secondary"). */
.topo-node__glyph { color: var(--text-muted); }
.topo-node__glyph svg { color: var(--text-muted); }
.topo-node__name {
  fill: var(--text);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
}
.topo-node__secondary {
  fill: var(--text-muted);
  font-family: var(--font-sans);
  font-size: var(--text-xs);
}
/* Hover/focus a node: raise the box and accent its frame (it is a filter target).
   The resting fill is now --surface-raised, so hover steps to --surface-active
   (which differs from --surface-raised in BOTH themes) to keep a visible fill
   shift on hover rather than relying on the accent stroke alone. */
.topo-node[tabindex]:hover .topo-node__box { fill: var(--surface-active); stroke: var(--accent); }
.topo-node[tabindex]:focus-visible { outline: none; }
.topo-node[tabindex]:focus-visible .topo-node__box {
  stroke: var(--ring);
  stroke-width: 2px;
}
/* A DESTINATION the engine currently cannot reach (its last replicate attempt failed): a danger-toned
   frame PLUS a "down" badge line in place of the muted kind line. Never colour alone — the word "down"
   carries it for a monochrome reader, and the node names itself "currently unreachable" in its title. */
.topo-node--down .topo-node__box { stroke: var(--danger); stroke-width: 1.5px; }
.topo-node__down {
  fill: var(--danger);
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
}
/* Mirror the destination column's content to its OUTER (right) edge so the two columns read as a true
   left<->right pair: source text keeps the default text-anchor:start (anchored left, flows toward the
   centre); destination text anchors at the right and flows toward the centre. buildNode sets the per-side
   glyph + text x-coordinates; this flips only the text-anchor for the destination side. */
.topo-node--destination .topo-node__name,
.topo-node--destination .topo-node__secondary,
.topo-node--destination .topo-node__down { text-anchor: end; }

/* ---- Topology: the status cell in the accessible table -------------------- */
/* hue (the glyph tone) + glyph (shape) + label (text), never colour alone. */
/* max-width and a shrinkable badge. This sits in a table cell, so the CELL decides the width while the
   inline-flex sizes to its content: the badge could not give way and painted past its own row, measured by
   spec/visual/route-set-containment.spec.ts at 10px to 14px past the right on /map at 1062 and 1280. The
   glyph keeps flex:none because it is a fixed mark; the badge is the part that yields. */
.topo-status { display: inline-flex; align-items: center; gap: var(--space-2); max-width: 100%; min-width: 0; }
.topo-status > .badge { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.topo-status__glyph { display: inline-flex; flex: none; }
.topo-status--trust   .topo-status__glyph { color: var(--trust); }
.topo-status--warn    .topo-status__glyph { color: var(--warn); }
.topo-status--danger  .topo-status__glyph { color: var(--danger); }
.topo-status--neutral .topo-status__glyph { color: var(--neutral-fg); }

/* The endpoint cell in the table: a name plus a muted kind suffix. */
.topo-cell-node { display: inline-flex; align-items: baseline; gap: var(--space-1); min-width: 0; }
.topo-cell-node__name { font-weight: var(--weight-medium); overflow: hidden; text-overflow: ellipsis; }
.topo-cell-node__kind { white-space: nowrap; }

/* The per-row open button (icon-only ghost; sits flush in the trailing column). */
.topo-open { padding: 0 var(--space-2); }

/* ---- Topology: the screen chrome (filter toolbar, drawer runs list) -------- */
.topo-filters { /* layout is inline on the element; nothing extra needed here */ }
/* The active facet button gets a clear pressed treatment beyond aria-pressed. */
.topo-filters .btn.is-active {
  background: var(--accent-subtle-bg);
  color: var(--accent-subtle-fg);
  border-color: var(--accent-subtle-fg);
}
.topo-runs { /* the recent-runs list; layout is inline on the element */ }
.topo--loading { display: block; }

/* ---- Topology: reflow to the stacked/list form at the narrow breakpoint ----
   On Compact the SVG instrument is the wrong tool (it would force horizontal
   scroll), so it is hidden and the always-present accessible table carries the
   full map (WCAG 1.4.10 reflow; the table is the parity-guaranteed equivalent).
   The figure frame collapses so the table stands alone. */
@media (max-width: 640px) {
  .topo__figure { display: none; }
  .topo__table { margin-top: 0; }
}

/* The flowing-dash keyframe (one place; the animation is gated below). The dash
   marches from the source toward the destination, reading as data in flight. */
@keyframes dp-topo-flow { to { stroke-dashoffset: -32; } }

/* Reduced motion: the flowing dash is non-essential motion, so freeze it into a
   static dashed overlay (the component ALSO renders a static dot + "running"
   label in its reduced-motion branch, so the running state never relies on this
   moving). The global hard gate (section 9) already neutralises the animation;
   this is the explicit, belt-and-braces static fallback, matching the
   per-section reduced-motion pattern used elsewhere in this file. */
@media (prefers-reduced-motion: reduce) {
  .topo-edge__flow { animation: none; }
  /* The ambient drift is PURELY decorative: a frozen long-dash overlay would
     just double the edge's own dash pattern, so it hides entirely. */
  .topo-edge__ambient { display: none; }
}
:root[data-motion="reduced"] .topo-edge__flow { animation: none; }
:root[data-motion="reduced"] .topo-edge__ambient { display: none; }
/* The explicit "Full" motion choice animates the console even when the OS
   prefers reduced motion (the operator's persisted opt-in; the same override
   the canvas and the refetch comet honour). */
:root[data-motion="full"] .topo-edge__flow { animation: dp-topo-flow 0.9s linear infinite; }
:root[data-motion="full"] .topo-edge__ambient { display: inline; animation: dp-topo-ambient 6.5s linear infinite; }

@keyframes dp-topo-ambient { to { stroke-dashoffset: -46; } }

/* =============================================================================
   Live-flow (/map, WebGL view): the GPU-accelerated, opt-in spectacle over the
   same data as the SVG topology (ENTERPRISE-UX-BLUEPRINT 8.2). The canvas is a
   pure visual enhancement: it sits in the same recessed instrument panel as the
   SVG figure (the faint trust wash radiates from the engine hub at the centre
   of the tripartite layout), and the canonical accessible table always follows
   it. The canvas paints with alpha, so this panel IS the visualisation's
   backdrop in both themes. The element needs explicit CSS sizing (a canvas has
   no responsive intrinsic box; unstyled it renders at the 300x150 default,
   which is exactly what the view shipped as before this layer existed), and
   the GL controller multiplies the CSS box by devicePixelRatio for a crisp
   raster.
   ========================================================================== */
.live-flow { display: block; }
.live-flow__figure {
  margin: 0;
  padding: var(--space-5);
  background:
    radial-gradient(120% 78% at 50% 50%,
      color-mix(in srgb, var(--trust) 7%, transparent) 0%,
      transparent 58%),
    linear-gradient(90deg,
      transparent 0%,
      color-mix(in srgb, var(--surface) 60%, transparent) 38%,
      color-mix(in srgb, var(--surface) 60%, transparent) 62%,
      transparent 100%),
    var(--surface-inset);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  /* overflow-x AUTO, not hidden. At a 390px viewport this figure clipped its own source labels with no way
     to reach them: measured 384px of content inside a 356px box, rendering "R_DB" for LEDGER_DB, "NTS_KV"
     for PAYMENTS_KV, "NTS_R2" for STATEMENTS_R2 and "ECRETS" for SECRETS, with the destination reading
     "Primary (ap-southeast-2" and no closing bracket. overflow:hidden made that unreachable rather than
     merely tight. Vertical stays hidden because nothing overflows it and the gradient masks depend on it. */
  overflow-x: auto;
  overflow-y: hidden;
}
.live-flow__canvas {
  display: block;
  width: 100%;
  /* The hero band: tall enough to read as the centrepiece, bounded so the
     canonical table stays within reach below it on a laptop display. */
  height: clamp(300px, 46vh, 540px);
}

/* The DOM node layer over the canvas: labelled anchors for every endpoint,
   positioned by the model's normalised coordinates (left/top percentages, so
   they track the canvas box through any resize). The layer itself swallows no
   pointer events; only the operable node buttons do. */
.live-flow__stage { position: relative; }
.live-flow__nodes { position: absolute; inset: 0; pointer-events: none; }
.live-flow__node {
  position: absolute;
  width: 0; height: 0; /* a zero-size anchor; the dot and label hang off it */
  background: none; border: 0; padding: 0; margin: 0;
  font: inherit; color: var(--text);
  pointer-events: auto;
}
.live-flow__node:is(button) { cursor: pointer; }
/* Hover or keyboard-focus a node to trace its paths: the layer gets --focused and dims every node, then
   the lit ones (the hovered node, the engine hub, and the opposite-column nodes its paths reach) come
   back to full. The canvas dims the pipes/particles to match (live-flow.ts setFocus). Motion-gated. */
.live-flow__node { transition: opacity var(--dur-fast) var(--ease-out); }
.live-flow__nodes--focused .live-flow__node { opacity: 0.26; }
.live-flow__nodes--focused .live-flow__node--lit { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .live-flow__node { transition: none; } }
.live-flow__node-dot {
  position: absolute; left: -5px; top: -5px;
  width: 10px; height: 10px; border-radius: var(--radius-full);
  background: color-mix(in srgb, var(--neutral) 85%, var(--surface));
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--neutral) 20%, transparent);
}
.live-flow__node-label {
  position: absolute; top: 0; transform: translateY(-50%);
  display: flex; align-items: baseline; gap: var(--space-1);
  white-space: nowrap;
  font-size: var(--text-sm); line-height: 1.3;
}
/* Source labels hang LEFT of their dot (the canvas's left margin is theirs);
   destination labels sit BELOW their dot, centred, because the right margin is
   too narrow for a destination name and the egress pipes arrive horizontally,
   leaving the space beneath the dot clear. */
.live-flow__node--source .live-flow__node-label { right: 12px; justify-content: flex-end; }
.live-flow__node--destination .live-flow__node-label {
  top: 16px; left: 0; transform: translateX(-50%);
  justify-content: center;
}
.live-flow__node-name {
  overflow: hidden; text-overflow: ellipsis; max-width: 17ch;
  font-weight: var(--weight-medium);
}
.live-flow__node--destination .live-flow__node-name { max-width: 24ch; }
.live-flow__node-kind { font-size: var(--text-xs); color: var(--text-muted); }
/* The hub caption sits under the GL ring; the ring is the mark, this names it. */
.live-flow__node--engine .live-flow__node-label {
  top: 26px; left: 0; right: auto; transform: translateX(-50%);
  font-size: var(--text-xs); color: var(--text-muted);
  text-transform: uppercase; letter-spacing: 0.08em;
}
/* A dense column (a fleet-scale bank) drops per-node labels for one calm count
   chip at the column head; the table below carries every name. */
.live-flow__node-summary {
  position: absolute; transform: translate(-50%, -50%);
  white-space: nowrap;
  font-size: var(--text-xs); color: var(--text-muted);
  background: color-mix(in srgb, var(--surface) 78%, transparent);
  border: 1px solid var(--border);
  border-radius: var(--radius-full);
  padding: 1px var(--space-2);
}

/* The empty state mirrors the topology's calm explanation (never a blank
   canvas; the component hides the figure entirely while there are no flows). */
.live-flow__empty { display: grid; gap: var(--space-2); text-align: center; padding: var(--space-7) var(--space-5); }
.live-flow__empty-title { font-size: var(--text-md); font-weight: var(--weight-semibold); }

/* The canonical table beneath the figure, with the topology table's rhythm. */
.live-flow__table { display: block; margin-top: var(--space-6); }
.live-flow__table-title {
  font-size: var(--text-md); font-weight: var(--weight-semibold);
  margin-bottom: var(--space-3);
}

/* The compact embed variant (the overview hero card, the onboarding first-run
   moment): a shorter canvas band; everything else (encoding, table contract,
   reduced motion) is identical. Node labels shrink with the band: name only
   (no kind tag), tighter truncation, a smaller dot, the hub caption tucked in. */
.live-flow--compact .live-flow__canvas { height: clamp(180px, 26vh, 280px); }
.live-flow--compact .live-flow__figure { padding: var(--space-4); }
.live-flow--compact .live-flow__node-kind { display: none; }
.live-flow--compact .live-flow__node-label { font-size: var(--text-xs); }
.live-flow--compact .live-flow__node-name { max-width: 12ch; }
.live-flow--compact .live-flow__node-dot {
  left: -4px; top: -4px; width: 8px; height: 8px;
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--neutral) 20%, transparent);
}
.live-flow--compact .live-flow__node--engine .live-flow__node-label { top: 18px; }
.live-flow--compact .live-flow__node--destination .live-flow__node-label { top: 13px; }

/* The onboarding first-run embed: like the overview topology embed, the canvas
   is a pure read (the step's own copy carries the words) and the component's
   table is hidden here; the run verdict and the readiness checklist are the
   operable surfaces. Covers the SVG-fallback's table too. The canvas is a slim
   RIBBON: the moment always shows exactly one flow, and one stream through a
   tall band reads as emptiness where a tight band reads as a wire that is
   genuinely carrying the first backup. */
.ob-flow-embed { display: block; margin-top: var(--space-4); }
.ob-flow-embed .live-flow__table { display: none; }
.ob-flow-embed .topo__table { display: none; }
.ob-flow-embed .live-flow__figure,
.ob-flow-embed .topo__figure { margin: 0; }
.ob-flow-embed .live-flow__canvas { height: 132px; }
.ob-flow-embed .live-flow__figure { padding: var(--space-3); }

/* =============================================================================
   Aurora backdrop (design/03-visual-directions/aurora; design-system.md 1):
   the OPT-IN depth skin. A faint three-hue wash (indigo accent, teal, violet)
   in huge blurred blobs drifting very slowly behind the app canvas. Hidden
   unless the operator sets data-aurora="on" (the Settings toggle via
   lib/aurora-pref.ts). Purely decorative: aria-hidden, pointer-events none,
   carries no information, never prints, and the drift sits behind the global
   prefers-reduced-motion gate above (the wash then holds as a static tint).
   The hues are the aurora direction's own values, theme-tuned (stronger in
   the dark themes where the near-black canvas absorbs them).
   ========================================================================== */
:root {
  --aurora-1: rgba(59, 102, 240, 0.10);  /* indigo */
  --aurora-2: rgba(15, 157, 142, 0.08);  /* teal */
  --aurora-3: rgba(122, 82, 214, 0.08);  /* violet */
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --aurora-1: rgba(80, 120, 255, 0.16);
    --aurora-2: rgba(20, 200, 180, 0.12);
    --aurora-3: rgba(150, 110, 255, 0.14);
  }
}
:root[data-theme="dark"] {
  --aurora-1: rgba(80, 120, 255, 0.16);
  --aurora-2: rgba(20, 200, 180, 0.12);
  --aurora-3: rgba(150, 110, 255, 0.14);
}

.aurora-field { display: none; }
:root[data-aurora="on"] .aurora-field {
  display: block;
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  overflow: hidden;
  background: var(--bg);
}
.aurora-field::before,
.aurora-field::after {
  content: "";
  position: absolute;
  border-radius: 9999px;
  filter: blur(80px);
}
.aurora-field::before {
  width: 60vw; height: 60vw; top: -22vw; left: -12vw;
  background: radial-gradient(circle at 30% 30%, var(--aurora-1), transparent 62%);
  animation: dp-aurora-a 38s var(--ease-in-out) infinite alternate;
}
.aurora-field::after {
  width: 55vw; height: 55vw; bottom: -26vw; right: -14vw;
  background: radial-gradient(circle at 60% 60%, var(--aurora-3), transparent 62%);
  animation: dp-aurora-b 46s var(--ease-in-out) infinite alternate;
}
.aurora-field__teal {
  position: absolute;
  width: 46vw; height: 46vw; top: 34%; left: 42%;
  background: radial-gradient(circle at 50% 50%, var(--aurora-2), transparent 60%);
  border-radius: 9999px;
  filter: blur(90px);
  animation: dp-aurora-c 52s var(--ease-in-out) infinite alternate;
}
@keyframes dp-aurora-a { from { transform: translate3d(0, 0, 0); } to { transform: translate3d(6vw, 4vw, 0); } }
@keyframes dp-aurora-b { from { transform: translate3d(0, 0, 0); } to { transform: translate3d(-5vw, -3vw, 0); } }
@keyframes dp-aurora-c { from { transform: translate3d(0, 0, 0) scale(1); } to { transform: translate3d(-4vw, 3vw, 0) scale(1.08); } }
@media print {
  .aurora-field { display: none !important; }
}

/* =============================================================================
   Cost calculator (/costs): inputs grid, pricing, results (stat tiles + the
   projection table + the sparkline area), the assumptions panel and the mode
   toggle. Stat tiles, the projection/sensitivity data-tables and the sparklines
   are existing components with their own CSS (.stat*, .stat-grid, .dp-table*,
   .spark*); this layer is the screen's layout + the bespoke mode toggle, on the
   same tokens, so both themes apply for free. Every figure is an estimate;
   nothing here implies a quote.
   ========================================================================== */

/* The two-column inputs / pricing grid (collapses to one column when narrow;
   the screen sets the grid inline, this is the responsive collapse + a floor so
   the columns never crush). */
.cost-form-grid { min-width: 0; }
@media (max-width: 880px) {
  .cost-form-grid { grid-template-columns: minmax(0, 1fr) !important; }
}

/* ---- Mode toggle: a two-option segmented control (the radiogroup idiom) ----
   Reads by shape + label and is keyboard operable. The active segment carries a
   raised surface + the accent text; the inactive ones are quiet. Disabled
   (observed-without-a-seed) dims with a not-allowed cursor and keeps its title
   reason. The control sits on a single inset track so the two options read as
   one switch, not two loose buttons. */
.cost-mode { /* a .card; nothing extra at the card level */ }
.cost-mode__note { margin-top: var(--space-3); }
.cost-segmented {
  display: inline-flex;
  padding: var(--space-1);
  gap: var(--space-1);
  background: var(--bg-subtle);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.cost-segmented__btn {
  appearance: none;
  display: inline-flex; align-items: center; justify-content: center;
  height: var(--control-h-sm);
  padding: 0 var(--space-4);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text-muted);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out),
              box-shadow var(--dur-fast) var(--ease-out);
}
.cost-segmented__btn:hover:not(:disabled):not(.cost-segmented__btn--active) {
  color: var(--text);
  background: var(--surface-hover);
}
.cost-segmented__btn--active {
  background: var(--surface);
  color: var(--accent-subtle-fg);
  border-color: var(--border);
  box-shadow: var(--shadow-sm);
}
.cost-segmented__btn:disabled { opacity: 0.5; cursor: not-allowed; }
.cost-segmented__btn:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
}

/* The empty-state estimate banner + the egress fact note both use .card
   --inset; these only set rhythm/measure around their copy. */
.cost-estimate-banner { /* margin is inline; on the inset card surface already */ }
.cost-egress-note { /* on the inset card surface; copy is set inline */ }

/* ---- 2. Inputs section ---------------------------------------------------- */
.cost-inputs { display: block; }
/* Each logical input occupies a row with consistent vertical rhythm. */
.cost-field { display: grid; gap: var(--space-2); margin-bottom: var(--space-4); }
.cost-field:last-child { margin-bottom: 0; }

/* The source-size row: a number field beside a unit select on one line, with the
   field flexing and the unit a fixed, baseline-aligned trailer. */
.cost-field-row {
  display: flex;
  align-items: flex-end;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}
.cost-field-row > .field { flex: 1 1 auto; min-width: 0; }
.cost-field-row__unit { flex: none; }
/* The unit select is sized to its content, not stretched full width. */
.cost-unit { width: auto; min-width: 76px; }

/* The custom-cadence sub-field nests under the cadence select (the screen
   toggles its display); give it a small inset so it reads as a child control. */
.cost-cadence-custom { margin-top: var(--space-2); }
/* The derived "runs per month" read-out sits tight under its control. */
.cost-derived { margin-top: var(--space-1); }

/* The churn low/expected/high quick-set: a small wrap of ghost buttons. */
.cost-quickset { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.cost-quickset__btn { /* a .btn--ghost --sm; layout handled by the row */ }

/* The drills/restores pair share a row, collapsing when narrow. */
.cost-ops {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--space-3);
}
@media (max-width: 480px) {
  .cost-ops { grid-template-columns: minmax(0, 1fr); }
}

/* ---- 3. Pricing section --------------------------------------------------- */
.cost-pricing { display: block; }
/* The four editable rate fields in a responsive two-up grid. */
.cost-price-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--space-3);
  margin-top: var(--space-3);
}
@media (max-width: 480px) {
  .cost-price-grid { grid-template-columns: minmax(0, 1fr); }
}

/* ---- 4. Results: headline, split tiles, projection, sparklines, marginals -- */
.cost-results { display: block; }

/* The headline figure card: the projected month-12 monthly cost, the largest
   single number on the screen, so it gets the most presence. The value reads at
   the display size with tabular numerals so it does not jiggle on recompute.
   It must clearly OUT-RANK the four split tiles (storage / writes / reads /
   egress) beneath it: a near-equal weight/size made the total and its parts
   read as peers, which inverts the hierarchy (the total is the headline, the
   tiles are its decomposition). So the headline card gets its own full-width
   emphasis (it spans the results column, not a tile-sized box) and the value is
   pushed to the top of the type scale with the heaviest weight; the tiles are
   then toned DOWN below, opening an unmistakable rank between them. */
.cost-headline {
  display: grid;
  gap: var(--space-2);
  border-color: var(--border-strong);
}
.cost-headline__value {
  /* Largest token on the scale (--text-3xl is the top step) scaled up via a
     fluid clamp so the total dominates on a wide results column while never
     overflowing the card on a narrow one. The floor is --text-3xl (34px); the
     ceiling is a touch larger so the headline reads as a display figure, not
     just one step up from the 28px tile value. Stays within the type-scale
     intent (no raw size beyond the spirit of the ramp). */
  font-size: clamp(var(--text-3xl), 4.6vw, 46px);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

/* The split tiles (storage / writes / reads / egress) render via statGrid +
   statTile, which already carry their CSS; the headline + the grid just need
   rhythm between them. The tiles are the headline's DECOMPOSITION, so their
   value tone is stepped down here (from the default --text-2xl stat value to
   --text-xl, and from semibold to medium) so the projected total clearly
   out-ranks its four components in size AND weight, in both themes. The label
   stays as-is so the tiles still read as labelled figures. */
.cost-results .stat-grid { margin-top: var(--space-4); }
.cost-results .stat-grid .stat__value {
  font-size: var(--text-xl);
  font-weight: var(--weight-medium);
}

/* The one-off initial seal line: a quiet inset card, label then figure. */
.cost-initial { display: block; }

/* The projection block: heading, the by-month data-table, then the two
   cumulative-cost sparkline figures side by side. */
.cost-projection { display: block; }
.cost-charts { /* flex layout is inline; the figures flex to fill */ }
.cost-chart {
  flex: 1 1 240px;
  min-width: 0;
  margin: 0;
  padding: var(--space-3) var(--space-4);
  background: var(--surface-inset);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius);
}
/* The sparkline itself fills the figure width; the component owns its tone
   colour (.spark--accent / .spark--trust), so this only sizes the canvas. */
.cost-chart .spark-wrap { display: block; width: 100%; }
.cost-chart .spark { width: 100%; height: auto; }

/* The marginal/one-off costs grid: stacked inset rows, two-up on wide. */
.cost-marginal { display: block; }
.cost-marginal__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--space-3);
}
@media (max-width: 640px) {
  .cost-marginal__grid { grid-template-columns: minmax(0, 1fr); }
}
.cost-marginal__row { display: block; }

/* The retention insight: a quiet inset note with a lead-in label. */
.cost-retention { display: block; }

/* The sensitivity view: three side-by-side blocks (churn / retention / cadence),
   each a small data-table. The grid layout is inline on the element; the block
   width floor is set there too, so this only keeps the heading rhythm. */
.cost-sensitivity { display: block; }
/* The three blocks wrap and flex (layout set inline on the element); each block
   keeps a sensible width floor so the small tables stay legible side by side. */
.cost-sensitivity__grid { /* flex/wrap/gap set inline; blocks size below */ }
.cost-sensitivity__block { flex: 1 1 240px; min-width: 0; }

/* ---- 5. Assumptions and limits panel (always visible) -------------------- */
.cost-assumptions { display: block; }
.cost-assumptions__list {
  margin: 0;
  padding-left: var(--space-5);
  display: grid;
  gap: var(--space-2);
  color: var(--text);
  font-size: var(--text-sm);
  line-height: var(--leading-base);
}
.cost-assumptions__list li { color: var(--text); }

/* =============================================================================
   13. Overview screen layout (.ov-grid and related). CRAFT-03: the two-column
   lower region must collapse to a single column under the wide breakpoint so
   the fleet table and the side column stack on medium and compact (WCAG 1.4.10
   Reflow). Built only from the semantic token layer; both Obsidian (dark) and
   the light companion apply for free. This stylesheet is the ONLY source of
   these rules (the injection-era fallback in overview.ts is long gone).
   ========================================================================== */

/* The composite two-column layout for the overview body: wider fleet column on
   the left, narrower side column (recovery posture card, embedded maps) on the
   right. Collapses to a single column at the medium/compact breakpoint (1023px),
   matching the shell's rail-toggle threshold, so the content never forces
   horizontal overflow. */
.ov-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
  gap: var(--space-5);
  align-items: start;
  margin-top: var(--space-6);
}

/* The side column stacks its cards vertically with consistent rhythm. */
.ov-side { display: flex; flex-direction: column; gap: var(--space-5); }

/* The two embedded cards (topology summary and cost estimate) sit side by side
   on wide, stack on medium/compact. Each carries min-width:0 so a long flow
   name never overflows its column. */
.ov-embed-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--space-5);
  margin-top: var(--space-6);
}
.ov-embed-grid > * { min-width: 0; }

/* The compact topology embed: a contained read-only map; the full /map link is
   the accessible operable surface. The table is hidden in the embed (the card
   carries a text summary so the region is not silent to assistive tech). */
.ov-topo-embed .topo__table { display: none; }
.ov-topo-embed .topo__figure { margin: 0; }
.ov-topo-embed .topo-svg { max-height: 240px; width: 100%; height: auto; }
/* The live-flow embed (the card's default visual) follows the same contract:
   the canvas is a pure read, the component's table is hidden here (the fleet
   table above the card is the screen's canonical data). */
.ov-topo-embed .live-flow__table { display: none; }
.ov-topo-embed .live-flow__figure { margin: 0; }

/* The cost-estimate figures inside the overview side card. */
.ov-cost-figures { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-5); }
.ov-cost-fig { display: flex; flex-direction: column; gap: 2px; }
.ov-cost-fig__value { font-size: var(--text-xl); font-weight: var(--weight-semibold); }

/* CRAFT-03 responsive collapse. Single column under the medium breakpoint
   (1023px, the same threshold the shell uses for rail-toggle) so the fleet
   table and the side column stack vertically on medium and compact viewports
   (WCAG 1.4.10 Reflow at 320px / 1280px-equivalent 400%). */
@media (max-width: 1023px) {
  /* The owner scans "what needs me?" first: on a single column the side rail
     (needs-attention + recovery posture) rises above the fleet table (sweep:
     medium-collapse-needsme-below-table). */
  .ov-grid > .ov-side { order: -1; }
  .ov-grid        { grid-template-columns: 1fr; }
  .ov-embed-grid  { grid-template-columns: 1fr; }
}

/* 13a. Recovery posture (overview side column). The standing posture reads as first-class stat
   tiles matching the status row at the top of the page, with the fleet Drill all action beside
   its heading, rather than a demoted disclosure. The tiles reuse .stat / .stat-grid, but with a
   smaller column minimum than the top status grid so they hold an even two-up layout inside the
   narrower side column (the content measure caps at 1280px, so the side column never widens
   enough to break the two-up into an odd three-up); they collapse to one column only on the
   smallest screens, where the top status grid is single-column too. The .ov-recovery descendant
   selector outweighs the base .stat-grid column rule regardless of source order. */
.ov-recovery { display: flex; flex-direction: column; gap: var(--space-4); }
.ov-recovery__head {
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; gap: var(--space-2) var(--space-3);
}
.ov-recovery__title { margin: 0; font-size: var(--text-lg); font-weight: var(--weight-semibold); }
.ov-recovery .stat-grid { grid-template-columns: repeat(auto-fill, minmax(165px, 1fr)); gap: var(--space-3); }
@media (max-width: 480px) { .ov-recovery .stat-grid { grid-template-columns: 1fr; } }

/* =============================================================================
   14. Command palette (.cmdp and related). This stylesheet is the single
   authoritative source for the palette rules; command-palette.ts's
   ensureStyles() is a no-op marker kept only so isolated test environments
   without the stylesheet do not re-check on every open (it injects no CSS).
   Built only from the semantic token layer; both Obsidian (dark) and the
   light companion apply for free. prefers-reduced-motion is honoured below.
   WCAG: the active row's 2px leading accent bar clears 3:1 against the surface
   (the tinted background alone does not, and is reinforcement only); the focus
   ring is the shared --ring; state is conveyed by the bar + tint AND
   aria-selected, not colour alone.
   ========================================================================== */

/* The palette anchors near the top (not centred) and sits ABOVE modals on the
   z-ladder (--z-palette > --z-modal), since it can be summoned from anywhere
   (design-system 4.6). The overlay engine mounts it as a "modal" variant; this
   lifts that one instance. */
.overlay:has(> .cmdp) { align-items: flex-start; z-index: var(--z-palette); }

.cmdp {
  width: min(640px, calc(100vw - var(--space-6)));
  margin-top: 12vh;
  max-height: 70vh;
  display: flex;
  flex-direction: column;
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
}

.cmdp__search {
  display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--border-subtle);
}

/* The search bar IS the focus surface: a focus-within ring keeps the
   visible-focus contract (the input itself suppresses its own outline so the
   ring is not doubled), matching the .target-opt:focus-within precedent above.
   >=3:1 via --ring. */
.cmdp__search:focus-within { outline: 2px solid var(--ring); outline-offset: -2px; }

.cmdp__search-icon { display: inline-flex; color: var(--text-muted); flex: none; }

.cmdp__input {
  flex: 1 1 auto;
  border: none; outline: none; background: transparent;
  color: var(--text);
  font-family: var(--font-sans);
  font-size: var(--text-md);
  line-height: var(--leading-snug);
  min-width: 0;
}
.cmdp__input::placeholder { color: var(--text-muted); }

.cmdp__list {
  list-style: none; margin: 0; padding: var(--space-2);
  overflow-y: auto; flex: 1 1 auto;
}

.cmdp__group {
  font-size: var(--text-xs); font-weight: var(--weight-semibold);
  color: var(--text-muted);
  padding: var(--space-3) var(--space-2) var(--space-1);
}

.cmdp__row {
  display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius);
  cursor: pointer;
  color: var(--text);
  scroll-margin: var(--space-3);
}
.cmdp__row-icon { display: inline-flex; color: var(--text-muted); flex: none; }
.cmdp__row-title {
  flex: 1 1 auto; font-size: var(--text-sm); min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The keyboard cursor: a 2px leading accent bar (>=3:1 against the surface — the load-bearing
   indicator, matching the nav-item idiom) over the accent tint (which alone was ~1.13:1 as
   --surface-active and is kept only as reinforcement). aria-selected carries the state to AT. */
.cmdp__row--active {
  background: var(--accent-subtle-bg);
  box-shadow: inset 2px 0 0 var(--accent);
}
.cmdp__row--active .cmdp__row-icon { color: var(--accent); }
.cmdp__mark { background: transparent; color: var(--accent); font-weight: var(--weight-semibold); }
.cmdp__row-hint {
  flex: none; display: inline-flex; align-items: center; gap: 2px;
  font-size: var(--text-xs); color: var(--text-muted);
}
.cmdp__row-hint--flow {
  color: var(--warn-fg); background: var(--warn-bg);
  border: 1px solid var(--warn-border);
  padding: 0 var(--space-2); height: 18px; border-radius: var(--radius-full);
}
.cmdp__row-hint--meta { color: var(--text-muted); font-variant-numeric: tabular-nums; }

.cmdp__empty, .cmdp__spinner {
  display: flex; flex-direction: column; gap: var(--space-1);
  padding: var(--space-5) var(--space-4); text-align: center;
}
.cmdp__spinner {
  flex-direction: row; align-items: center; justify-content: center;
  gap: var(--space-3); text-align: left;
}
.cmdp__empty-title { font-size: var(--text-sm); font-weight: var(--weight-medium); color: var(--text); }

.cmdp__footer {
  display: flex; align-items: center; gap: var(--space-4);
  padding: var(--space-2) var(--space-4);
  border-top: 1px solid var(--border-subtle);
  background: var(--bg-subtle);
  font-size: var(--text-xs); color: var(--text-muted);
}
.cmdp__footer-note { margin-left: auto; }
.cmdp__hint { display: inline-flex; align-items: center; gap: var(--space-1); }
.cmdp__hint-label { color: var(--text-muted); }

/* The small spinner: a static ring under reduced motion (the global reduced-motion
   gate in section 9 freezes the spin; this defines the ring and the animation it
   gates). Shared by the palette loading state. */
.spinner {
  display: inline-block; width: 16px; height: 16px; flex: none;
  border: 2px solid var(--border-strong); border-top-color: var(--accent);
  border-radius: var(--radius-full);
  animation: cmdp-spin 0.7s linear infinite;
}
.spinner--sm { width: 14px; height: 14px; }
@keyframes cmdp-spin { to { transform: rotate(360deg); } }

@media (prefers-reduced-motion: reduce) {
  .spinner { animation: none; }
}

/* The cheat-sheet rows on the /command-palette landing page. */
.shortcut-list { display: flex; flex-direction: column; }
.shortcut-row {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-4);
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--border-subtle);
}
.shortcut-row:last-child { border-bottom: none; }
.shortcut-row__what { font-size: var(--text-sm); color: var(--text); }
.shortcut-row__keys { display: inline-flex; align-items: center; gap: 2px; flex: none; color: var(--text-muted); }
.shortcut-row__then { color: var(--text-muted); font-size: var(--text-xs); }

/* Compact viewport: the palette stretches wider and drops lower so it does not
   crowd the Compact context bar (which wraps to two rows at 767px). */
@media (max-width: 767px) {
  .cmdp { width: calc(100vw - var(--space-4)); margin-top: 8vh; max-height: 80vh; }
}

/* =============================================================================
   15. Calm foundation (UI-CALM-TRACKER Wave 1; audit CALM-01/02/03/08, UIA-27).
   The calm-density rules: a section is a heading + whitespace on the canvas;
   boxes are reserved for interactive or elevated objects; rhythm comes from
   space, not borders; informational notices are quiet lines, with the reserved
   tints kept for action-needed-now. These primitives carry the per-screen
   diets (Wave 2).
   ========================================================================== */

/* Screen rhythm: the scaffold stacks a screen's major blocks with section-scale
   gaps, so groups form without boxes. The page-header's own margin is zeroed
   (the gap owns the rhythm). */
.screen-stack { display: flex; flex-direction: column; gap: var(--space-8); }
.screen-stack > .page-header { margin-bottom: 0; }

/* A quiet grouping: content that previously sat in a bordered card but is not
   an interactive/elevated object. Heading + content, no box. */
.card--quiet { background: transparent; border: none; padding: 0; border-radius: 0; }

/* Double-border kill (CALM-02): a bordered table wrap inside a card drops its
   own border — the card already provides the containment. */
.card > .dp-table-wrap,
.card > .data-table .dp-table-wrap { border: none; border-radius: 0; }

/* Unboxed section heading (the settings template; Wave 2 diets adopt it):
   sections are heading + whitespace, not cards. */
.section-title { font-size: var(--text-md); font-weight: var(--weight-semibold); margin-bottom: var(--space-3); }

/* Lazy sections (CALM-01): a collapsed disclosure whose summary carries the
   section's REAL heading, so heading navigation and the outline survive the
   demotion. The danger tone marks the offboarding/danger-zone group. */
.disclosure > summary { list-style-position: outside; }
.disclosure__heading { display: inline; font-size: var(--text-md); font-weight: var(--weight-semibold); }
.disclosure--danger > summary .disclosure__heading { color: var(--danger-fg); }

/* Quiet note (CALM-04): the calm replacement for an informational banner — one
   muted line with a glyph; no border, no tint. The tinted banner family stays
   reserved for action-needed-now. */
.note-quiet {
  display: flex; align-items: flex-start; gap: var(--space-2);
  font-size: var(--text-sm); color: var(--text-muted);
}
.note-quiet svg { flex: none; margin-top: 2px; width: 14px; height: 14px; }
.note-quiet .disclosure { border: none; padding: 0; }

/* Quiet pointer row (overview "More"): linklike entries to the screens whose
   detail this screen no longer embeds (CALM-10). */
.ov-more { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-5); }

/* Posture strip (CALM-04): ONE line carrying a screen's standing posture
   (dot + phrase per item), detail behind its own affordance. Replaces stacked
   caveat banners. */
.posture-strip {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: var(--space-2) var(--space-4);
  font-size: var(--text-sm); color: var(--text-muted);
}
.posture-strip__item { display: inline-flex; align-items: center; gap: var(--space-2); }

/* Segmented controls: one hover/active convention across every variant
   (UIA-27; previously only the cost calculator's had a hover). Selected states
   keep their tint. */
.dt-density__btn:hover:not([aria-pressed="true"]),
.type-seg__btn:hover:not([aria-checked="true"]),
.ob-theme__btn:hover:not([aria-pressed="true"]),
.theme-option:hover:not(.theme-option--active) { background: var(--surface-hover); color: var(--text); }
.dt-density__btn:active:not([aria-pressed="true"]),
.type-seg__btn:active:not([aria-checked="true"]),
.ob-theme__btn:active:not([aria-pressed="true"]),
.theme-option:active:not(.theme-option--active) { background: var(--surface-active); }

/* =============================================================================
   16. Forced colors (Windows High Contrast / Contrast Themes; UIA-13). The
   system palette replaces ours, so any state carried by a background fill
   alone vanishes. Repairs: real borders so shapes survive, Highlight outlines
   where a fill carried state, system keywords throughout (no brand colours).
   ========================================================================== */
@media (forced-colors: active) {
  /* The switch was background-only and disappeared entirely. */
  .switch .track { border: 1px solid ButtonText; }
  .switch .thumb { background: ButtonText; box-shadow: none; }
  .switch input:checked ~ .track { background: Highlight; border-color: Highlight; }
  .switch input:checked ~ .track .thumb { background: HighlightText; }
  /* Selected/active states whose tint is stripped: the system draws outlines. */
  .cmdp__row--active,
  .nav-item[aria-current="page"],
  .dt-facet[aria-pressed="true"],
  .dt-density__btn[aria-pressed="true"],
  .ob-theme__btn[aria-pressed="true"],
  .type-seg__btn[aria-checked="true"],
  .theme-option--active,
  .dp-table tbody tr[aria-selected="true"],
  .cal-day[aria-selected="true"] { outline: 2px solid Highlight; outline-offset: -2px; }
  /* Run cells: status fills flatten; borders keep the cells visible and the
     dashed in-flight treatment still reads (the x mark + aria-label carry status). */
  .run-cell { border: 1px solid ButtonText; }
  .run-cell--inflight { border-style: dashed; }
  /* Calendar day cells: tinted fills flatten; borders keep available/failed-only/beyond-ring
     distinguishable from the plain "empty" default and from each other by border style alone. */
  .cal-day--available { border: 1px solid ButtonText; }
  .cal-day--failed-only { border-style: double; }
  .cal-day--beyond-ring { border-style: dashed; }
  /* Skeletons would render as invisible boxes. */
  .skeleton { background: Canvas; border: 1px solid GrayText; }
}

/* =============================================================================
   17. Increased contrast (prefers-contrast: more; UIA-14). Raises secondary
   text toward the primary tone, hairlines to the control-boundary tone, and
   thickens the focus ring — the practical AAA 1.4.6 tier without changing the
   default skin. The in-app setting (data-contrast="more", Wave 5) mirrors
   these exact overrides.
   ========================================================================== */
@media (prefers-contrast: more) {
  :root {
    --text-muted: var(--n-700);     /* ~10:1 on the light canvas */
    --border: var(--n-450);
    --border-subtle: var(--n-300);
    --placeholder: var(--n-600);
  }
  @media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
      --text-muted: var(--n-200);   /* ~12:1 on the dark surface */
      --border: var(--n-500);
      --border-subtle: var(--n-600);
      --placeholder: var(--n-300);
    }
  }
  :root[data-theme="dark"] {
    --text-muted: var(--n-200);
    --border: var(--n-500);
    --border-subtle: var(--n-600);
    --placeholder: var(--n-300);
  }
  :where(a, button, input, select, textarea, summary, [tabindex], [role="button"],
         [role="option"], [role="tab"], [role="menuitem"]):focus-visible {
    outline-width: 3px;
  }
}

/* iOS focus-zoom guard (UIA-16): every focusable text input at >=16px under a
   coarse pointer, so Safari does not zoom the viewport on focus. Desktop keeps
   the 14px scale. */
@media (pointer: coarse) {
  .input, .textarea, .select, .cmdp__input, .import-list { font-size: 1rem; }
}

/* =============================================================================
   18. In-app accessibility settings (Wave 5; audit §8). Attribute twins of the
   OS-preference gates above: Settings > Accessibility writes these attributes
   via lib/a11y-prefs.ts; "System"/default REMOVES the attribute so the media
   queries (or base styles) rule — the settings AUGMENT the user agent, never
   compete with it (the GDS rule).
   ========================================================================== */

/* Motion (twin of the prefers-reduced-motion gate, section 9). */
:root[data-motion="reduced"] *,
:root[data-motion="reduced"] *::before,
:root[data-motion="reduced"] *::after {
  animation-duration: 0.001ms !important;
  animation-iteration-count: 1 !important;
  transition-duration: 0.001ms !important;
  scroll-behavior: auto !important;
}
:root[data-motion="reduced"] .skeleton { background: var(--skeleton-base); animation: none; }
:root[data-motion="reduced"] .btn__spinner { animation: none; opacity: 0.6; }
:root[data-motion="reduced"] .spinner { animation: none; }
:root[data-motion="reduced"] .async-region.is-refetching::before {
  background: color-mix(in srgb, var(--accent) 70%, transparent);
}
:root[data-motion="reduced"] .async-region.is-refetching::after { display: none; }
/* The explicit "Full" motion choice animates the console even when the OS
   prefers reduced motion (the operator's own persisted opt-in; the canvas
   honours the same override in lib/a11y-prefs.ts motionOK / the renderers). */
:root[data-motion="full"] .async-region.is-refetching::before {
  background: color-mix(in srgb, var(--accent) 16%, transparent);
}
:root[data-motion="full"] .async-region.is-refetching::after { display: block; }

/* Contrast (twin of prefers-contrast: more, section 17). */
:root[data-contrast="more"] {
  --text-muted: var(--n-700);
  --border: var(--n-450);
  --border-subtle: var(--n-300);
  --placeholder: var(--n-600);
}
@media (prefers-color-scheme: dark) {
  :root[data-contrast="more"]:not([data-theme="light"]) {
    --text-muted: var(--n-200);
    --border: var(--n-500);
    --border-subtle: var(--n-600);
    --placeholder: var(--n-300);
  }
}
:root[data-contrast="more"][data-theme="dark"] {
  --text-muted: var(--n-200);
  --border: var(--n-500);
  --border-subtle: var(--n-600);
  --placeholder: var(--n-300);
}
:root[data-contrast="more"] :where(a, button, input, select, textarea, summary, [tabindex], [role="button"],
       [role="option"], [role="tab"], [role="menuitem"]):focus-visible {
  outline-width: 3px;
}

/* Link underlines: at rest, everywhere (GitHub's accessibility toggle). */
:root[data-underlines="always"] a,
:root[data-underlines="always"] .linklike,
:root[data-underlines="always"] .stepper__link { text-decoration: underline; }

/* Large targets (the AAA 2.5.5 tier): the compact-viewport 44px rule at every
   viewport, plus roomier run cells. */
:root[data-targets="large"] .nav-item,
:root[data-targets="large"] .btn,
:root[data-targets="large"] .engine-chip,
:root[data-targets="large"] .input,
:root[data-targets="large"] .select,
:root[data-targets="large"] .dt-facet,
:root[data-targets="large"] .dt-density,
:root[data-targets="large"] .theme-option,
:root[data-targets="large"] .palette-trigger { min-height: var(--control-h-lg); }
:root[data-targets="large"] .run-strip { gap: var(--space-2); }
:root[data-targets="large"] .run-cell { width: 24px; height: 28px; }

/* Always-visible focus ring: the :focus-visible contract extended to every
   focus, for operators who want the ring under pointer interaction too. */
:root[data-focus="always"] :where(a, button, input, select, textarea, summary, [tabindex], [role="button"],
       [role="option"], [role="tab"], [role="menuitem"]):focus {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
}

/* Colour-vision-friendly status palette (GitHub Protanopia/Deuteranopia
   precedent): ok shifts to the BLUE info family and danger to the AMBER warn
   family — every pair below is already contrast-proven in both themes. Status
   stays shape + label redundant (triangle warn, square+hatch+x danger), which
   is what disambiguates danger from warn under this palette; the Settings copy
   says so plainly. */
:root[data-status-palette="cvd"] {
  --ok: var(--info-500); --ok-bg: var(--info-100); --ok-fg: var(--info-600); --ok-border: var(--info-500); --ok-solid-fg: #ffffff;
  --danger: var(--warn-600); --danger-bg: var(--warn-100); --danger-fg: var(--warn-600); --danger-border: var(--warn-500); --danger-solid-fg: #ffffff;
}
@media (prefers-color-scheme: dark) {
  :root[data-status-palette="cvd"]:not([data-theme="light"]) {
    --ok: var(--info-300d); --ok-bg: var(--info-150d); --ok-fg: var(--info-300d); --ok-border: #1c3c63; --ok-solid-fg: var(--n-950);
    --danger: var(--warn-300d); --danger-bg: var(--warn-150d); --danger-fg: var(--warn-300d); --danger-border: #5a440f; --danger-solid-fg: var(--n-950);
  }
}
:root[data-status-palette="cvd"][data-theme="dark"] {
  --ok: var(--info-300d); --ok-bg: var(--info-150d); --ok-fg: var(--info-300d); --ok-border: #1c3c63; --ok-solid-fg: var(--n-950);
  --danger: var(--warn-300d); --danger-bg: var(--warn-150d); --danger-fg: var(--warn-300d); --danger-border: #5a440f; --danger-solid-fg: var(--n-950);
}

/* =============================================================================
   19. GUIDED SETUP (the setup-first IA: the strip, the locked rail, the checklist)
   The strip is chrome, not a banner: one quiet line + step pips between the
   context bar and the main region, surface-toned (no tint), hidden with the
   chrome on full-bleed routes. Locked rail items dim but stay visible and
   clickable (the route gate explains and redirects); the lock is never a trap.
   ========================================================================== */
.setup-strip {
  grid-area: setup;   /* seat the strip in its own .shell row (under the header), not a trailing implicit row */
  display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-2) var(--space-6);
  border-bottom: 1px solid var(--border);
  background: var(--bg-subtle);
  font-size: var(--text-sm); color: var(--text-muted);
}
.setup-strip[hidden] { display: none; }
[data-chrome="off"] .setup-strip { display: none; }
.setup-strip__pips { display: inline-flex; gap: var(--space-1); }
.setup-pip {
  width: 8px; height: 8px; border-radius: 50%;
  border: 1px solid var(--border-strong); background: transparent;
}
.setup-pip--done { background: var(--ok); border-color: var(--ok); }
.setup-pip--current { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-subtle-bg); }
.setup-strip__text { color: var(--text); }
.setup-strip__go { font-size: var(--text-sm); }
.setup-strip__dismiss {
  margin-left: auto; border: none; background: transparent; color: var(--text-muted);
  cursor: pointer; font-size: var(--text-sm); line-height: 1; padding: var(--space-1);
  border-radius: var(--radius-sm);
}
.setup-strip__dismiss:hover { color: var(--text); background: var(--surface-hover); }

.nav-item[data-setup-locked="true"] { opacity: 0.45; }
.nav-item[data-setup-locked="true"]:hover { opacity: 0.7; }

/* The Overview setup checklist (lib/setup-state.ts setupChecklistCard). */
.setup-check { list-style: none; padding: 0; margin: var(--space-4) 0 0; display: grid; gap: var(--space-3); }
.setup-check__item { display: flex; gap: var(--space-3); align-items: flex-start; }
.setup-check__mark {
  width: 22px; height: 22px; flex: none; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: var(--text-xs); font-weight: var(--weight-semibold);
  border: 1px solid var(--border-strong); color: var(--text-muted); background: var(--surface);
}
.setup-check__mark--done { background: var(--ok); border-color: var(--ok); color: var(--ok-solid-fg); }
.setup-check__mark--current { border-color: var(--accent); color: var(--accent-subtle-fg); background: var(--accent-subtle-bg); }
.setup-check__label { font-size: var(--text-sm); color: var(--text-muted); }
.setup-check__label--current { color: var(--text); font-weight: var(--weight-medium); }

/* Width relief (owner feedback 2026-06-11: content squished into a narrow middle
   column, forcing scroll). The catalogue grid lets the Sources tiers' product
   groups sit side by side where the viewport allows; ob-twocol pairs the wizard
   configure step's commands with its readiness checklist. Both collapse to one
   column naturally via auto-fit. */
.catalogue-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: var(--space-5) var(--space-7);
  align-items: start;
}

/* Long source lists (owner feedback 2026-06-12: 100+ namespaces made the page a wall).
   A large product group collapses to a one-line summary; expanded, its rows live in a
   height-bounded scroll area so the group occupies a fixed compact box you scroll
   within, never a page-long stack. The filter input narrows the rows live. */
.source-list { display: grid; gap: var(--space-1); }
.source-list--scroll {
  max-height: 340px; overflow-y: auto;
  border: 1px solid var(--border); border-radius: var(--radius);
  padding: var(--space-2) var(--space-3);
  /* contain the scroll so it never steals the page scroll on a trackpad flick */
  overscroll-behavior: contain;
}
.source-list__filter { margin-bottom: var(--space-2); }
/* The summary keeps the browser's default disclosure marker (consistent with every
   other .disclosure), with the icon + label + live selected-count inline after it. */
.source-list__summary { padding: var(--space-1) 0; }
.source-list__summary > span { vertical-align: middle; }
.source-list__details[open] > .source-list__summary { margin-bottom: var(--space-2); }
.ob-twocol {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(380px, 1fr));
  gap: var(--space-6);
  align-items: start;
  margin-top: var(--space-4);
}

/* The configure step (key install + readiness) is the widest content in the wizard: two
   side-by-side columns of install affordances and the live readiness checklist. The default
   .ob-content prose cap (680px) squished it into a tall single column that forced a lot of
   scrolling (the owner's complaint). Let this step use the full content width the .ob-body grid
   already affords (capped at the body's 1280px, minus the rail), so the two columns sit
   comfortably side by side without going edge-to-edge. The calm-density budget is respected: it
   is two readable columns, not a wall — each column's own content stays measure-bounded. */
.ob-content:has(.ob-configure) { max-width: none; }
.ob-configure { width: 100%; }
.ob-configure__cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(420px, 100%), 1fr));
  gap: var(--space-6);
  align-items: start;
  margin-top: var(--space-4);
}
/* The install affordances column keeps a readable measure within the wider layout. */
.ob-configure__install { min-width: 0; max-width: 640px; }
.ob-install__row .input { min-width: 16rem; flex: 1 1 18rem; }

/* The key CEREMONY uses the full width too (no narrow centred column, no long scroll): the
   must-do path (download the files, then save-and-continue) sits in the left column; the
   optional reference + the custody picker sit in the right column. It collapses to one column
   on narrow viewports. Each column stays readable on its own; the calm-density budget holds. */
.ob-content:has(.ob-ceremony) { max-width: none; }
/* The wide steps (ceremony + configure) also lift the whole onboarding container off its
   1280px centred cap so they use the real page width instead of sitting in the middle third.
   The narrow steps keep the centred measure. */
.ob-body:has(.ob-ceremony), .ob-body:has(.ob-configure) { max-width: none; }
.ob-ceremony {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
  gap: var(--space-7);
  align-items: start;
}
.ob-ceremony__main, .ob-ceremony__aside { min-width: 0; }
/* The aside's optional disclosures read calmly in a slightly narrower measure. */
.ob-ceremony__aside { max-width: 640px; }
@media (max-width: 900px) {
  .ob-ceremony { grid-template-columns: minmax(0, 1fr); gap: var(--space-5); }
  .ob-ceremony__aside { max-width: none; }
}

/* =============================================================================
   Ceremony ENTRY (pre-generation): the premium, type-led realisation. Scoped to
   the wizard so none of this leaks to the rest of the app. The entry gets a touch
   more width so the key-distribution panel breathes, while the prose inside it
   stays on a readable measure. The re-run notice is demoted from a saturated amber
   slab to a quiet left-rule note (one calm warning, never a box), and the wizard's
   primary CTA gains a whisper of depth.
   ========================================================================== */
.ob-content:has(.ob-ceremony-entry) { max-width: 920px; }
.ob-body:has(.ob-ceremony-entry) { max-width: none; }
.ob-ceremony-entry > p, .ob-ceremony-entry > .field__hint { max-width: 68ch; }
.ob-ceremony-entry > .section-label { display: block; margin-top: var(--space-7); margin-bottom: var(--space-1); }

/* The re-run warning: a calm left-rule notice, not a filled slab. Scoped to the
   wizard so other surfaces' verdict tones are unchanged. */
.ob-page .verdict--warn {
  background: transparent;
  border: 1px solid var(--border); border-left: 3px solid var(--warn);
}

/* The wizard's primary CTA: a confident button with a hair of depth (a vertical
   accent gradient + a soft shadow + a top inner-light), kept subtle so it reads as
   craft, not chrome. Scoped to the wizard so global buttons are untouched. */
.ob-page .btn--primary {
  background-image: linear-gradient(180deg, var(--accent), var(--accent-active));
  border-color: var(--accent-active);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.28), inset 0 1px 0 rgba(255, 255, 255, 0.10);
}
.ob-page .btn--primary:hover { background-image: linear-gradient(180deg, var(--accent-hover), var(--accent-active)); border-color: var(--accent-active); }
.ob-page .btn--primary:active { background-image: none; }
@media (prefers-reduced-motion: no-preference) {
  .ob-page .btn--primary { transition: background var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out); }
}

/* =============================================================================
   Wave 1 — event-driven "aliveness" motion (2026-06-13). Every effect is finite
   and purposeful (it explains a real change: a navigation, a forged key, a fresh
   poll, a confirmation, the palette opening); none is ambient and nothing loops,
   so the §1 / §4.7 / §7a calm contract holds — with ONE deliberate exception, the
   ambient field (the owner's atomic-green-snow), a continuous backdrop sanctioned
   like the opt-in Aurora wash. The IMPERATIVE effects (the downpipe charge in
   shell/app-shell.ts, the ambient field in components/ambient-field.ts) gate on
   motionOK() in JS — the single predicate — so only the CSS-driven flourishes below
   carry the reduced-motion twins.
   ========================================================================== */

/* The downpipe charge: static paint only — the motion is the Web Animations API in
   chargeRailMark() (which also pulses the mark's scale). A blue (accent) fill slightly
   wider than the rail mark's downspout, drawn down then faded, so the new screen reads as
   having "rained down the pipe". */
.dp-mark-charge {
  fill: none;
  stroke: var(--accent);
  stroke-width: 88;
  stroke-linecap: butt;
  stroke-dasharray: 100;
  pointer-events: none;
}

/* The ambient field (the owner's atomic-green-snow): a fixed canvas drifting faint
   green motes behind ALL content. Mirrors the Aurora layer — z-index -1, transparent,
   pointer-events none — so the page background shows through and content sits above.
   It runs only when motionOK() (the JS gate in components/ambient-field.ts), so under
   reduced motion the canvas stays empty. Hidden when the Aurora wash is on; never printed. */
.ambient-field {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
}
:root[data-aurora="on"] .ambient-field { display: none; }
@media print { .ambient-field { display: none !important; } }

/* Command-palette open (Expressive): the surface springs in (scale + rise), an
   accent rule draws beneath the search from the centre out, and the glyph settles
   from a hair lower. Pure CSS on mount (the surface is built fresh on each open). */
.cmdp { animation: dp-cmdp-in var(--dur) var(--ease-out); }
@keyframes dp-cmdp-in {
  from { opacity: 0; transform: translateY(-10px) scale(0.96); }
  to { opacity: 1; transform: none; }
}
.cmdp__search { position: relative; }
.cmdp__search::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -1px;
  height: 2px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: center;
  animation: dp-cmdp-draw var(--dur) var(--ease-out) forwards;
}
.cmdp__search-icon { animation: dp-cmdp-glyph var(--dur) var(--ease-out); }
@keyframes dp-cmdp-draw { to { transform: scaleX(1); } }
@keyframes dp-cmdp-glyph {
  from { transform: translateY(3px); opacity: 0; }
  to { transform: none; opacity: 1; }
}

/* Toast check-pop (Expressive): a one-shot scale+settle on the SUCCESS toast's check
   PLUS a single expanding ring, the cheapest "cared-for" detail. info/warn keep the
   plain rise (a warning must not feel celebratory). Plays once on mount; never repeats. */
.toast--success .toast__icon {
  position: relative;
  animation: dp-check-pop var(--dur) var(--ease-out);
  transform-origin: center;
}
.toast--success .toast__icon::after {
  content: "";
  position: absolute;
  inset: -5px;
  border-radius: var(--radius-full);
  border: 2px solid var(--ok);
  animation: dp-check-ring 520ms var(--ease-out) forwards;
  pointer-events: none;
}
@keyframes dp-check-pop {
  0% { transform: scale(0.5); opacity: 0; }
  60% { transform: scale(1.12); }
  100% { transform: scale(1); opacity: 1; }
}
@keyframes dp-check-ring {
  0% { transform: scale(0.6); opacity: 0.7; }
  100% { transform: scale(1.8); opacity: 0; }
}

/* Value-settle (Expressive): a one-shot wash on a stat tile whose value ACTUALLY
   changed on a poll (overview.ts diffs by the tile's unique label and adds .dp-settle,
   which self-removes on animationend). The dashboard breathes when reality changes
   and stays perfectly still otherwise. */
.stat.dp-settle { animation: dp-value-settle 700ms var(--ease-out); }
@keyframes dp-value-settle {
  0% { background: color-mix(in srgb, var(--ok) 22%, transparent); }
  100% { background: transparent; }
}

/* Reduced-motion twins for the CSS-driven flourishes (the imperative effects are
   already gated in JS via motionOK()). Mirrors BOTH the in-app data-motion="reduced"
   attribute and the OS media query, like the topology gate. These are pure
   flourishes carrying no information, so under reduced motion they simply do not
   play (the palette/toast/tile all keep their normal static appearance). */
:root[data-motion="reduced"] .cmdp,
:root[data-motion="reduced"] .toast--success .toast__icon,
:root[data-motion="reduced"] .toast--success .toast__icon::after,
:root[data-motion="reduced"] .cmdp__search::after,
:root[data-motion="reduced"] .cmdp__search-icon,
:root[data-motion="reduced"] .stat.dp-settle { animation: none; }
@media (prefers-reduced-motion: reduce) {
  .cmdp,
  .toast--success .toast__icon,
  .toast--success .toast__icon::after,
  .cmdp__search::after,
  .cmdp__search-icon,
  .stat.dp-settle { animation: none; }
}


/* ===========================================================================
   Canary feature illustration: a yellow canary in a hairline wire cage.

   Drops into the calm enterprise console. Conventions mirror the topology SVG
   (tokens.css section 9/10): ALL motion is OFF by default and only switches ON
   under :root[data-motion="full"]; the global prefers-reduced-motion hard gate
   (section 9) plus the explicit static fallbacks below keep every state legible
   with zero animation. Status is carried by SHAPE (marker outline + glyph), the
   bird's POSE and a text LABEL - never by colour alone (WCAG 1.4.1).

   The root <svg> carries exactly one state class:
     .canary--alive | .canary--dead | .canary--ailing | .canary--pending

   ---------------------------------------------------------------------------
   ILLUSTRATION COLOUR vs STATUS COLOUR
     The bird is canary yellow because that is illustration colour, not a status
     signal (the marker shape + glyph + label carry status). BUT when the check
     is NOT alive the body is DESATURATED toward a muted, lower-chroma yellow so
     the picture never looks cheerfully alive while the check is failing. This
     is a dimming of the same hue, not a re-colouring into a status palette.

   ---------------------------------------------------------------------------
   TUNING KNOBS (operability)
     Every animation's duration lives in a single named --dp-canary-*-dur var on
     .canary below, and is referenced from the :root[data-motion="full"] rule -
     edit the var once and it applies everywhere it is used. Amplitudes are
     deliberately tiny (sub-pixel-to-few-px, <=1deg) so the scene reads as
     "alive and breathing", never "busy". Durations are kept mutually
     co-prime-ish (7.3 / 3.1 / 6.7 / 5.3 / 4.1 / 5.9 / 6.1 / 5.7 / 7.9 / 9.1 s)
     so sway / breath / hop / blink / chirp never resync into a visible lockstep
     loop. Documented pivot anchors (transform-origin) sit beside each rule.

   ---------------------------------------------------------------------------
   INTEGRATION / MULTI-INSTANCE SAFETY
     This component intentionally uses NO url(#id) gradient / clip / filter / use
     references - the dome "glass" sheen is a static low-opacity overlay PATH, so
     a second svg.canary on the same page CANNOT collide. The only ids present
     are the a11y ids "canaryTitle" / "canaryDesc". If you render MORE THAN ONE
     canary in a single document, suffix BOTH ids per instance (e.g. -1, -2) and
     update each aria-labelledby to match. The same rule applies to ANY future
     gradient/clip/filter you add: give it a per-instance-suffixed id and a
     matching url() reference, or instances will share (and fight over) one def.
   ======================================================================== */

.canary {
  /* illustration palette --------------------------------------------------- */
  --canary-yellow:  #F5C518;   /* canary body (alive) */
  --canary-bright:  #FFD21E;   /* highlight / shine */
  --canary-shade:   #D9A406;   /* wing / beak / tail shade */
  /* desaturated twins used when the check is NOT alive (dead / ailing) ------ */
  --canary-yellow-muted: #CBB45E;   /* lower-chroma body */
  --canary-bright-muted: #DCC878;   /* lower-chroma highlight */
  --canary-shade-muted:  #A8923F;   /* lower-chroma shade */
  /* storm flourish: the umbrella the alive bird raises under Storm rain ------ */
  --canary-umbrella:       #3FB7AE;   /* canopy (illustration colour, not status) */
  --canary-umbrella-shade: #2E9890;   /* alternating canopy panels */
  --canary-umbrella-edge:  #2A8F86;   /* canopy outline */

  /* cage + plate, pulled from the console tokens --------------------------- */
  --cage-wire:      var(--border-strong, #b9bfc8);
  --cage-ink:       var(--text-muted, #5b6470);
  --plate-bg:       var(--surface-inset, #f4f5f7);
  --plate-edge:     var(--border-subtle, #e6e8ec);
  --dome-sheen:     var(--surface, #fff);   /* glass highlight = surface white */

  /* motion tuning knobs (one duration per loop) ---------------------------- */
  --dp-canary-sway-dur:    7.3s;   /* cage sway        (alive)   */
  --dp-canary-breath-dur:  3.1s;   /* chest breath     (alive)   */
  --dp-canary-hop-dur:     6.7s;   /* occasional hop   (alive)   */
  --dp-canary-blink-dur:   5.3s;   /* eye blink        (alive)   */
  --dp-canary-chirp-dur:   4.1s;   /* song notes       (alive)   */
  --dp-canary-uneasy-dur:  5.9s;   /* uneasy shiver    (ailing)  */
  --dp-canary-sleep-dur:   6.1s;   /* sleep breath     (pending) */
  --dp-canary-zz-dur:      5.7s;   /* sleep z marks    (pending) */
  --dp-canary-feather-a-dur: 7.9s; /* settling feather (dead)    */
  --dp-canary-feather-b-dur: 9.1s; /* settling feather (dead)    */

  display: block;
  width: 100%;
  height: auto;
  color: var(--cage-ink);
  overflow: visible;
}

/* ---- background plate ----------------------------------------------------- */
.canary-plate__bg {
  fill: var(--plate-bg);
  stroke: var(--plate-edge);
  stroke-width: 1.5px;
  transition: fill var(--dur, 0.18s) ease, stroke var(--dur, 0.18s) ease;
}
/* the dead plate darkens to a danger tint, reading as "the mine went dark" */
.canary--dead .canary-plate__bg {
  fill: var(--danger-150d, #f6eaea);
  stroke: var(--danger-border, #f3b7b4);
}

/* ===========================================================================
   STATUS MARKER (top-left badge): one SHAPE per state, glyph inside.
   circle=alive, square=dead, triangle=ailing, hollow ring=pending.
   ======================================================================== */
.canary-status__circle,
.canary-status__square,
.canary-status__triangle,
.canary-status__hollow,
.canary-status__tick,
.canary-status__cross,
.canary-status__bang,
.canary-status__dot { display: none; }

.canary-status__tick,
.canary-status__cross,
.canary-status__bang {
  fill: none;
  stroke-width: 2.4px;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* alive: green filled circle + tick */
.canary--alive .canary-status__circle { display: block; fill: var(--ok, #1f9d57); }
.canary--alive .canary-status__tick   { display: block; stroke: var(--accent-fg, #fff); }

/* dead: red filled square + cross */
.canary--dead .canary-status__square { display: block; fill: var(--danger, #cf2f2f); }
.canary--dead .canary-status__cross  { display: block; stroke: var(--danger-solid-fg, #fff); }

/* ailing: amber filled triangle + exclamation */
.canary--ailing .canary-status__triangle { display: block; fill: var(--warn, #a8730b); }
.canary--ailing .canary-status__bang      { display: block; stroke: var(--accent-fg, #fff); }

/* pending: neutral hollow ring + dot */
.canary--pending .canary-status__hollow {
  display: block; fill: none;
  stroke: var(--text-muted, #5b6470); stroke-width: 2px;
}
.canary--pending .canary-status__dot { display: block; fill: var(--text-muted, #5b6470); }

/* ===========================================================================
   THE CAGE - hairline wire
   ======================================================================== */
.canary-cage__cord,
.canary-cage__dome,
.canary-cage__hoop,
.canary-cage__bar,
.canary-cage__base-line {
  fill: none;
  stroke: var(--cage-wire);
  stroke-width: 2px;
  stroke-linecap: round;
}
.canary-cage__dome-rib {
  fill: none;
  stroke: var(--cage-wire);
  stroke-width: 1.4px;
  stroke-linecap: round;
  opacity: 0.7;
}
.canary-cage__ring {
  fill: none;
  stroke: var(--cage-wire);
  stroke-width: 2.4px;
}
.canary-cage__base {
  fill: var(--surface, #fff);
  stroke: var(--cage-wire);
  stroke-width: 2px;
  stroke-linejoin: round;
}
.canary-cage__bar--front { opacity: 0.92; }

/* glass-dome sheen: a static, token-driven, very-low-opacity highlight that
   gives the hairline cage a touch of premium depth. Implemented as a plain
   overlay PATH (no url() gradient) so multiple instances can never collide. */
.canary-cage__sheen {
  fill: var(--dome-sheen);
  opacity: 0.14;
  stroke: none;
  pointer-events: none;
}
/* on death the cheerful glass glint goes out */
.canary--dead .canary-cage__sheen { opacity: 0; }

/* dead: the cage darkens + tilts a little (static pose; the tilt is a transform) */
.canary--dead .canary-cage__dome,
.canary--dead .canary-cage__hoop,
.canary--dead .canary-cage__bar,
.canary--dead .canary-cage__dome-rib,
.canary--dead .canary-cage__cord,
.canary--dead .canary-cage__ring,
.canary--dead .canary-cage__base-line { stroke: var(--cage-ink); opacity: 0.85; }
.canary--dead .canary-cage__body { transform: rotate(-2.5deg); transform-origin: 120px 20px; }

/* the perch */
.canary-perch__bar {
  fill: none;
  stroke: var(--cage-ink);
  stroke-width: 3px;
  stroke-linecap: round;
}
.canary-perch__cap { fill: var(--cage-ink); }

/* ===========================================================================
   THE BIRD - shared fills. Yellow when alive; desaturated when not.
   ======================================================================== */
.canary-bird__body,
.canary-bird__skull,
.canary-deadbird__body,
.canary-deadbird__skull { fill: var(--canary-yellow); }

.canary-bird__wing,
.canary-bird__tail,
.canary-deadbird__tail,
.canary-bird__beak,
.canary-deadbird__beak { fill: var(--canary-shade); }

.canary-bird__shine,
.canary-deadbird__shine {
  fill: none;
  stroke: var(--canary-bright);
  stroke-width: 3px;
  stroke-linecap: round;
  opacity: 0.85;
}
.canary-bird__wing-line {
  fill: none;
  stroke: var(--canary-yellow);
  stroke-width: 1.4px;
  stroke-linecap: round;
  opacity: 0.6;
}
/* warmer, friendlier eye: bigger pupil + a white catch-light glint */
.canary-bird__eye   { fill: var(--text, #1a1d21); }
.canary-bird__catch { fill: #fff; opacity: 0.92; }
.canary-bird__lid {
  fill: none;
  stroke: var(--canary-shade);
  stroke-width: 2.4px;
  stroke-linecap: round;
}
.canary-deadbird__x {
  fill: none;
  stroke: var(--text, #1a1d21);
  stroke-width: 2.4px;
  stroke-linecap: round;
}
.canary-bird__leg,
.canary-deadbird__leg {
  fill: none;
  stroke: var(--cage-ink);
  stroke-width: 2px;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* DESATURATE when the check is not alive: dead + ailing shift the body OFF true
   #F5C518 toward a muted, lower-chroma yellow. Same hue family, just dimmed -
   so the illustration never looks happily alive over a failing check. The
   catch-light also fades a touch so the eye loses its "spark". */
.canary--dead   .canary-bird__body,
.canary--dead   .canary-bird__skull,
.canary--dead   .canary-deadbird__body,
.canary--dead   .canary-deadbird__skull,
.canary--ailing .canary-bird__body,
.canary--ailing .canary-bird__skull { fill: var(--canary-yellow-muted); }

.canary--dead   .canary-bird__wing,
.canary--dead   .canary-bird__tail,
.canary--dead   .canary-bird__beak,
.canary--dead   .canary-deadbird__tail,
.canary--dead   .canary-deadbird__beak,
.canary--ailing .canary-bird__wing,
.canary--ailing .canary-bird__tail,
.canary--ailing .canary-bird__beak { fill: var(--canary-shade-muted); }

.canary--dead   .canary-bird__shine,
.canary--dead   .canary-deadbird__shine,
.canary--ailing .canary-bird__shine { stroke: var(--canary-bright-muted); opacity: 0.6; }

.canary--ailing .canary-bird__catch { opacity: 0.5; }
.canary--dead   .canary-feather     { fill: var(--canary-yellow-muted); }

/* The live bird shows for alive / ailing / pending; the dead bird only for
   dead. Everything is hidden by default, then revealed per state. */
.canary-bird,
.canary-dead-bird,
.canary-feathers,
.canary-chirp,
.canary-sleep { display: none; }

.canary--alive   .canary-bird,
.canary--ailing  .canary-bird,
.canary--pending .canary-bird { display: block; }

.canary--dead .canary-dead-bird,
.canary--dead .canary-feathers { display: block; }

.canary--alive .canary-chirp { display: block; }
.canary--pending .canary-sleep { display: block; }

/* eyelid + catch-light hidden by default; revealed per state below */
.canary-bird__lid   { display: none; }
.canary-bird__catch { display: none; }

/* ---- ALIVE pose: upright, eye open + glint, singing ---------------------- */
.canary--alive .canary-bird__eye   { display: block; }
.canary--alive .canary-bird__catch { display: block; }

/* ---- AILING pose: hunched forward + uneasy ------------------------------- */
.canary--ailing .canary-bird {
  transform: translate(7px, 12px) rotate(5deg) scale(0.92);
  transform-origin: 120px 130px;
}
/* the head sinks into the shoulders and tips forward (distinct from upright) */
.canary--ailing .canary-bird__head {
  transform: translate(6px, 9px) rotate(9deg);
  transform-origin: 103px 108px;
}
.canary--ailing .canary-bird__eye   { display: block; }
.canary--ailing .canary-bird__catch { display: block; }
/* a heavy half-lid sits over the eye to read as unwell */
.canary--ailing .canary-bird__lid { display: block; opacity: 0.95; }

/* ---- PENDING pose: asleep, head tucked onto the back --------------------- */
.canary--pending .canary-bird {
  transform: scale(0.97);
  transform-origin: 120px 130px;
}
/* the head settles back and down onto the body so the silhouette rounds into a
   sleeping puff; a small rotation tips the (closed) eye to face out. */
.canary--pending .canary-bird__head {
  transform: translate(22px, 10px) rotate(20deg);
  transform-origin: 103px 108px;
}
/* eye closed: hide the dot + glint, show the lid as a calm shut curve */
.canary--pending .canary-bird__eye   { display: none; }
.canary--pending .canary-bird__catch { display: none; }
.canary--pending .canary-bird__lid   { display: block; }
/* beak tucked away behind the wing: hide it */
.canary--pending .canary-bird__beak { opacity: 0; }

/* ---- chirp + sleep marks: resting (static) styling ----------------------- */
.canary-chirp__note {
  fill: none;
  stroke: var(--ok, #1f9d57);
  stroke-width: 2.2px;
  stroke-linecap: round;
  opacity: 0.9;
}
.canary-sleep__z {
  fill: none;
  stroke: var(--text-muted, #5b6470);
  stroke-width: 2px;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: 0.8;
}

/* ---- feathers (dead): static resting position ---------------------------- */
.canary-feather { opacity: 0.7; }
.canary-feather--a { transform: translateY(56px); }
.canary-feather--b { transform: translateY(70px); }

/* ===========================================================================
   MOTION. OFF by default; ON only under :root[data-motion="full"]. Mirrors the
   topology SVG gating exactly. Reduced motion = the static poses above.

   Amplitudes are deliberately tiny; each duration is read from the named knob
   on .canary (above) so the value lives in exactly one place. Pivot anchors
   (transform-origin) are noted beside every rule.
   ======================================================================== */
@keyframes dp-canary-breath {
  0%, 100% { transform: scaleY(1) translateY(0); }
  50%      { transform: scaleY(1.045) translateY(-0.8px); }   /* tiny */
}
@keyframes dp-canary-blink {
  /* lid hidden most of the cycle, a quick double dip closes it */
  0%, 90%, 100% { opacity: 0; }
  92%, 95%      { opacity: 1; }
}
@keyframes dp-canary-blink-eye {
  0%, 90%, 100% { opacity: 1; }
  92%, 95%      { opacity: 0; }
}
@keyframes dp-canary-sway {
  0%, 100% { transform: rotate(-1deg); }     /* <=1deg */
  50%      { transform: rotate(1deg); }
}
@keyframes dp-canary-hop {
  0%, 72%, 100% { transform: translateY(0); }
  79%           { transform: translateY(-6px); }
  87%           { transform: translateY(0); }
  91%           { transform: translateY(-1.5px); }
  95%           { transform: translateY(0); }
}
@keyframes dp-canary-chirp {
  0%, 100% { opacity: 0; transform: translate(0,0) scale(0.6); }
  20%      { opacity: 0.95; transform: translate(-2px,-2px) scale(1); }
  70%      { opacity: 0.5; transform: translate(-7px,-7px) scale(1.12); }
  85%      { opacity: 0; transform: translate(-10px,-10px) scale(1.18); }
}
@keyframes dp-canary-feather {
  0%   { opacity: 0; }
  10%  { opacity: 0.7; }
  100% { transform: translateY(0); opacity: 0; }
}
@keyframes dp-canary-uneasy {
  0%, 100% { transform: translate(7px,12px) rotate(5deg) scale(0.92); }
  25%      { transform: translate(7px,12.6px) rotate(4.5deg) scale(0.92); }
  50%      { transform: translate(7.6px,12px) rotate(5.5deg) scale(0.92); }
  75%      { transform: translate(7px,12.6px) rotate(4.7deg) scale(0.92); }
}
@keyframes dp-canary-sleep-breath {
  0%, 100% { transform: scale(0.97); }
  50%      { transform: scale(0.982); }   /* tiny */
}
@keyframes dp-canary-zz {
  0%, 100% { opacity: 0; transform: translate(0,0); }
  30%      { opacity: 0.85; }
  70%      { opacity: 0.4; transform: translate(3px,-6px); }
  100%     { opacity: 0; transform: translate(5px,-9px); }
}

/* alive ------------------------------------------------------------------- */
:root[data-motion="full"] .canary--alive .canary-cage__body {
  /* knob: --dp-canary-sway-dur ; pivot: the hanging ring (120,20) */
  animation: dp-canary-sway var(--dp-canary-sway-dur) ease-in-out infinite;
  transform-origin: 120px 20px;
}
:root[data-motion="full"] .canary--alive .canary-bird {
  /* knob: --dp-canary-hop-dur ; pivot: the feet on the perch (120,150) */
  animation: dp-canary-hop var(--dp-canary-hop-dur) ease-in-out infinite;
  transform-origin: 120px 150px;
}
:root[data-motion="full"] .canary--alive .canary-bird__breath {
  /* knob: --dp-canary-breath-dur ; pivot: chest centre (120,140) */
  animation: dp-canary-breath var(--dp-canary-breath-dur) ease-in-out infinite;
  transform-origin: 120px 140px;
}
:root[data-motion="full"] .canary--alive .canary-bird__lid {
  display: block;
  animation: dp-canary-blink var(--dp-canary-blink-dur) ease-in-out infinite;
}
:root[data-motion="full"] .canary--alive .canary-bird__eye {
  animation: dp-canary-blink-eye var(--dp-canary-blink-dur) ease-in-out infinite;
}
:root[data-motion="full"] .canary--alive .canary-bird__catch {
  animation: dp-canary-blink-eye var(--dp-canary-blink-dur) ease-in-out infinite;
}
:root[data-motion="full"] .canary--alive .canary-chirp__note--1 {
  animation: dp-canary-chirp var(--dp-canary-chirp-dur) ease-out infinite;
}
:root[data-motion="full"] .canary--alive .canary-chirp__note--2 {
  animation: dp-canary-chirp var(--dp-canary-chirp-dur) ease-out 0.18s infinite;
}
:root[data-motion="full"] .canary--alive .canary-chirp__note--3 {
  animation: dp-canary-chirp var(--dp-canary-chirp-dur) ease-out 0.36s infinite;
}

/* ailing ------------------------------------------------------------------ */
:root[data-motion="full"] .canary--ailing .canary-bird {
  /* knob: --dp-canary-uneasy-dur ; pivot: body centre (120,130) */
  animation: dp-canary-uneasy var(--dp-canary-uneasy-dur) ease-in-out infinite;
  transform-origin: 120px 130px;
}

/* pending ----------------------------------------------------------------- */
:root[data-motion="full"] .canary--pending .canary-bird {
  /* knob: --dp-canary-sleep-dur ; pivot: body centre (120,130) */
  animation: dp-canary-sleep-breath var(--dp-canary-sleep-dur) ease-in-out infinite;
  transform-origin: 120px 130px;
}
:root[data-motion="full"] .canary--pending .canary-sleep__z--1 {
  animation: dp-canary-zz var(--dp-canary-zz-dur) ease-out infinite;
}
:root[data-motion="full"] .canary--pending .canary-sleep__z--2 {
  animation: dp-canary-zz var(--dp-canary-zz-dur) ease-out 0.6s infinite;
}

/* dead -------------------------------------------------------------------- */
:root[data-motion="full"] .canary--dead .canary-feather--a {
  animation: dp-canary-feather var(--dp-canary-feather-a-dur) ease-in 0.2s infinite;
}
:root[data-motion="full"] .canary--dead .canary-feather--b {
  animation: dp-canary-feather var(--dp-canary-feather-b-dur) ease-in 1.4s infinite;
}

/* ===========================================================================
   Reduced motion: hard belt-and-braces. The global section-9 gate already
   neutralises animation-duration; here we explicitly stop the per-part
   animations and keep every state legible from its static pose.
   ======================================================================== */
@media (prefers-reduced-motion: reduce) {
  .canary-cage__body,
  .canary-bird,
  .canary-bird__breath,
  .canary-bird__lid,
  .canary-bird__eye,
  .canary-bird__catch,
  .canary-feather { animation: none !important; }
  /* with no blink animation, the alive bird simply holds its eye open */
  .canary--alive .canary-bird__lid { display: none; }
  /* transient marks hold one calm frame instead of flashing */
  .canary-chirp__note { opacity: 0.9; }
  .canary-sleep__z    { opacity: 0.8; }
  .canary-feather     { opacity: 0.7; }
}
:root[data-motion="reduced"] .canary-cage__body,
:root[data-motion="reduced"] .canary-bird,
:root[data-motion="reduced"] .canary-bird__breath,
:root[data-motion="reduced"] .canary-bird__lid,
:root[data-motion="reduced"] .canary-bird__eye,
:root[data-motion="reduced"] .canary-bird__catch,
:root[data-motion="reduced"] .canary-feather { animation: none; }
:root[data-motion="reduced"] .canary--alive .canary-bird__lid { display: none; }

/* ===========================================================================
   Canary: the in-flight (flapping) pose + the dead-canary skeleton.
   Added after the owner asked for a flying state (flaps until it lands or dies)
   and a skeleton for death. Motion is gated exactly like the rest of the canary
   art: OFF by default, ON under :root[data-motion="full"], with a legible static
   pose for reduced motion. Status is still carried by shape + colour + label.
   ======================================================================== */

/* ---- IN FLIGHT: the cage falls away and the bird flies free, bobbing + flapping --- */
.canary--flying .canary-bird { display: block; }
.canary--flying .canary-bird__eye { display: block; }
.canary--flying .canary-bird__catch { display: block; }
.canary--flying .canary-chirp,
.canary--flying .canary-sleep { display: none; }
/* the cage, its hook and the perch disappear so the bird is in open air (legs tuck away too) */
.canary--flying .canary-cage__hook,
.canary--flying .canary-cage__dome,
.canary--flying .canary-cage__dome-rib,
.canary--flying .canary-cage__sheen,
.canary--flying .canary-cage__hoop,
.canary--flying .canary-cage__bar,
.canary--flying .canary-cage__base,
.canary--flying .canary-cage__base-line,
.canary--flying .canary-perch,
.canary--flying .canary-bird__legs { display: none; }

/* static (reduced-motion) flight pose: airborne and centred, wing raised mid-beat */
.canary--flying .canary-bird {
  transform: translate(4px, -16px) rotate(-7deg);
  transform-origin: 120px 130px;
}
.canary--flying .canary-bird__wing {
  transform: rotate(-26deg);
  transform-origin: 126px 122px;
}

/* ---- DEAD: a little canary skeleton replaces the slumped bird -------------- */
.canary-skeleton { display: none; }
.canary--dead .canary-skeleton { display: block; }
.canary--dead .canary-dead-bird { display: none; }
.canary-skeleton__bone {
  fill: none;
  stroke: #cfcabb;
  stroke-width: 2px;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.canary-skeleton__skull { fill: #ece8db; stroke: #cfcabb; stroke-width: 1.5px; }
.canary-skeleton__socket { fill: #2f3338; }
.canary-skeleton__beak { fill: var(--canary-shade-muted, #a8923f); }
.canary--dead .canary-skeleton__bone { stroke: #d7d2c4; }

/* ---- MOTION (gated): wing-flap + a gentle airborne bob; a slow lifeless settle for the skeleton --- */
@keyframes dp-canary-flap {
  0%, 100% { transform: rotate(-30deg); }
  50%      { transform: rotate(30deg); }
}
@keyframes dp-canary-airborne {
  0%, 100% { transform: translate(4px, -8px) rotate(-7deg); }
  50%      { transform: translate(4px, -26px) rotate(-3deg); }
}
@keyframes dp-canary-skeleton-settle {
  0%, 100% { transform: translateY(0); opacity: 0.95; }
  50%      { transform: translateY(0.7px); opacity: 1; }
}
:root[data-motion="full"] .canary--flying .canary-bird {
  animation: dp-canary-airborne 0.9s ease-in-out infinite;
  transform-origin: 120px 130px;
}
:root[data-motion="full"] .canary--flying .canary-bird__wing {
  animation: dp-canary-flap 0.3s ease-in-out infinite;
  transform-origin: 126px 122px;
}
:root[data-motion="full"] .canary--dead .canary-skeleton {
  animation: dp-canary-skeleton-settle 4.2s ease-in-out infinite;
}

@media (prefers-reduced-motion: reduce) {
  .canary--flying .canary-bird,
  .canary--flying .canary-bird__wing,
  .canary--dead .canary-skeleton { animation: none !important; }
}
:root[data-motion="reduced"] .canary--flying .canary-bird,
:root[data-motion="reduced"] .canary--flying .canary-bird__wing,
:root[data-motion="reduced"] .canary--dead .canary-skeleton { animation: none; }


/* ===========================================================================
   Canary singing: music notes that LEAVE the beak and float away.
   Replaces the old sound-wave arcs (which grew and drifted toward the bird).
   The notes pop out at the beak (bird faces left) and drift up-and-left, away
   from the bird, fading as they go. Staggered so the song reads as continuous.
   Motion gated as usual; reduced motion holds a couple of static notes.
   ======================================================================== */
.canary-note { opacity: 0.92; }
.canary-note__head { fill: var(--ok, #1f9d57); }
.canary-note__stem { fill: none; stroke: var(--ok, #1f9d57); stroke-width: 1.5px; stroke-linecap: round; }
.canary-note__flag { fill: var(--ok, #1f9d57); stroke: none; }

@keyframes dp-canary-sing {
  0%   { opacity: 0; transform: translate(0px, 0px) scale(0.6); }
  20%  { opacity: 1; transform: translate(-4px, -6px) scale(1); }
  100% { opacity: 0; transform: translate(-20px, -26px) scale(0.95) rotate(-12deg); }
}
:root[data-motion="full"] .canary--alive .canary-note--1 { animation: dp-canary-sing 2.6s ease-out infinite; }
:root[data-motion="full"] .canary--alive .canary-note--2 { animation: dp-canary-sing 2.6s ease-out 0.85s infinite; }
:root[data-motion="full"] .canary--alive .canary-note--3 { animation: dp-canary-sing 2.6s ease-out 1.7s infinite; }

@media (prefers-reduced-motion: reduce) {
  .canary-note { animation: none !important; }
}
:root[data-motion="reduced"] .canary-note { animation: none; }


/* ===========================================================================
   Face the canary to the RIGHT: one horizontal mirror about the viewBox centre,
   applied to the cage (which holds the bird, dead-bird, skeleton, feathers and
   perch), the song notes and the sleep marks, so the bird looks, flies, sings and
   sleeps facing right. The status marker and background plate are NOT mirrored.
   ======================================================================== */
.canary-cage,
.canary-chirp,
.canary-sleep {
  transform: scaleX(-1);
  transform-origin: 120px 120px;
}


/* ===========================================================================
   Canary CONNECTED TRANSITIONS. The screen keeps ONE figure mounted and only
   re-sets its state class, so the bird can animate BETWEEN poses instead of a
   hard cut. Take-off and landing glide the bird up/down while the cage fades
   out/in; death drops the bird and cross-fades it into the skeleton. The dead
   cage is aligned with the alive cage (no tilt). Motion gated as usual: the
   transitions run only under data-motion=full; reduced motion stays instant.
   These rules sit last so they win the cascade over the earlier display swaps.
   ======================================================================== */

/* Align the dead cage with the alive cage (drop the wonky tilt). */
.canary--dead .canary-cage__body { transform: none; }

/* The bird is always mounted (so it can fall + cross-fade on death); OPACITY, not display, decides
   whether it shows. On death it drops and tumbles toward the floor as it fades into the skeleton. */
.canary-bird { display: block; opacity: 0; transform-origin: 120px 130px; }
.canary--alive .canary-bird,
.canary--ailing .canary-bird,
.canary--pending .canary-bird,
.canary--flying .canary-bird { opacity: 1; }
.canary--dead .canary-bird { opacity: 0; transform: translate(0px, 40px) rotate(150deg); }

/* The skeleton cross-fades in rather than hard-swapping. */
.canary-skeleton { display: block; opacity: 0; }
.canary--dead .canary-skeleton { opacity: 1; }

/* The cage wire (and perch and legs) FADE when the bird leaves, instead of vanishing. */
.canary--flying .canary-cage__hook,
.canary--flying .canary-cage__dome,
.canary--flying .canary-cage__dome-rib,
.canary--flying .canary-cage__sheen,
.canary--flying .canary-cage__hoop,
.canary--flying .canary-cage__bar,
.canary--flying .canary-cage__base,
.canary--flying .canary-cage__base-line,
.canary--flying .canary-perch,
.canary--flying .canary-bird__legs { display: inline; opacity: 0; }

/* Song notes and sleep marks fade with the pose. */
.canary-chirp, .canary-sleep { opacity: 0; }
.canary--alive .canary-chirp { opacity: 1; }
.canary--pending .canary-sleep { opacity: 1; }

/* Move the bob OFF .canary-bird (which now transitions its position) onto the inner breath layer, so
   the flight bob composes with, instead of blocking, the take-off/landing glide. */
:root[data-motion="full"] .canary--alive .canary-bird,
:root[data-motion="full"] .canary--flying .canary-bird { animation: none; }
@keyframes dp-canary-flybob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-9px); } }
:root[data-motion="full"] .canary--flying .canary-bird__breath { animation: dp-canary-flybob 0.9s ease-in-out infinite; }

/* The skeleton settle must NOT touch opacity (the cross-fade owns it); keep only the tiny vertical drift. */
@keyframes dp-canary-skeleton-settle { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(0.7px); } }

/* THE TRANSITIONS (full motion only; instant otherwise). */
:root[data-motion="full"] .canary-bird { transition: transform 0.6s cubic-bezier(0.34, 0.08, 0.2, 1), opacity 0.4s ease; }
:root[data-motion="full"] .canary-skeleton { transition: opacity 0.55s ease 0.2s; }
:root[data-motion="full"] .canary-cage__hook,
:root[data-motion="full"] .canary-cage__dome,
:root[data-motion="full"] .canary-cage__dome-rib,
:root[data-motion="full"] .canary-cage__sheen,
:root[data-motion="full"] .canary-cage__hoop,
:root[data-motion="full"] .canary-cage__bar,
:root[data-motion="full"] .canary-cage__base,
:root[data-motion="full"] .canary-cage__base-line,
:root[data-motion="full"] .canary-perch,
:root[data-motion="full"] .canary-bird__legs,
:root[data-motion="full"] .canary-chirp,
:root[data-motion="full"] .canary-sleep { transition: opacity 0.45s ease; }


/* ===========================================================================
   Canary STORM FLOURISH. Under the Storm rain backdrop (Settings > Rain), the
   singing bird raises a little umbrella and pulls on gumboots. Shown ONLY in the alive pose AND only
   when the figure carries data-rain="storm" - the canary screen reflects the
   rain-pref onto the svg as an attribute (not a class), so the pose class can
   never clobber it. The group rides inside .canary-bird, so it sways with the
   cage and fades with the bird; nothing here animates on its own (calm budget),
   and the teal/white is illustration colour that carries NO status meaning -
   status stays on the marker shape + glyph + label, exactly as before. These
   rules sit last so the display reveal wins the cascade over .canary-bird's
   default hide. Reduced motion needs no special-case: the rain gear is static.
   ======================================================================== */
.canary-umbrella,
.canary-gumboots { display: none; }
.canary--alive[data-rain="storm"] .canary-umbrella,
.canary--alive[data-rain="storm"] .canary-gumboots { display: block; }

.canary-umbrella__canopy {
  fill: var(--canary-umbrella, #3fb7ae);
  stroke: var(--canary-umbrella-edge, #2a8f86);
  stroke-width: 1px;
  stroke-linejoin: round;
}
.canary-umbrella__panel { fill: var(--canary-umbrella-shade, #2e9890); stroke: none; }
.canary-umbrella__seam {
  fill: none;
  stroke: #fff;
  stroke-width: 1.1px;
  stroke-linecap: round;
  opacity: 0.5;
}
.canary-umbrella__pole,
.canary-umbrella__handle {
  fill: none;
  stroke: var(--cage-ink);
  stroke-width: 2.4px;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.canary-umbrella__ferrule {
  fill: none;
  stroke: var(--cage-ink);
  stroke-width: 2px;
  stroke-linecap: round;
}

/* Gumboots: a pair of little teal rubber boots on the bird's feet, the matching half of the
   rain-gear set. Same illustration teal as the umbrella, with a darker rim and a soft sheen.
   The boots REPLACE the bare feet, so the thin stick legs are hidden while they are on (the body
   sits low enough that the boots tuck straight under it); the feet return when Storm is off. */
.canary--alive[data-rain="storm"] .canary-bird__legs { display: none; }
.canary-gumboot {
  fill: var(--canary-umbrella, #3fb7ae);
  stroke: var(--canary-umbrella-edge, #2a8f86);
  stroke-width: 1.2px;
  stroke-linejoin: round;
}
.canary-gumboot__shine {
  fill: none;
  stroke: #fff;
  stroke-width: 1.4px;
  stroke-linecap: round;
  opacity: 0.45;
}

/* ============================================================================
   Onboarding carousel (screen 10). The first-run wizard renders as a rotating
   card deck rather than a side-railed single step: bite-size cards grouped into
   five chapters, with a 3D deal-off-left transition, a chapter progress rail,
   and forward movement GATED on the action steps. Bound to the tokens above;
   reuses .ob-page / .ob-top / .btn / .card / .ob-spinner from elsewhere in this
   sheet. Class prefix cx- keeps it self-contained.
   ============================================================================ */

/* chapter progress rail */
.cx-rail { padding: var(--space-5) clamp(var(--space-4), 4vw, var(--space-8)) var(--space-3); max-width: 920px; width: 100%; margin: 0 auto; }
.cx-chapters { list-style: none; margin: 0 0 var(--space-3); padding: 0; display: flex; gap: var(--space-2); }
.cx-chapter { flex: 1 1 0; display: flex; align-items: center; gap: 7px; background: none; border: 0; cursor: pointer; color: var(--text-muted); font: inherit; font-size: var(--text-xs); letter-spacing: 0.01em; padding: 0; text-align: left; transition: color var(--dur) var(--ease-out); }
.cx-chapter:disabled { cursor: default; }
.cx-chapter__dot { flex: none; width: 16px; height: 16px; border-radius: var(--radius-full); display: grid; place-items: center; border: 1.5px solid var(--border-strong); color: transparent; transition: all var(--dur) var(--ease-out); }
.cx-chapter__dot svg { width: 10px; height: 10px; }
.cx-chapter--done .cx-chapter__dot { background: var(--accent); border-color: var(--accent); color: var(--accent-fg); }
.cx-chapter--on .cx-chapter__dot { border-color: var(--accent); color: var(--accent); box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 16%, transparent); }
.cx-chapter--on { color: var(--text); font-weight: var(--weight-semibold); }
.cx-chapter__label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cx-track { height: 3px; border-radius: var(--radius-full); background: var(--border-subtle); overflow: hidden; }
.cx-fill { height: 100%; width: 0%; border-radius: inherit; background: linear-gradient(90deg, var(--accent), color-mix(in srgb, var(--accent) 70%, var(--trust))); transition: width 0.55s var(--ease-out); }

/* the deck stage. The carousel owns the exact viewport height: the header, rail and footer
   hint stay fixed while a tall card scrolls INSIDE itself, so nothing is ever lost below the
   fold and the page resizes cleanly (owner: "hint lost at the bottom / make it responsive"). */
.ob-page:has(.cx-stage-wrap) { height: 100vh; height: 100dvh; min-height: 0; overflow: hidden; }
.cx-stage-wrap { flex: 1 1 auto; min-height: 0; overflow: hidden; display: flex; align-items: center; justify-content: center; position: relative; padding: var(--space-5) clamp(var(--space-4), 5vw, var(--space-10)) var(--space-6); }
.cx-stage { position: relative; width: min(620px, 100%); height: 100%; display: grid; place-items: start center; perspective: 1700px; }

.cx-card {
  grid-area: 1 / 1; width: 100%;
  min-height: min(420px, 100%); max-height: 100%; overflow-y: auto;
  display: flex; flex-direction: column;
  padding: clamp(var(--space-6), 3.4vw, var(--space-8));
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-lg); box-shadow: var(--shadow-lg);
  padding-bottom: 0;   /* the action row owns the bottom padding so it can stick (see below) */
  transform-origin: center 64%; backface-visibility: hidden;
  transition: transform 0.62s var(--ease-out), opacity 0.46s var(--ease-out), filter 0.46s var(--ease-out);
  will-change: transform, opacity;
}
.cx-card[data-pos="0"]  { transform: none;                                              opacity: 1;    filter: none;        z-index: 40; pointer-events: auto; }
.cx-card[data-pos="1"]  { transform: translate3d(0, 22px, -70px)  scale(0.95);          opacity: 0.62; filter: blur(1px);   z-index: 30; pointer-events: none; }
.cx-card[data-pos="2"]  { transform: translate3d(0, 42px, -140px) scale(0.90);          opacity: 0.26; filter: blur(2.5px); z-index: 20; pointer-events: none; }
.cx-card[data-pos="3"]  { transform: translate3d(0, 40px, -180px) scale(0.88);          opacity: 0;    filter: blur(4px);   z-index: 10; pointer-events: none; }
.cx-card[data-pos="-1"] { transform: translate3d(-82%, -3%, 50px) rotateY(17deg) scale(0.97); opacity: 0; z-index: 5; pointer-events: none; }
.cx-card[data-pos="hidden"] { transform: translate3d(0, 44px, -200px) scale(0.86); opacity: 0; pointer-events: none; }

.cx-card__in { display: flex; flex-direction: column; flex: 1 1 auto; min-width: 0; }
.cx-card__in > * { opacity: 0; }
.cx-card[data-pos="0"] .cx-card__in > * { animation: cxRise 0.5s var(--ease-out) both; }
.cx-card[data-pos="0"] .cx-card__in > *:nth-child(1) { animation-delay: 0.04s; }
.cx-card[data-pos="0"] .cx-card__in > *:nth-child(2) { animation-delay: 0.10s; }
.cx-card[data-pos="0"] .cx-card__in > *:nth-child(3) { animation-delay: 0.16s; }
.cx-card[data-pos="0"] .cx-card__in > *:nth-child(4) { animation-delay: 0.22s; }
.cx-card[data-pos="0"] .cx-card__in > *:nth-child(5) { animation-delay: 0.28s; }
@keyframes cxRise { from { opacity: 0; transform: translateY(11px); } to { opacity: 1; transform: none; } }

/* card content: one uniform type scale */
.cx-eyebrow { font-family: var(--font-mono); font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.16em; color: var(--text-muted); margin: 0 0 var(--space-4); display: flex; align-items: center; gap: var(--space-2); }
.cx-eyebrow svg { color: var(--ok); }
.cx-title { margin: 0; font-size: var(--text-3xl); line-height: var(--leading-tight); letter-spacing: -0.022em; font-weight: var(--weight-semibold); color: var(--text); }
.cx-lede { margin: var(--space-4) 0 0; max-width: 46ch; font-size: var(--text-lg); line-height: var(--leading-base); color: var(--text-muted); }
.cx-note { margin: var(--space-4) 0 0; font-size: var(--text-sm); line-height: var(--leading-base); color: var(--text-muted); max-width: 50ch; }
.cx-body { margin-top: var(--space-5); }
/* The action row is a sticky footer INSIDE the (scrollable) card, so the primary action stays
   visible even when a tall card scrolls on a short viewport (owner: responsiveness). On a short
   card it simply sits at the bottom as before. The surface background + hairline keep scrolling
   content legible behind it. */
.cx-card__actions { margin-top: auto; position: sticky; bottom: 0; z-index: 1; background: var(--surface); border-top: 1px solid var(--border-subtle); padding: var(--space-4) 0 clamp(var(--space-6), 3.4vw, var(--space-8)); display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.cx-spacer { flex: 1 1 auto; }
.cx-back { display: inline-flex; align-items: center; gap: 6px; background: none; border: 0; cursor: pointer; padding: 8px 6px; color: var(--text-muted); font: inherit; font-size: var(--text-sm); border-radius: var(--radius); }
.cx-back:hover { color: var(--text); }

.cx-card--hero .cx-card__in { align-items: center; text-align: center; justify-content: center; }
.cx-card--hero .cx-eyebrow { justify-content: center; }
.cx-card--hero .cx-lede { margin-inline: auto; }
.cx-card--hero .cx-card__actions { justify-content: center; margin-top: var(--space-7); border-top: none; background: transparent; }
.cx-hero-mark { width: 64px; height: 64px; border-radius: var(--radius-full); display: grid; place-items: center; margin: 0 auto var(--space-5); color: var(--accent); background: color-mix(in srgb, var(--accent) 14%, transparent); border: 1px solid color-mix(in srgb, var(--accent) 32%, transparent); }
.cx-card--ok .cx-hero-mark { color: var(--ok); background: color-mix(in srgb, var(--ok) 15%, transparent); border-color: color-mix(in srgb, var(--ok) 34%, transparent); }

/* callout */
.cx-callout { display: flex; gap: var(--space-3); align-items: flex-start; padding: var(--space-4); border-radius: var(--radius); border: 1px solid var(--border); background: var(--bg-subtle); }
.cx-callout--trust { border-color: color-mix(in srgb, var(--trust) 38%, var(--border)); background: var(--trust-bg); }
.cx-callout--trust .cx-callout__icon { color: var(--trust); }
.cx-callout__icon { flex: none; }
.cx-callout__icon svg { display: block; }
.cx-callout strong { display: block; color: var(--text); font-weight: var(--weight-semibold); font-size: var(--text-md); margin-bottom: 2px; }
.cx-callout__b { display: block; font-size: var(--text-sm); line-height: var(--leading-base); color: var(--text-muted); }
.cx-callout--trust .cx-callout__b { color: var(--trust-fg); }

/* The onboarding key-posture fork: two descriptive panels the operator chooses between, with a
   reveal-on-select acceptance below. Two full panels (not the compact logo-tile grid) because this is
   a weighty either-or; they stack on a narrow screen. */
.ob-posture-fork { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-3); margin-top: var(--space-2); }
@media (max-width: 640px) { .ob-posture-fork { grid-template-columns: 1fr; } }
.ob-posture-card { display: flex; flex-direction: column; gap: var(--space-3); padding: var(--space-4); border: 1px solid var(--border); border-radius: var(--radius-lg); background: var(--surface); }
.ob-posture-card[data-selected="true"] { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.ob-posture-card__title { font-weight: var(--weight-semibold); font-size: var(--text-md); color: var(--text); }
.ob-posture-card__summary { font-size: var(--text-sm); color: var(--text-muted); line-height: var(--leading-base); }
.ob-posture-card__points { margin: 0; padding-left: var(--space-4); display: flex; flex-direction: column; gap: var(--space-1); font-size: var(--text-sm); color: var(--text-muted); line-height: var(--leading-base); }
.ob-posture-card__spacer { flex: 1 1 auto; }
.ob-posture-accept { margin-top: var(--space-4); padding: var(--space-4); border: 1px solid var(--border); border-radius: var(--radius-lg); background: var(--bg-subtle); }
.ob-posture-accept__statement { font-size: var(--text-sm); color: var(--text); line-height: var(--leading-base); margin-bottom: var(--space-3); }
.ob-posture-accept__check { display: flex; gap: var(--space-2); align-items: flex-start; font-size: var(--text-sm); color: var(--text); }

/* prerequisite checklist (manual validation). Compact rows (so more items fit) with a
   pop-style how-to per row; the info-tip sits OUTSIDE the label so opening it never ticks. */
.cx-reqs { list-style: none; margin: 0; padding: 0; border: 1px solid var(--border); border-radius: var(--radius); }
.cx-req { display: flex; align-items: center; }
.cx-req + .cx-req { border-top: 1px solid var(--border-subtle); }
.cx-req__lab { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-4); cursor: pointer; }
.cx-req__lab input { position: absolute; opacity: 0; width: 0; height: 0; }
.cx-req__box { flex: none; width: 20px; height: 20px; border-radius: var(--radius-sm); border: 1.5px solid var(--border-strong); display: grid; place-items: center; color: transparent; transition: all var(--dur) var(--ease-out); }
.cx-req__box svg { width: 12px; height: 12px; }
.cx-req__lab input:checked + .cx-req__box { background: var(--accent); border-color: var(--accent); color: var(--accent-fg); }
.cx-req__lab input:focus-visible + .cx-req__box { box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 30%, transparent); }
.cx-req__txt { min-width: 0; }
.cx-req__t { display: block; font-size: var(--text-sm); font-weight: var(--weight-semibold); color: var(--text); }
.cx-req__s { display: block; font-size: var(--text-xs); color: var(--text-muted); margin-top: 1px; line-height: var(--leading-snug); }
.cx-req > .info-tip__wrap { flex: none; margin-right: var(--space-4); }

/* compact status + nudge */
.cx-status { display: inline-flex; align-items: center; gap: var(--space-2); font-size: var(--text-sm); color: var(--text-muted); }
.cx-status--ok { color: var(--ok-fg); }
.cx-status svg { flex: none; }

/* side chevrons */
.cx-arrow { position: absolute; top: 50%; transform: translateY(-50%); width: 44px; height: 44px; border-radius: var(--radius-full); display: grid; place-items: center; z-index: 60; border: 1px solid var(--border); background: var(--surface); color: var(--text); cursor: pointer; box-shadow: var(--shadow-sm); transition: opacity var(--dur) var(--ease-out), background var(--dur); }
.cx-arrow:hover:not(:disabled) { background: var(--bg-subtle); }
.cx-arrow:disabled { opacity: 0.28; pointer-events: none; }
.cx-arrow--prev { left: clamp(8px, 3vw, 40px); }
.cx-arrow--next { right: clamp(8px, 3vw, 40px); }
.cx-arrow.cx-shake { animation: cxShake 0.4s var(--ease-out); }
@keyframes cxShake { 0%,100%{transform:translateY(-50%)} 25%{transform:translateY(-50%) translateX(-4px)} 50%{transform:translateY(-50%) translateX(4px)} 75%{transform:translateY(-50%) translateX(-2px)} }

.cx-hint { flex: none; text-align: center; color: var(--text-muted); font-size: var(--text-xs); padding: 0 var(--space-4) var(--space-4); }
.cx-hint kbd { font-family: var(--font-mono); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 1px 5px; background: var(--surface); }

@media (prefers-reduced-motion: reduce) {
  .cx-card { transition: opacity 0.2s linear; }
  .cx-card[data-pos] { transform: none !important; filter: none !important; }
  .cx-card:not([data-pos="0"]) { opacity: 0 !important; }
  .cx-card[data-pos="0"] .cx-card__in > * { animation: none; opacity: 1; }
  .cx-fill { transition: none; }
  .cx-arrow.cx-shake { animation: none; }
}

@media (max-width: 560px) {
  .cx-chapter__label { display: none; }
  .cx-arrow { display: none; }
  .cx-card__actions { flex-direction: column-reverse; align-items: stretch; gap: var(--space-2); }
  .cx-spacer { display: none; }
  .cx-card__actions .btn { width: 100%; justify-content: center; }
  .cx-back { align-self: center; margin-top: var(--space-1); }
}

/* =============================================================================
   20. Mobile responsive hardening (2026-06-28). Consolidated overflow guards and
   small-viewport refinements from a full device-width audit (320/375/768px) of
   every console screen and the public tour. Appended last so these overrides win
   at equal specificity. Class-based only (the strict CSP forbids inline styles +
   <style>); the tour overlay's own responsive tweaks live in its CSSOM
   (src/lib/demo/tour/*). Root causes, not per-screen patches: long tokens wrap,
   the layout chain can shrink, tables + code + the topology map scroll inside
   their frame instead of widening the page, and the context bar reflows.
   ========================================================================== */

/* ---- Global overflow guards ------------------------------------------------ */
/* Long unbroken tokens (hashes, emails, IDs, URLs, dp-names) must wrap rather
   than push a container past the viewport. Prose, headings and hints opt in. */
h1, h2, h3, h4,
p, li, dd, dt, figcaption,
.field__hint, .banner__msg, .page-header__desc, .page-header__title,
.card__title, .stat-label, .verdict__title, .verdict__body {
  overflow-wrap: break-word;
}
/* Flex/grid children default to min-width:auto (their content width), letting a
   wide child (a table, a long line) blow the row out. Zero the floor along the
   layout chain so children shrink and the inner scroll/ellipsis can engage. */
.main, .main__inner,
.card, .stack, .stack-sm, .data-table, .dp-table-wrap,
.verdict, .verdict__headtext, .banner, .banner__msg, .code-block,
.ov-grid > *, .stat-grid > *, .target-grid > * {
  min-width: 0;
}

/* ---- Tables: scroll inside the frame, never widen the page ----------------- */
/* .dp-table-wrap already has overflow-x:auto; cap its width so the auto actually
   clips + scrolls rather than stretching the parent. -webkit-overflow-scrolling
   gives momentum scrolling on iOS. */
.dp-table-wrap { max-width: 100%; -webkit-overflow-scrolling: touch; }
/* The windowed-list path (.dt-windowed-wrap) had no horizontal scroller; add one
   on the OUTER wrap only (never on .dt-viewport, whose vertical windowing must
   not be turned into a clip). */
.dt-windowed-wrap { max-width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; }

/* ---- Code / pre blocks: scroll inside, never widen the page ---------------- */
.code, .code-block, .code-block__pre,
.key-field__value, .fp-row__value { max-width: 100%; }

/* ---- File pickers: shrink to the field rather than widening it ------------- */
/* A file input is a native control whose intrinsic width is its button plus "No file chosen", and nothing
   in this stylesheet had ever bounded it. Inside .field, which is a grid, that intrinsic width became the
   column track's floor, so the label, the input and the hint ALL painted outside the field together.
   Measured on console d9bc3c5e: 56px past .field at 375 and 390 on /keys/custody, /restore/attend,
   /restore/break-glass, /restore/break-glass/:runId, /restore/break-glass-prune and
   /restore/break-glass-prune/:downpipeId, and 2px past a plain wrapper on /keys/custody's own picker.
   min-width:0 drops the grid item's automatic minimum and lowers what the control contributes to the track;
   max-width:100% holds it inside a narrower box. Nothing is hidden: the Choose file button keeps its full
   size and every one of these pickers carries a visible label and hint above it, so the only thing the
   browser shortens is the control's own selected-file text, which it truncates natively. */
input[type="file"] { min-width: 0; max-width: 100%; }

/* ---- Topology map: a gentler floor on phones so it is not mostly off-frame -- */
@media (max-width: 767px) {
  .topo-svg { min-width: 380px; }
}

/* ---- Shell: context bar + account chip ------------------------------------- */
/* The account chip (access-verdict chip + email + role badge + Sign out, plus the
   tour's "Sample identity" badge) is the single widest thing in the bar (~566px).
   Its layout now lives here (moved off an inline style in chrome.ts) so the query
   below can wrap it; the email ellipsises instead of forcing the bar wider. */
.account-chip { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; min-width: 0; }
.account-chip__id { min-width: 0; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The account slot opts out of the flex AUTOMATIC MINIMUM SIZE at EVERY width, not only below 1023.
   A flex item's default `min-width: auto` floors it at its own min-content, and the slot's min-content is
   the whole account chip: verdict chip + email + role badge + Sign out. The chip's shrink chain was already
   written (.trust-chip min-width:0, .trust-chip__label ellipsis, .trust-chip__email max-width min(22vw,280px)),
   but with the slot itself unshrinkable the bar could never reach it, so the chip simply hung off the bar's
   right edge and the whole document scrolled sideways: measured 58px live on the deployed probe console at a
   1280px viewport with the Owner identity `Passkey verified: raingauge-owner-probe@example.invalid`
   (scrollWidth 1338 against clientWidth 1280), identical on Overview, /downpipes, /runs and /security,
   because it is shell chrome and not screen content. This is the same automatic-minimum-size
   defect the .shell grid fixed with minmax(0, 1fr); the slot is the one remaining item that never opted out.
   max-width:100% keeps a block-level slot from being widened past its track by anything inside it. */
.account-slot { min-width: 0; max-width: 100%; }

/* Medium + compact, INCLUDING the 768–1023 dead zone (rail still present, but the
   §7 bar wrap only kicked in ≤767): let the whole bar wrap and every item shrink
   so nothing overflows down to 320px. */
@media (max-width: 1023px) {
  .context-bar {
    flex-wrap: wrap;
    height: auto;
    min-height: var(--header-h);
    row-gap: var(--space-2);
    padding-top: var(--space-2);
    padding-bottom: var(--space-2);
  }
  .account-chip { max-width: 100%; }
  .account-chip__id { max-width: min(46vw, 240px); }
  .engine-chip { min-width: 0; max-width: 100%; }
  .view-toggle { flex: none; }
}

/* ---- 20b. Mobile audit follow-ups (2026-06-28, per-area visual audit) ------ */

/* Touch-target floor on coarse pointers: lift the dense secondary controls (facet/density chips,
   selection box, segmented toggles, palette rows) to the 44px coarse-pointer target. height:auto first
   where a fixed height would cap min-height. */
@media (pointer: coarse) {
  .dt-facet, .dt-density { height: auto; min-height: var(--control-h-lg); }
  .dt-density__btn { min-height: var(--control-h-lg); }
  .dt-check { width: 22px; height: 22px; }
  .theme-option, .type-seg__btn, .cost-segmented__btn { min-height: var(--control-h-lg); }
  .cmdp__footer { display: none; }   /* the keyboard-hint footer is dead weight on touch */
}
/* Single-line controls + secondary affordances reach 44px on a phone (the multiline .textarea is already
   tall and unaffected; .input/.select/.cmdp__row/back-crumb were below the floor). */
@media (max-width: 767px) {
  .input, .select, .cmdp__row { min-height: var(--control-h-lg); }
  .btn--icon.btn--sm { min-width: var(--control-h-lg); }
  .page-header__back { min-height: var(--control-h-lg); display: inline-flex; align-items: center; }
  .cost-segmented { display: flex; width: 100%; }
  .cost-segmented__btn { flex: 1 1 0; }
}
/* The 768–1023 docked dead zone is touch too (a tablet): apply the same floor there. */
@media (max-width: 1023px) and (pointer: coarse) {
  .nav-item, .btn, .engine-chip, .palette-trigger, .update-chip, .cmdp__row { min-height: var(--control-h-lg); }
}

/* Account chip: tighten the wrapped rhythm on a phone; on the smallest phones drop the mono email entirely
   (the Access-verified chip + role + Sign out still identify the session, and the email rides in the title). */
@media (max-width: 767px) {
  .account-chip { row-gap: var(--space-1); }
  .account-chip__id { max-width: min(62vw, 220px); }
}
@media (max-width: 380px) {
  .account-chip .account-chip__id { display: none; }
}

/* Slide-over rail: widen so the longest nav labels fit (keeps a tappable scrim margin even at 320). */
@media (max-width: 767px) {
  .rail { width: min(88vw, 300px); }
}

/* Overview fleet filter: in column-mode the 260px flex-basis became a 260px-TALL void around the 36px input.
   flex:0 0 auto makes the wrap only as tall as the input. */
@media (max-width: 767px) {
  .dt-filter-wrap { flex: 0 0 auto; }
}

/* /map live-flow canvas: its source labels hang left of their dots and clip to garbage at phone width (the
   canvas can't scroll-compensate like the SVG twin). Hide it ≤640 exactly as the SVG figure already is — the
   accessible flow table + the filterable downpipes table keep the map fully readable (WCAG 1.4.10 reflow). */
@media (max-width: 640px) {
  .live-flow__figure { display: none; }
  .live-flow__table { margin-top: 0; }
}

/* Canary hero: the 2-col [bird | status] grid is a CSP-inline style (no media query), so the 260px bird
   crushes the status column on a phone. Stack it ≤640 (the figure has margin-inline:auto, so it centres).
   !important beats the CSSOM-set inline grid; :has() is already a baseline selector here. */
@media (max-width: 640px) {
  .card:has(> .canary-figure) { grid-template-columns: 1fr !important; gap: var(--space-4) !important; }
}

/* Settings theme radiogroup: wrap instead of overflowing when text-size / large-targets is on. */
.theme-control { flex-wrap: wrap; }

/* Credentials: let the fingerprint hash take the full row, and let a wide status chip drop under a long card
   title, on the narrowest phones. */
@media (max-width: 480px) {
  .fp-row { flex-wrap: wrap; }
  .fp-row__label { flex-basis: 100%; }
  .card__header { flex-wrap: wrap; align-items: flex-start; }
}

/* Onboarding/carousel header (.ob-top): stop "Back to the console" wrapping mid-phrase, drop the now-meaningless
   lone custody lock, give the controls 44px targets, and (in tour mode only) clear the fixed DEMO corner pill.
   Carousel dots centre in their slice once the labels hide. */
@media (max-width: 560px) {
  .ob-top { flex-wrap: wrap; gap: var(--space-2) var(--space-3); padding: var(--space-3) var(--space-4); }
  .ob-top__custody { display: none; }
  .ob-top .linklike { white-space: nowrap; min-height: var(--control-h-lg); display: inline-flex; align-items: center; }
  .ob-theme { margin-left: auto; }
  .ob-theme__btn { min-height: var(--control-h-lg); }
  body:has(#tour-demo-corner) .ob-top { padding-right: calc(var(--space-3) + 76px); }
  .cx-chapter { justify-content: center; }
}

/* Public tour/demo only: installTourBanner (src/lib/demo/banner.ts) tags <body> dp-demo-banner while the
   floating "Demo. Sample data. Resets on reload." pill is mounted; this clearance keeps the pill from sitting
   over the last line of page content at rest. Never set on the genuine console. */
body.dp-demo-banner .main { padding-bottom: 72px; }

/* ---- 20c. Wide data tables → card-stack on mobile/tablet (≤1023) ----------- */
/* Horizontal scroll keeps a 7–8 column ops table (Downpipes, Runs, Audit, Credentials, …) from overflowing
   the page, but on a phone it is a 2-column peephole: headers clip and row identity is lost on scroll. Below
   1023px (covering the rail-docked tablet dead zone too) each row becomes a CARD: the identity cell is the
   card title, every other cell is a label/value line (the label from data-label, set in data-table-rows.ts),
   and the bulk-select box pins to the card corner. Scoped to the data-table component (.dt-wrap) and to the
   NON-windowed path — the 150-row windowed tables depend on table-layout:fixed + spacer maths and must keep
   the scroll-in-frame behaviour. Trade-off: column-sort lives in the now-hidden thead, so inline sort is
   unavailable in card mode (filter + facets remain). */
@media (max-width: 1023px) {
  .dt-wrap.dp-table-wrap { overflow-x: visible; border: 0; border-radius: 0; }
  .dt-wrap .dp-table:not(.dt-table--windowed) thead {
    position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
  }
  .dt-wrap .dp-table:not(.dt-table--windowed) tbody,
  .dt-wrap .dp-table:not(.dt-table--windowed) tr,
  .dt-wrap .dp-table:not(.dt-table--windowed) td { display: block; }
  .dt-wrap .dp-table:not(.dt-table--windowed) tbody tr {
    position: relative;
    border: 1px solid var(--border); border-radius: var(--radius-lg);
    background: var(--surface);
    margin: 0 0 var(--space-3); padding: var(--space-3) var(--space-4);
  }
  .dt-wrap .dp-table:not(.dt-table--windowed) td {
    display: flex; gap: var(--space-4); justify-content: space-between; align-items: baseline;
    padding: var(--space-2) 0; border-bottom: 1px solid var(--border-subtle);
    text-align: right; min-width: 0;
  }
  .dt-wrap .dp-table:not(.dt-table--windowed) td:last-child { border-bottom: 0; padding-bottom: 0; }
  .dt-wrap .dp-table:not(.dt-table--windowed) td::before {
    content: attr(data-label); flex: none; text-align: left;
    color: var(--text-muted); font-size: var(--text-xs); font-weight: var(--weight-semibold);
  }
  /* The identity cell (first non-checkbox column) is the card title: full width, left, no inline label. */
  .dt-wrap .dp-table:not(.dt-table--windowed) td:first-child:not(.dt-col-check),
  .dt-wrap .dp-table:not(.dt-table--windowed) td.dt-col-check + td {
    display: block; text-align: left; padding: 0 var(--space-7) var(--space-2) 0;
    font-weight: var(--weight-semibold);
  }
  .dt-wrap .dp-table:not(.dt-table--windowed) td:first-child:not(.dt-col-check)::before,
  .dt-wrap .dp-table:not(.dt-table--windowed) td.dt-col-check + td::before { content: none; }
  /* A Runs Status cell (badge + inline error) stacks full width. */
  .dt-wrap .dp-table:not(.dt-table--windowed) td[data-label="Status"] {
    flex-direction: column; align-items: flex-start; text-align: left;
  }
  /* Bulk-select box to the card corner. */
  .dt-wrap .dp-table:not(.dt-table--windowed) td.dt-col-check {
    position: absolute; top: var(--space-3); right: var(--space-3); width: auto; padding: 0; border: 0;
  }
  .dt-wrap .dp-table:not(.dt-table--windowed) td.dt-col-check::before { content: none; }
}
/* 20c-bis. Grid/flex items default to min-width:auto, so a child that carries wide content (a static
   permission-matrix table, a disclosure summary, a form field) grows to its min-content and pushes the page
   wide at ~320px instead of shrinking and letting the content wrap or scroll-in-frame. Let the common
   structural wrappers shrink below their content on phone/tablet; each already wraps or scrolls internally. */
@media (max-width: 1023px) {
  .async-region, .disclosure, .card, .field, .stack, .dp-table-wrap { min-width: 0; }
  /* A long unbreakable string (an email, id or token) in a card-stacked cell, a field label/hint, or a
     disclosure summary sets a wide min-content that pushes the page past a 320px phone even once the wrappers
     may shrink. overflow-wrap:anywhere (not break-word) breaks it AND lowers min-content so the flex/grid item
     can shrink to the viewport. */
  .dp-table td, .field__label, .field__hint, .disclosure > summary, .card__title { overflow-wrap: anywhere; }
}
/* A failed run's full error in the Status cell otherwise balloons the card; clamp to 2 lines (full text is in
   the run drawer). Pairs with the Status-cell stack above. */
@media (max-width: 640px) {
  .run-status-cell__err {
    display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  }
}

/* Canary proof + recent-flight rows are CSP-inline 3-col grids (no media query); stack them ≤480 so the
   detail column is not crushed to a sliver. !important beats the CSSOM-set inline grid. */
@media (max-width: 480px) {
  .canary-proof-row, .canary-flight-row {
    grid-template-columns: 1fr !important; gap: var(--space-1) !important; align-items: start !important;
  }
}

/* ---- 20d. Mobile residual edges (≤320 polish) ------------------------------ */
/* Card-mode value cells: let a long mono value (a hash, run id, target) shrink + break within its share
   instead of pushing the card past the viewport. */
@media (max-width: 1023px) {
  .dt-wrap .dp-table:not(.dt-table--windowed) td > * { min-width: 0; overflow-wrap: anywhere; }
}
/* Long mono strings + hints break aggressively on a phone rather than overflow by a few px (scroll
   containers keep white-space:nowrap, so this only acts where wrapping is already allowed). */
@media (max-width: 1023px) {
  .field__hint, .mono { overflow-wrap: anywhere; }
}
/* Tab strips (access roles/sessions, drawer tabs) scroll horizontally on a phone instead of edging the
   document a few px wider. */
@media (max-width: 767px) {
  .drawer-tablist { overflow-x: auto; -webkit-overflow-scrolling: touch; }
}

/* Destination 'Storage pricing' rate grid: 2-up normally, one column on the narrowest phones (was a
   CSP-inline grid that couldn't carry a media query). */
.dest-price-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-3); }
@media (max-width: 480px) { .dest-price-grid { grid-template-columns: 1fr; } }

/* Auth screens (full-bleed): a long SSO label ("Sign in with Microsoft Entra ID (OIDC)") is nowrap in a
   min-content grid column, so it edges the card a few px past a 320px phone. Let the button labels wrap. */
@media (max-width: 480px) {
  .ob-page .btn { white-space: normal; }
}

/* ============================================================================
   §21 Identity-provider grid (screens/idp-connections)
   The provider catalogue as a grid of logo tiles: one tile per provider, ordered
   by popularity. A "live" tile (an enabled connection) runs a teal/accent
   particle around its border via a registered angle property and lifts on a
   teal glow; a "configured" tile (a connection that is currently off) gets a
   static teal ring; an "available" tile is plain with an "Add" corner chip.
   Click an available tile to open its add wizard, a configured/live one to
   manage it. The orbit + chip pulse stop under reduced motion (a static ring
   remains so "live" still reads) and the ring degrades to a solid border under
   forced colours.
   ========================================================================== */

/* The animatable border angle for the orbiting particle. Registering it as an
   <angle> is what lets a keyframe interpolate the conic-gradient sweep. */
@property --dp-idp-angle {
  syntax: "<angle>";
  initial-value: 0deg;
  inherits: false;
}
@keyframes dp-idp-orbit { to { --dp-idp-angle: 360deg; } }
@keyframes dp-idp-pulse {
  0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--trust) 55%, transparent); }
  70%  { box-shadow: 0 0 0 5px color-mix(in srgb, var(--trust) 0%, transparent); }
  100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--trust) 0%, transparent); }
}

.idp-grid {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: var(--space-3);
}
@media (max-width: 1100px) { .idp-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
@media (max-width: 820px)  { .idp-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 560px)  { .idp-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-2); } }

.idp-tile {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 126px;
  padding: var(--space-4) var(--space-3) calc(var(--space-5) + 2px);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
  color: var(--text);
  cursor: pointer;
  text-align: center;
  transition: transform 120ms ease, box-shadow 120ms ease, border-color 120ms ease, background 120ms ease;
  -webkit-appearance: none;
  appearance: none;
}
.idp-tile > * { position: relative; z-index: 1; }
.idp-tile:hover {
  transform: translateY(-2px);
  border-color: var(--border-strong);
  box-shadow: var(--shadow);
}
.idp-tile:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
.idp-tile[data-selected="true"] {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent);
}

.idp-tile__logo { display: inline-flex; align-items: center; justify-content: center; width: 44px; height: 44px; }
.idp-tile__logo .idp-logo { width: 34px; height: 34px; display: block; }

.idp-tile__name {
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  line-height: 1.25;
  color: var(--text);
  max-width: 100%;
}

/* The bottom-corner status chip. */
.idp-tile__chip {
  position: absolute;
  right: var(--space-2);
  bottom: var(--space-2);
  z-index: 2;
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 2px 7px;
  border-radius: var(--radius-full);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  line-height: 1.4;
  border: 1px solid var(--border);
  background: var(--bg-subtle);
  color: var(--text-muted);
}
.idp-tile:hover .idp-tile__chip--add { color: var(--accent); border-color: var(--accent); }
.idp-tile__chip--active {
  color: var(--trust-fg);
  background: var(--trust-bg);
  border-color: var(--trust-border);
}
.idp-tile__chip-dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--trust);
  animation: dp-idp-pulse 2s ease-out infinite;
}

/* Live (an enabled connection): a glowing tile with an animated particle border. */
.idp-tile[data-state="live"] {
  border-color: color-mix(in srgb, var(--trust) 45%, var(--border));
  background:
    radial-gradient(120% 90% at 50% 0%, color-mix(in srgb, var(--trust) 9%, transparent), transparent 70%),
    var(--surface);
  box-shadow:
    0 0 0 1px color-mix(in srgb, var(--trust) 22%, transparent),
    0 10px 28px -12px color-mix(in srgb, var(--trust) 55%, transparent);
}
.idp-tile[data-state="live"]::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  border-radius: inherit;
  padding: 2px;
  background: conic-gradient(
    from var(--dp-idp-angle),
    transparent 0 62%,
    color-mix(in srgb, var(--trust) 70%, transparent) 74%,
    var(--trust) 84%,
    #eafff9 90%,
    var(--accent) 96%,
    transparent 100%
  );
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  animation: dp-idp-orbit 3.6s linear infinite;
  pointer-events: none;
}

/* Configured but every connection disabled: a static teal ring, no orbit (not live). */
.idp-tile[data-state="configured"] {
  border-color: color-mix(in srgb, var(--trust) 35%, var(--border));
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--trust) 14%, transparent);
}

/* The detail panel a tile opens. */
.idp-detail { scroll-margin-top: var(--space-6); }
.idp-detail:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
.idp-detail__head { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.idp-detail__title { display: inline-flex; align-items: center; gap: var(--space-2); }
.idp-detail__logo { display: inline-flex; }
.idp-detail__logo .idp-logo { width: 26px; height: 26px; display: block; }
.idp-detail__actions { display: flex; gap: var(--space-2); flex-wrap: wrap; }

.idp-trademark-note { font-size: var(--text-xs); color: var(--text-muted); line-height: 1.5; }

/* Reduced motion (OS query AND the in-app data-motion choice): stop the orbit + pulse, but keep a calm
   static ring so a "live" tile still reads as active. */
@media (prefers-reduced-motion: reduce) {
  .idp-tile[data-state="live"]::before { animation: none; background: conic-gradient(from 90deg, var(--trust), var(--accent), var(--trust)); opacity: 0.7; }
  .idp-tile__chip-dot { animation: none; }
  .idp-tile { transition: none; }
}
:root[data-motion="reduced"] .idp-tile[data-state="live"]::before {
  background: conic-gradient(from 90deg, var(--trust), var(--accent), var(--trust));
  opacity: 0.7;
}

/* Forced colours: the gradient ring is dropped; a solid thick border carries the state instead. */
@media (forced-colors: active) {
  .idp-tile[data-state="live"]::before { display: none; }
  .idp-tile[data-state="live"], .idp-tile[data-state="configured"] { border: 2px solid Highlight; }
  .idp-tile[data-selected="true"] { outline: 2px solid Highlight; }
}

/* ============================================================================
   §22 Cloudflare-coverage grid (screens/overview/surface-coverage)
   The Overview hero: the nine Cloudflare surfaces from the public adverts as a
   3x3 grid, each tile coloured by where that surface sits in the backup journey.
   covered (ok / green) = a downpipe backs it up; added (warn / amber) = added as
   a source but no downpipe yet (the real gap); unadded (neutral / slate) = not
   added, deliberately quiet not red (an unused surface is not a fault). The
   actionable tiles are buttons that navigate; unavailable/unknown are static.
   Colour is never the only signal: every tile has a status dot AND a text label.
   All colours are semantic tokens, so light and dark both clear the contrast gate.
   ============================================================================ */
.ov-coverage { margin-bottom: var(--space-6); }
.ov-coverage__head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--space-3) var(--space-5); flex-wrap: wrap; margin-bottom: var(--space-3);
}
.ov-coverage__title { font-size: var(--text-md); font-weight: var(--weight-semibold); margin: 0; }
.ov-coverage__summary {
  font-family: var(--font-mono); font-size: var(--text-sm); color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
.ov-coverage__summary b { font-weight: var(--weight-semibold); }
.ov-coverage__summary .s-ok { color: var(--ok-fg); }
.ov-coverage__summary .s-amber { color: var(--warn-fg); }
.ov-coverage__summary .s-un { color: var(--text-muted); }
.ov-coverage__summary .s-muted { color: var(--text-muted); }

.ov-coverage-grid {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-3);
}
@media (max-width: 700px) { .ov-coverage-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 460px) { .ov-coverage-grid { grid-template-columns: 1fr; } }

/* One surface tile (a button when actionable, a div when not). The whole card is the hit target. */
.surf {
  display: flex; flex-direction: column; gap: var(--space-2);
  min-height: 116px; padding: var(--space-4);
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg);
  color: var(--text); text-align: left; width: 100%;
  font: inherit; appearance: none;
  transition: transform 0.12s ease, box-shadow 0.14s ease, border-color 0.14s ease;
}
button.surf { cursor: pointer; }
div.surf { cursor: default; }
.surf__top { display: flex; align-items: center; gap: var(--space-2); }
.surf__icon { color: var(--text-muted); display: inline-flex; flex: none; }
/* A surface name is a binding name in monospace ("GATEWAY_SECRETS", "Configuration"): one unbroken token.
   .surf__top is a flex row, so the name's automatic minimum size is its min-content and the name runs past
   the tile edge once the coverage grid narrows the column (measured: "Configuration" 28px past the tile at
   768). Same pairing as .stat__secondary and .dp-name--cell: shrink the item, wrap the token. */
.surf__name { font-family: var(--font-mono); font-size: var(--text-md); letter-spacing: var(--tracking-tight); min-width: 0; overflow-wrap: anywhere; }
.surf__status {
  margin-top: auto; display: inline-flex; align-items: center; gap: var(--space-2);
  font-family: var(--font-mono); font-size: var(--text-sm); color: var(--text-muted);
}
.surf__dot { width: 9px; height: 9px; border-radius: var(--radius-full); flex: none; background: var(--neutral); }
.surf__cta {
  font-size: var(--text-xs); color: var(--text-muted); margin-top: var(--space-1);
  opacity: 0; transform: translateY(2px); transition: opacity 0.14s ease, transform 0.14s ease;
}
button.surf:hover { transform: translateY(-2px); }
button.surf:hover .surf__cta { opacity: 1; transform: none; }
.surf:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }

/* covered (green): a downpipe backs it up. */
.surf[data-state="covered"] {
  border-color: var(--ok-border);
  background: linear-gradient(180deg, color-mix(in srgb, var(--ok) 7%, transparent), transparent 58%), var(--surface);
}
.surf[data-state="covered"] .surf__status { color: var(--ok-fg); }
.surf[data-state="covered"] .surf__dot { background: var(--ok); }
button.surf[data-state="covered"]:hover { box-shadow: 0 0 0 1px color-mix(in srgb, var(--ok) 30%, transparent), 0 12px 26px -16px color-mix(in srgb, var(--ok) 55%, transparent); }

/* added (amber): added as a source but no downpipe yet (the actionable gap). */
.surf[data-state="added"] {
  border-color: var(--warn-border);
  background: linear-gradient(180deg, color-mix(in srgb, var(--warn) 6%, transparent), transparent 58%), var(--surface);
}
.surf[data-state="added"] .surf__status { color: var(--warn-fg); }
.surf[data-state="added"] .surf__dot { background: var(--warn); }
button.surf[data-state="added"]:hover { box-shadow: 0 0 0 1px color-mix(in srgb, var(--warn) 30%, transparent), 0 12px 26px -16px color-mix(in srgb, var(--warn) 50%, transparent); }

/* unadded (slate): not added. Quiet, not red. */
.surf[data-state="unadded"] .surf__status { color: var(--text-muted); }
.surf[data-state="unadded"] .surf__dot { background: var(--neutral); }
button.surf[data-state="unadded"]:hover { border-color: var(--border-strong); box-shadow: 0 12px 26px -16px rgba(0, 0, 0, 0.55); }

/* unavailable / unknown: nothing to do here, so a muted static tile (no hover lift, hollow dot). */
.surf[data-state="unavailable"], .surf[data-state="unknown"] { opacity: 0.72; }
.surf[data-state="unavailable"] .surf__dot, .surf[data-state="unknown"] .surf__dot {
  background: transparent; border: 1.5px solid var(--neutral);
}

@media (prefers-reduced-motion: reduce) {
  .surf, .surf__cta { transition: none; }
  button.surf:hover { transform: none; }
}
@media (forced-colors: active) {
  .surf[data-state="covered"], .surf[data-state="added"] { border: 2px solid Highlight; }
  .surf:focus-visible { outline: 2px solid Highlight; }
}

/* ============================================================================
   §23 Enterprise visual-quality pass (2026-07-02)
   Fixes from the console-wide visual review (CONSOLE-VISUAL-REVIEW-TRACKER-
   2026-07-02.md). Deliberate overrides live HERE, after every base section,
   each tagged with its finding, so the pass reads as one diff and the base
   sections above stay untouched.
   ============================================================================ */

/* Approval inboxes (B0-7): a long cue value (an approver email) wraps inside its
   grid cell instead of colliding with the neighbouring column. */
.approval-cue { min-width: 0; }
.approval-cue__value { overflow-wrap: anywhere; }
/* R10: value-appropriate wrapping, scoped so config-changes.ts / owner-actions.ts cue values are unaffected. */
.approval-cue__value.approval-cue__value--email { display: block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; overflow-wrap: normal; }
.approval-cue__value--mono { font-family: var(--font-mono); overflow-wrap: normal; word-break: normal; }
.approval-cue__value--mono.approval-cue__value--hash { overflow-wrap: anywhere; word-break: break-all; }

/* Executive Overview (B0-2): the plain-English answer grid + cards previously
   had NO rules (raw shrink-to-fit buttons). The card skin comes from .card;
   these add the grid, the button reset and the equal-height column so the CTA
   baseline aligns across cards. */
.ov-exec-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  gap: var(--space-4); margin-top: var(--space-5);
}
.ov-exec-card {
  display: flex; flex-direction: column; width: 100%;
  text-align: left; font: inherit; appearance: none; cursor: pointer;
}
.ov-exec-card__cta { margin-top: auto; }

/* Header view toggle (B0-2): a real segmented control; the active segment reads
   by fill, never only by aria-pressed. */
.view-toggle {
  display: inline-flex; flex: none; align-items: center; gap: 2px;
  padding: 2px; border: 1px solid var(--border); border-radius: var(--radius-full);
  background: var(--bg-subtle);
}
.view-toggle__btn { border: none; border-radius: var(--radius-full); }
.view-toggle__btn.is-active { background: var(--surface-raised); color: var(--text); }

/* Top bar: the account cluster stays on one line at desktop; the ≤1023 media
   block above re-enables wrapping for the compact bar. */
.account-chip { flex-wrap: nowrap; }
@media (max-width: 1023px) { .account-chip { flex-wrap: wrap; } }

/* Tour/demo on a phone: the fixed top-right corner markers (the DEMO honesty pill + the downpipes.io
   link) would otherwise sit on top of the wrapped context-bar chips. On a narrow screen we drop the
   redundant site link (the welcome card and the funnel close both carry it) and reserve the corner the
   DEMO pill occupies so the account identity is never painted over. Scoped to the demo body class so the
   real console is untouched. */
@media (max-width: 560px) {
  #tour-site-link { display: none !important; }
  body.dp-demo-banner .context-bar { padding-right: 92px; }
}

/* Data-table Compact density (B0-3): the toggle wrote data-density but no rule
   consumed it, so Compact changed nothing. 37px pitch — keep in step with
   rowHeightCompact in components/data-table.ts. */
.data-table[data-density="compact"] .dp-table th,
.data-table[data-density="compact"] .dp-table td { padding: var(--space-2) var(--space-4); }
.data-table[data-density="compact"] .dt-sort { padding: var(--space-2) var(--space-4); }

/* Overview tile band: five tiles fit one row at the wide measure (the fifth
   previously sat orphaned beside a void). */
.stat-grid { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); }

/* Coverage grid density: each tile reserves a fixed CTA line (the hover reveal
   cannot shift layout) and drops the dead vertical space. */
.surf { min-height: 92px; padding: var(--space-3) var(--space-4); }
.surf__cta { min-height: 14px; }

/* Licence summary tiles: the three stats sit 3-up inside the measure instead of
   a 2+1 orphan row. */
.lic-tiles .stat-grid { grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); }

/* Rail at short viewports: the last items scroll clear of the pinned footer and
   a soft bottom fade reads as "more below" instead of a half-severed row. */
.rail__nav {
  padding-bottom: var(--space-5);
  scroll-padding-block: var(--space-2);
  mask-image: linear-gradient(180deg, #000 calc(100% - 24px), transparent);
}

/* Disclosure summary caret: the marker sits inside the card frame, not on the
   hairline. */
.disclosure > summary { list-style-position: inside; }

/* Tables: headers never wrap ("Last run" / "Next run"), and a .nowrap utility
   for one-line mono ids (run ids, scope ids). */
.dp-table thead th, .dt-sort { white-space: nowrap; }
.nowrap { white-space: nowrap; }

/* Runs list: the failed row's inline diagnosis reads muted at two lines; the
   red label + square marker carry the severity and the drawer keeps the full
   text. */
.run-status-cell__err {
  color: var(--text-muted);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Wide dialog: the report JSON view reads at a working width. */
.dialog--wide { width: min(880px, 100%); }

/* Restore stepper: a step and its trailing connector wrap as one unit, so the
   two-line stepper never strands a dangling separator. */
.restore-step-group { display: inline-flex; align-items: center; gap: var(--space-2); }

/* Onboarding chapter rail (dead CSS fix): the flex distribution targeted the
   button, but the flex items are the bare <li> wrappers — distribute those, so
   the five chapter labels span the full track and align with the fill. */
.cx-chapters > li { flex: 1 1 0; display: flex; min-width: 0; }
.cx-chapters > li > .cx-chapter { width: 100%; min-width: 0; }

/* Onboarding readiness rows: a wrapped detail line stays in the text column
   rather than returning to the container's left edge under the status dot. */
.readiness-item { display: flex; flex-wrap: wrap; align-items: baseline; column-gap: var(--space-2); }
.readiness-item__hint { flex: 1 1 16rem; min-width: 0; }

/* Onboarding card: a stable scrollbar gutter, so an internally-scrolling card
   never renders its heading flush under the frame edge. BOTH-EDGES, not the
   one-sided `stable` this carried from 2026-07-02 to 2026-08-12, because the
   one-sided reservation is only invisible on a browser that reserves nothing.
   Measured in the running page at 375x812: where the browser reserves a gutter
   (15px here, and a classic-scrollbar Windows or Linux browser reserves one
   too), `stable` takes it from the inline end only, so everything centred in
   the card -- the hero mark, the title, the lede, the primary action -- renders
   7.5px LEFT of the card's own centre, on the first screen a new customer sees.
   `both-edges` reserves symmetrically, so the content centre and the card
   centre coincide at 0.0px offset whether the browser reserves 0px or 15px.
   It does NOT make the render gutter-independent: the content column is still
   narrower by the reserved width, so the lede still wraps to three lines rather
   than two where a gutter is reserved. That residue is the visual lane's
   problem (it pins the bed), not a customer-visible misalignment. */
.cx-card { scrollbar-gutter: stable both-edges; }

/* Hero cards centre their action row, and cardActions() always emits the
   flex spacer that pushes the primary right on an ordinary left-aligned card
   (shared.ts's cardActions). On a hero card the row is shrink-to-fit inside a
   centred column, so the spacer cannot grow: it contributes nothing but its own
   12px `gap`, which pushes the primary button 6px right of the row's centre.
   Measured at 1440x900: the primary sat at 726.0 against a card centre of 720.0
   with no gutter reserved. Dropping the spacer on hero cards only leaves the
   ordinary cards' left-aligned rows untouched. */
.cx-card--hero .cx-card__actions .cx-spacer { display: none; }

/* File pickers: the native control matches the secondary button (the identity
   key selection is the highest-gravitas file input in the product). */
input[type="file"]::file-selector-button {
  font: inherit; font-size: var(--text-sm);
  margin-right: var(--space-3);
  padding: var(--space-1) var(--space-3);
  background: var(--surface); color: var(--text);
  border: 1px solid var(--control-border); border-radius: var(--radius);
  cursor: pointer;
}
input[type="file"]::file-selector-button:hover { background: var(--surface-hover); }

/* Roles builder: a capability the role cannot hold reads disabled as a whole
   row, not only by its tiny native checkbox. */
.check-row--disabled { color: var(--text-muted); cursor: not-allowed; }

/* Config history: the version list (previously styled through an element-level
   style write). */
.card-list { display: grid; gap: var(--space-3); margin-top: var(--space-4); }

/* ---- §23L Light-theme decoration tuning (the canvas/particle gaps the
   programmatic contrast gate cannot see) ------------------------------------ */
:root[data-theme="light"] .idp-tile[data-state="live"]::before {
  background: conic-gradient(
    from var(--dp-idp-angle),
    transparent 0 62%,
    color-mix(in srgb, var(--trust) 45%, transparent) 74%,
    var(--trust) 86%,
    color-mix(in srgb, var(--trust) 40%, #ffffff) 94%,
    transparent 100%
  );
}
:root[data-theme="light"] .idp-tile[data-state="live"] {
  box-shadow:
    0 0 0 1px color-mix(in srgb, var(--trust) 22%, transparent),
    0 10px 24px -14px color-mix(in srgb, var(--trust) 35%, transparent);
}
:root[data-theme="light"] .live-flow__node--engine .live-flow__node-label { color: var(--text); }
:root[data-theme="light"] .canary-chirp__note { stroke: var(--trust); opacity: 1; }
:root[data-theme="light"] .badge--info { border-color: color-mix(in srgb, var(--info) 45%, var(--surface)); }
:root[data-theme="light"] .badge--danger { border-color: color-mix(in srgb, var(--danger) 45%, var(--surface)); }
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) .idp-tile[data-state="live"]::before {
    background: conic-gradient(
      from var(--dp-idp-angle),
      transparent 0 62%,
      color-mix(in srgb, var(--trust) 45%, transparent) 74%,
      var(--trust) 86%,
      color-mix(in srgb, var(--trust) 40%, #ffffff) 94%,
      transparent 100%
    );
  }
  :root:not([data-theme="dark"]) .idp-tile[data-state="live"] {
    box-shadow:
      0 0 0 1px color-mix(in srgb, var(--trust) 22%, transparent),
      0 10px 24px -14px color-mix(in srgb, var(--trust) 35%, transparent);
  }
  :root:not([data-theme="dark"]) .live-flow__node--engine .live-flow__node-label { color: var(--text); }
  :root:not([data-theme="dark"]) .canary-chirp__note { stroke: var(--trust); opacity: 1; }
  :root:not([data-theme="dark"]) .badge--info { border-color: color-mix(in srgb, var(--info) 45%, var(--surface)); }
  :root:not([data-theme="dark"]) .badge--danger { border-color: color-mix(in srgb, var(--danger) 45%, var(--surface)); }
}

/* -----------------------------------------------------------------------------
   Integrations screen. The tile grid REUSES .idp-grid / .idp-tile (and their
   data-state orbit), so the SIEM/monitoring catalogue reads as one family with
   the IdP provider grid. Only the additions live here: the corner status chip,
   the "Auto-parses" tag, the category section heads, and the connected strip.
   Every colour is a semantic token, so both themes work with no extra rules.
   -------------------------------------------------------------------------- */
.integration-sections { display: grid; gap: var(--space-7); }
.integration-grp { display: grid; gap: var(--space-4); }
.integration-grp-head { display: flex; align-items: baseline; gap: var(--space-3); }
.integration-grp-title { font-size: var(--text-lg); letter-spacing: -0.01em; margin: 0; font-weight: var(--weight-semibold); }
.integration-grp-count { font-size: var(--text-sm); color: var(--text-muted); font-variant-numeric: tabular-nums; }
.integration-grp-auto { margin-left: auto; font-size: var(--text-xs); color: var(--trust-fg); display: inline-flex; align-items: center; gap: 5px; }
.integration-grp-auto__dot { width: 6px; height: 6px; border-radius: 50%; background: var(--trust); }

/* The teal "Auto-parses" tag: top-right corner of a tile (the chip sits bottom). */
.integration-autotag {
  position: absolute; top: var(--space-2); right: var(--space-2);
  font-size: 0.6875rem; font-weight: var(--weight-medium); line-height: 1.4;
  color: var(--trust-fg); background: var(--trust-bg);
  border-radius: var(--radius-full); padding: 1px 7px; z-index: 2;
}

/* The corner status chip (bottom-right; "Add" sits bottom-left to clear the auto tag). */
.integration-chip {
  position: absolute; right: var(--space-2); bottom: var(--space-2); z-index: 2;
  display: inline-flex; align-items: center; gap: 4px;
  padding: 2px 8px; border-radius: var(--radius-full);
  font-size: var(--text-xs); font-weight: var(--weight-medium); line-height: 1.4;
  border: 1px solid var(--border); background: var(--bg-subtle); color: var(--text-muted);
}
.integration-chip--add { left: var(--space-2); right: auto; }
.idp-tile:hover .integration-chip--add { color: var(--accent); border-color: var(--accent); }
.integration-chip--active { color: var(--trust-fg); border-color: var(--trust-border); background: var(--trust-bg); }
.integration-chip--owner { color: var(--text-muted); }
/* "Checking": no identity report has arrived, so the gate is not answered yet. It is deliberately quieter
   than --owner (a dashed border, no fill) so it does not read as a settled refusal at a glance. */
.integration-chip--pending { color: var(--text-muted); border-style: dashed; background: transparent; }
.integration-chip__dot { width: 6px; height: 6px; border-radius: 50%; background: var(--trust); flex: none; }

/* The connected strip above the grid. */
.integration-connected {
  display: flex; align-items: center; gap: var(--space-4); flex-wrap: wrap;
  padding: var(--space-4) var(--space-5); border: 1px solid var(--border);
  border-radius: var(--radius-lg); background: var(--surface);
}
.integration-connected__lbl { font-size: var(--text-sm); color: var(--text-muted); }
.integration-connected__lbl b { color: var(--text); font-weight: var(--weight-semibold); }
.integration-connected__chips { display: flex; gap: var(--space-2); flex-wrap: wrap; }
.integration-connected__chip {
  display: inline-flex; align-items: center; gap: 7px; padding: 5px 11px 5px 9px;
  border: 1px solid var(--border); border-radius: var(--radius-full);
  background: var(--bg-subtle); font-size: var(--text-sm); font-weight: var(--weight-medium);
}
.integration-connected__mk { display: inline-flex; width: 17px; height: 17px; }

.integration-mark { display: block; }

/* The setup-panel body helpers (panels.ts): the method/auto note, the copyable URL row, a channel row. */
.integration-note { font-size: var(--text-sm); line-height: 1.55; border-radius: var(--radius); padding: 10px 12px; border: 1px solid var(--border-subtle); background: var(--bg-subtle); color: var(--text-muted); }
.integration-note--auto { color: var(--trust-fg); background: var(--trust-bg); border-color: var(--trust-border); }
.integration-note b { color: inherit; font-weight: var(--weight-semibold); }
.integration-urlrow { display: flex; align-items: center; gap: var(--space-2); }
.integration-url { flex: 1; min-width: 0; overflow-x: auto; font-family: var(--font-mono); font-size: var(--text-xs); color: var(--text-muted); background: var(--bg-subtle); border: 1px solid var(--border); border-radius: var(--radius); padding: 8px 10px; white-space: nowrap; }
.integration-chrow { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); padding: 8px 12px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); font-size: var(--text-sm); }
