/* micro-substrate UI — Microcelium design system port.
 * Ported from microcelium-website-pelican (themes/microcelium-theme/static/css/style.css)
 * 2026-05-29. Tokens + component shapes (cards, buttons, forms, tables,
 * badges, focus ring); substrate-specific layout (sidebar, table-scroll,
 * fleet-grid, build-cards, tab-nav, HTMX morph hooks) is preserved.
 *
 * Theme handling:
 *   - :root carries the dark palette as the default (matches the website).
 *   - data-theme="light" forces the light palette.
 *   - data-theme="dark"  forces the dark palette (already :root, no override).
 *   - data-theme="auto"  + bare <html> follow @media (prefers-color-scheme).
 * The inline boot script in base.html.j2 sets the attribute before paint.
 */

/* ============================================================
   0. Self-hosted webfonts
   ============================================================ */
@font-face {
  font-family: 'Outfit';
  font-style: normal;
  font-weight: 300 600;
  font-display: swap;
  src: url('fonts/outfit-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Outfit';
  font-style: normal;
  font-weight: 300 600;
  font-display: swap;
  src: url('fonts/outfit-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-weight: 500 700;
  font-display: swap;
  src: url('fonts/spacegrotesk-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-weight: 500 700;
  font-display: swap;
  src: url('fonts/spacegrotesk-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* ============================================================
   1. Tokens (dark — default :root, matches the website)
   ============================================================ */
:root {
  /* Surface tiers — Instrument: deep slate-blue ground, cool panels.
     Retuned 2026-07-07 (editorial refresh, Direction A). The whole
     visual identity lives in these token values; the 74 templates and
     the kit are untouched. */
  --bg-abyss: #070c17;
  --bg-deep: #0b1220;
  --bg-mid: #0f1a2e;
  --bg-surface: #132038;
  --bg-surface-hover: #1a2c49;
  --bg-elevated: #16253f;
  --bg-code: #0a101d;

  /* Brand palette: Microcelium cyber-cyan accent (matches the website).
     The trio is collapsed onto the cyan family — accent (cyan), a
     slightly deeper dim, a soft cyan-teal alt for the rare
     second-signal (queued/visited/alt tints), and a brighter cyan pop
     for orphaned. No lavender/magenta — one coherent cyan identity on
     the deeper slate Instrument ground. */
  --accent: #00e5e3;
  --accent-dim: #00c4c2;
  --accent-glow: rgba(0, 229, 227, 0.12);
  --accent-glow-md: rgba(0, 229, 227, 0.25);
  --accent-glow-strong: rgba(0, 229, 227, 0.42);
  --accent-alt: #7fdedd;
  --accent-alt-dim: #5cc9c8;
  --accent-alt-glow: rgba(127, 222, 221, 0.15);
  --accent-pop: #4df3f0;
  --accent-pop-glow: rgba(77, 243, 240, 0.16);

  /* Text — slate-blue biased neutrals, not pure grey. */
  --text-bright: #eef3fb;
  --text-primary: #dce6f5;
  --text-secondary: #8ea0bd;
  --text-muted: #7488a8;
  --text-dim: #66799a;

  /* Borders — hairline slate rules. */
  --border: #1e2c44;
  --border-light: #2c3d5c;

  /* Status — semantic, held apart from the blue accent. */
  --success: #34d399;
  --warn:    #fbbf24;
  --danger:  #f87171;

  /* UI-VIZ-1 micro-visualisation tokens — the ONLY colours the viz
     kit (_viz.html.j2: meters, sparks, stat chips) may use. Aliases
     of the semantic set so a re-theme stays one edit; --viz-tick is
     the one theme-specific literal (track tick marks). */
  --viz-ok:     var(--success);
  --viz-warn:   var(--warn);
  --viz-crit:   var(--danger);
  --viz-accent: var(--accent);
  --viz-track:  var(--border);
  --viz-tick:   rgba(255, 255, 255, 0.10);

  /* Surface tints (cards, dashed borders, faint backgrounds) */
  --accent-bg-tint: rgba(0, 229, 227, 0.06);
  --accent-alt-bg-tint: rgba(127, 222, 221, 0.05);
  --accent-alt-border-dashed: rgba(127, 222, 221, 0.5);

  /* Focus ring — glow rather than hard outline */
  --focus-ring: 0 0 0 3px var(--accent-glow-md);
  --focus-ring-offset: 2px;

  /* Typography */
  --font-display: 'Space Grotesk', sans-serif;
  --font-body: 'Outfit', sans-serif;
  --font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', "SF Mono", Menlo, Consolas, monospace;

  /* Spacing */
  --space-xs: 0.25rem;
  --space-sm: 0.5rem;
  --space-md: 1rem;
  --space-lg: 1.5rem;
  --space-xl: 2rem;
  --space-2xl: 3rem;
  --space-3xl: 4rem;
  --space-4xl: 6rem;
  --space-5xl: 8rem;

  /* Container widths */
  --container: 1200px;
  --container-narrow: 800px;
  --container-wide: 1400px;
  --nav-height: 4rem;

  /* Motion */
  --transition-fast: 150ms ease;
  --transition: 300ms ease;
  --transition-slow: 500ms ease;

  /* Radii */
  --radius-sm: 0.375rem;
  --radius: 0.5rem;
  --radius-lg: 0.75rem;
  --radius-xl: 1rem;
  --radius-2xl: 1.5rem;

  /* ----- Substrate aliases for legacy var names -----
   * Rules elsewhere in the file (and in macros emitted from templates)
   * reference these short names. Map them onto the website token set
   * so we don't have to chase every callsite. */
  --fg:           var(--text-primary);
  --muted:        var(--text-muted);
  --bg:           var(--bg-abyss);
  --card:         var(--bg-surface);
  --accent-visited: var(--accent-alt);
  --error:        var(--danger);
  --error-bg:     rgba(248, 113, 113, 0.10);
  --error-border: rgba(248, 113, 113, 0.35);
  --pass:         var(--success);
  --fail:         var(--danger);
  --skip:         var(--text-muted);
  --code-bg:      var(--bg-code);
  --th-bg:        var(--bg-surface-hover);

  /* Role chips — shade off the existing brand palette rather than
   * inventing new hues. Default chip stays lavender (--accent-alt);
   * the three "loud" roles each pick a distinct band:
   *   firewall  → magenta (--accent-pop, the brand's "danger-ish loud"
   *               token; reads as edge/policy without colliding with
   *               the actual --danger red used for failed statuses);
   *   nas       → blue-cool (a darker, more saturated shade than the
   *               cyan accent, so storage reads as "different" from
   *               control-plane info-chrome);
   *   gateway   → cyan (--accent), echoing the brand's primary cyan
   *               for ingress / north-south flow.
   * Bastion / control-plane / appliance / workload reuse the default
   * lavender so the operator-facing change is opt-in per role.
   */
  --role-firewall: var(--accent-pop);
  --role-firewall-glow: var(--accent-pop-glow);
  --role-firewall-border: rgba(77, 243, 240, 0.30);
  --role-nas: #4a8df0;
  --role-nas-glow: rgba(74, 141, 240, 0.15);
  --role-nas-border: rgba(74, 141, 240, 0.30);
  --role-gateway: var(--accent);
  --role-gateway-glow: var(--accent-glow);
  --role-gateway-border: rgba(0, 229, 227, 0.30);
  --role-default: var(--accent-alt);
  --role-default-glow: var(--accent-alt-glow);
  --role-default-border: rgba(127, 222, 221, 0.30);

  /* ----- Phantom-token aliases (SLICE 0, design-system unify) -----
   * Feature blocks authored across ~100 tasks referenced token names
   * that were never defined (--warning, --card-bg, --rule, --ink, …).
   * Each silently fell through to a hard-coded literal fallback, so
   * those blocks IGNORED light mode. Map every phantom name onto the
   * real token so the whole UI themes coherently. The right long-term
   * fix is to sweep call-sites to canonical names (later slices); these
   * aliases make the theme correct immediately with zero risk. */
  --warning:      var(--warn);
  --ok:           var(--success);
  --bad:          var(--danger);
  --primary:      var(--accent);
  --ink:          var(--text-primary);
  --text:         var(--text-primary);
  --rule:         var(--border);
  --mono:         var(--font-mono);
  --card-bg:      var(--bg-surface);
  --card-bg-deep: var(--bg-mid);
  --surface:      var(--bg-surface);
  --surface-2:    var(--bg-surface-hover);
  --surface-alt:  var(--bg-mid);
  --accent-soft:  var(--accent-glow);
  --code-fg:      var(--text-primary);
  --success-bg:   rgba(52, 211, 153, 0.15);

  color-scheme: dark light;
}

/* ============================================================
   1b. Tokens (light — explicit override + auto via @media)
   ============================================================ */
[data-theme="light"] {
  /* Instrument, light: cool slate-white ground, same electric-blue
     accent as dark so the identity holds across the toggle. */
  /* Tell the UA to render NATIVE form widgets (checkboxes, number
     spinners, <select> popups, date pickers) in their light variant —
     without this the widgets keep the dark scheme from :root and show
     black backgrounds on the light page. */
  color-scheme: light;
  --bg-abyss: #ffffff;
  --bg-deep: #f5f7fb;
  --bg-mid: #eaeef6;
  --bg-surface: #e3e9f2;
  --bg-surface-hover: #d7deea;
  --bg-elevated: #ffffff;
  --bg-code: #eef2f8;

  --accent: #0a8a89;
  --accent-dim: #086d6c;
  --accent-glow: rgba(10, 138, 137, 0.10);
  --accent-glow-md: rgba(10, 138, 137, 0.20);
  --accent-glow-strong: rgba(10, 138, 137, 0.36);

  --accent-alt: #4f5fb0;
  --accent-alt-dim: #3c4a94;
  --accent-alt-glow: rgba(79, 95, 176, 0.12);

  --accent-pop: #3b82f6;
  --accent-pop-glow: rgba(59, 130, 246, 0.12);

  --text-bright: #0b1220;
  --text-primary: #1b2636;
  --text-secondary: #55667a;
  --text-muted: #6f7d91;
  --text-dim: #525e70;

  --border: #d3dae6;
  --border-light: #bcc6d6;

  --success: #0f9d58;
  --warn:    #c2790b;
  --danger:  #c43232;

  /* UI-VIZ-1: dark tick marks on the light track. */
  --viz-tick: rgba(0, 0, 0, 0.10);

  --accent-bg-tint: rgba(10, 138, 137, 0.06);
  --accent-alt-bg-tint: rgba(79, 95, 176, 0.06);
  --accent-alt-border-dashed: rgba(79, 95, 176, 0.4);

  --error-bg:     rgba(196, 50, 50, 0.08);
  --error-border: rgba(196, 50, 50, 0.35);

  /* Role-chip palette under light theme — same band assignments,
   * but each chip's text colour shifts to a darker shade for
   * contrast against the light card background. */
  --role-firewall: #0a8a89;
  --role-firewall-glow: rgba(59, 130, 246, 0.10);
  --role-firewall-border: rgba(59, 130, 246, 0.35);
  --role-nas: #1f4fbf;
  --role-nas-glow: rgba(31, 79, 191, 0.10);
  --role-nas-border: rgba(31, 79, 191, 0.35);
  --role-gateway: #086d6c;
  --role-gateway-glow: rgba(10, 138, 137, 0.10);
  --role-gateway-border: rgba(10, 138, 137, 0.35);
  --role-default: var(--accent-alt-dim);
  --role-default-glow: var(--accent-alt-glow);
  --role-default-border: rgba(79, 95, 176, 0.30);
}

/* Auto-mode + bare <html>: follow OS preference. Mirror the light
 * overrides under the @media query so a fresh page-load with no
 * stored choice still flips correctly. */
@media (prefers-color-scheme: light) {
  :root[data-theme="auto"],
  :root:not([data-theme]) {
    /* Match native form widgets to the light page (see the explicit
       [data-theme=light] note above). */
    color-scheme: light;
    --bg-abyss: #ffffff;
    --bg-deep: #f7f8fc;
    --bg-mid: #eef1f7;
    --bg-surface: #e8ecf3;
    --bg-surface-hover: #dde2eb;
    --bg-elevated: #ffffff;
    --bg-code: #f1f4f9;

    --accent: #0a8a89;
    --accent-dim: #086d6c;
    --accent-glow: rgba(10, 138, 137, 0.10);
    --accent-glow-md: rgba(10, 138, 137, 0.20);
    --accent-glow-strong: rgba(10, 138, 137, 0.36);

    --accent-alt: #4f5fb0;
    --accent-alt-dim: #3c4a94;
    --accent-alt-glow: rgba(79, 95, 176, 0.12);

    --accent-pop: #3b82f6;
    --accent-pop-glow: rgba(59, 130, 246, 0.12);

    --text-bright: #0b1220;
    --text-primary: #1b2636;
    --text-secondary: #55667a;
    --text-muted: #6f7d91;
    --text-dim: #525e70;

    --border: #d3dae6;
    --border-light: #bcc6d6;

    --success: #0f9d58;
    --warn:    #c2790b;
    --danger:  #c43232;

    --accent-bg-tint: rgba(10, 138, 137, 0.06);
    --accent-alt-bg-tint: rgba(79, 95, 176, 0.06);
    --accent-alt-border-dashed: rgba(79, 95, 176, 0.4);

    --error-bg:     rgba(196, 50, 50, 0.08);
    --error-border: rgba(196, 50, 50, 0.35);

    --role-firewall: #3b82f6;
    --role-firewall-glow: rgba(59, 130, 246, 0.10);
    --role-firewall-border: rgba(59, 130, 246, 0.35);
    --role-nas: #1f4fbf;
    --role-nas-glow: rgba(31, 79, 191, 0.10);
    --role-nas-border: rgba(31, 79, 191, 0.35);
    --role-gateway: #086d6c;
    --role-gateway-glow: rgba(10, 138, 137, 0.10);
    --role-gateway-border: rgba(10, 138, 137, 0.35);
    --role-default: var(--accent-alt-dim);
    --role-default-glow: var(--accent-alt-glow);
    --role-default-border: rgba(79, 95, 176, 0.30);
  }
}

/* ============================================================
   2. Reset + base
   ============================================================ */
* { box-sizing: border-box; }

/* SCROLLBAR-STABLE: always reserve the vertical scrollbar gutter so
   pages don't jar sideways when navigating between a content-fits
   page (Dashboard) and a long page (Fleet, Audit). Without this the
   viewport-anchored content shifts ~15px each time the scrollbar
   appears or disappears.

   `scrollbar-gutter: stable` is the modern (Chrome 94+, Firefox 97+,
   Safari 18+) one-liner that reserves the gutter even when overflow
   doesn't require a scrollbar. It applies on the scroll container —
   the root element here, since <main> is not itself scrollable. */
html { scrollbar-gutter: stable; }

body {
  margin: 0;
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 1rem;
  line-height: 1.55;
  color: var(--text-primary);
  background: var(--bg-abyss);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-display);
  font-weight: 600;
  line-height: 1.2;
  color: var(--text-bright);
  letter-spacing: -0.01em;
  margin: 0 0 var(--space-sm);
}
h1 { font-size: 1.5rem; }
h2 { font-size: 1.2rem; }
h3 { font-size: 1.05rem; }
h4 { font-size: 0.95rem; }

img, svg, video { display: block; max-width: 100%; }
ul, ol { list-style: none; padding: 0; margin: 0; }
button, input, textarea, select { font: inherit; color: inherit; }
/* Tint native checkboxes / radios / range with the brand accent (both
   themes; accent-color resolves the token per active theme). */
input[type="checkbox"], input[type="radio"], input[type="range"] { accent-color: var(--accent); }
button { cursor: pointer; }

:focus { outline: none; }
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: var(--focus-ring-offset);
  border-radius: var(--radius-sm);
}

/* ============================================================
   3. Sidebar shell + off-canvas drawer (substrate-specific)
   ============================================================ */
.nav-toggle { position: absolute; opacity: 0; pointer-events: none; }
.nav-backdrop { display: none; }
.nav-burger { display: none; }

.sidebar {
  position: fixed;
  left: 0;
  top: 0;
  bottom: 0;
  width: 14rem;
  padding: 1.25rem 1rem;
  background: var(--bg-deep);
  border-right: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  overflow-y: auto;
  z-index: 2;
}

.brand {
  font-family: var(--font-display);
  font-weight: 700;
  text-decoration: none;
  color: var(--text-bright);
  letter-spacing: -0.005em;
  font-size: 1.1rem;
  padding: 0.25rem 0.5rem;
  margin-bottom: 0.5rem;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
}
/* VERSION-1 — build/release identity under the wordmark + release-channel
   badge + the footer version link. */
.brand-version {
  display: block;
  margin: -0.35rem 0 0.4rem 2.35rem;   /* tuck under the wordmark text */
  font-size: 0.68rem;
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
  text-decoration: none;
  letter-spacing: 0.01em;
}
.brand-version:hover { color: var(--accent); }
.channel-badge {
  display: inline-block;
  margin: 0 0 0.5rem 2.35rem;
  padding: 0.05rem 0.4rem;
  font-size: 0.6rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  border-radius: 0.25rem;
  color: #1a1205;
  background: var(--warn);          /* amber = "not production" */
  border: 1px solid var(--warn);
}
.foot-version {
  margin-left: 0.5rem;
  color: var(--text-muted);
  text-decoration: none;
  font-variant-numeric: tabular-nums;
}
.foot-version:hover { color: var(--accent); }
.brand-mark {
  width: 22px;
  height: 22px;
  flex: none;
  display: block;
}

.sidebar nav {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
}

/* NAV-3: a quiet group heading that separates the primary-work links
 * from the platform destinations, so the sidebar reads as two labelled
 * clusters rather than one long list. */
.nav-group-label {
  margin: 0.7rem 0 0.2rem;
  padding: 0 0.75rem;
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
  opacity: 0.75;
}

.sidebar nav a {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  color: var(--text-secondary);
  text-decoration: none;
  padding: 0.5rem 0.75rem;
  border-radius: var(--radius-sm);
  font-size: 0.9375rem;
  transition: color var(--transition-fast), background var(--transition-fast);
}

.sidebar nav a:hover {
  background: var(--accent-glow);
  color: var(--accent);
}

/* Active page — same canonical `aria-current="page"` hook the tab
 * nav uses elsewhere. Slightly stronger than the hover state so it
 * reads as "you are here" rather than "you can click this". */
.sidebar nav a[aria-current="page"] {
  background: var(--accent-glow);
  color: var(--accent);
  font-weight: 600;
  box-shadow: inset 2px 0 0 var(--accent);
}

/* Same hook for sidebar-foot utility icons + topbar icons — keeps
 * the "you are here" treatment consistent across all nav regions. */
.logout .sidebar-utility[aria-current="page"],
.topbar-icon[aria-current="page"] {
  background: var(--accent-glow);
  color: var(--accent);
  border-color: var(--accent);
}

.sidebar nav a:focus-visible,
.nav-burger:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  box-shadow: var(--focus-ring);
}

.logout {
  margin-top: auto;
  padding-top: 1rem;
  border-top: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  align-items: stretch;
}

.logout .who {
  color: var(--text-muted);
  font-size: 0.85rem;
  word-break: break-word;
}

.logout .who small {
  display: block;
  font-size: 0.75rem;
  opacity: 0.85;
}

/* Sidebar foot utility row — small icon cluster for Config / MCP /
 * Audit. Replaces the floating .who-cog from earlier. Sits between
 * the divider and the username so the inspection/configure surfaces
 * group visually rather than being mid-sidebar nav items. */
.logout .sidebar-utility-row {
  display: flex;
  gap: 0.6rem;
  align-items: center;
  margin-bottom: 0.2rem;
}
/* Same hover treatment as `.topbar-icon` so the inspection-surface
 * affordance is consistent across the topbar + sidebar foot. */
.logout .sidebar-utility {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.3rem 0.45rem;
  color: var(--text-muted);
  text-decoration: none;
  border-radius: 6px;
  border: 1px solid transparent;
  transition: background 0.15s, color 0.15s;
}
.logout .sidebar-utility:hover {
  background: var(--card);
  color: var(--text);
  border-color: var(--border);
}

.logout button { width: 100%; }

main {
  margin-left: 14rem;
  padding: 1.5rem 1.5rem 0.5rem;
  /* DASH-1-FOLLOWUP-2: was 64rem (1024px) here + 76rem at the
     ≥1280px breakpoint below. That left ~280px of dead gutter on
     1920px monitors and clipped the Fleet VM table on both sides
     (VM names truncated at the left, Reprovision button cut on
     the right). 96rem (~1536px) gives wide tables + the dashboard
     dials enough room without making prose pages an unreadable
     full-width measure. */
  max-width: 96rem;
}

/* NO-SIDEBAR-WHEN-LOGGED-OUT: when base.html.j2 doesn't render the
   sidebar (anon visitors / /login), drop the 14rem margin so the
   login card centres against the full viewport instead of being
   pushed right by an invisible reservation. The topbar isn't
   rendered either, so undo its push-down padding too. */
body.no-sidebar main {
  margin-left: 0;
  padding-top: 1.5rem !important;
}

footer {
  margin-top: 2rem;
  padding: 1rem 0;
  color: var(--text-muted);
  text-align: center;
  border-top: 1px solid var(--border);
}
footer small { font-size: 0.8rem; }

/* NAV-1: topbar strip with live-count icons for Jobs + Runners. Sits
   to the right of the sidebar; aligns with the right edge of main
   content so the cluster of glanceable signals is in a consistent
   spot regardless of which page is open. */
.topbar {
  position: fixed;
  top: 0;
  left: 14rem;
  right: 0;
  height: 2.75rem;
  display: flex;
  /* Left-aligned (was flex-end) so the global flyout's right-side
   * slide-in doesn't cover the live-count Jobs/Runners/Fleet-history
   * icons. The flyout sits at the right edge; with right-aligned
   * topbar icons the operator lost sight of them whenever a flyout
   * was open. */
  justify-content: flex-start;
  align-items: center;
  padding: 0 1.5rem;
  background: var(--bg);
  border-bottom: 1px solid var(--border);
  z-index: 50;
}
.nav-toolbox {
  display: flex;
  gap: 0.5rem;
  align-items: center;
}
.topbar-icon {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.3rem 0.55rem;
  border-radius: 6px;
  text-decoration: none;
  color: var(--text-muted);
  font-size: 0.85rem;
  transition: background 0.15s, color 0.15s;
  border: 1px solid transparent;
}
.topbar-icon:hover {
  background: var(--card);
  color: var(--text);
  border-color: var(--border);
}
.topbar-icon.has-attention {
  color: var(--danger);
}
.topbar-icon.has-attention .topbar-badge {
  background: var(--danger);
  color: #fff;
  border-color: var(--danger);
}
.topbar-badge {
  font-size: 0.72rem;
  font-weight: 600;
  padding: 0.05rem 0.4rem;
  background: var(--card);
  color: var(--text);
  border-radius: 999px;
  border: 1px solid var(--border);
  font-variant-numeric: tabular-nums;
}
@keyframes nav-cog-spin {
  to { transform: rotate(360deg); }
}
/* Was `.icon-glyph-settings` (the cog) — the Jobs icon used the
   cog for spin theatre but that overloaded the Config page's
   glyph. Jobs icon is now `activity` (pulse line); spin still
   targets the icon under the Jobs link via the parent's
   data-spinning flag. */
.topbar-icon[data-spinning="true"] .icon-glyph-activity {
  animation: nav-cog-spin 4s linear infinite;
}
/* Push main content down a bit so the topbar doesn't overlap the first
   <section.card> of every page. The topbar is 2.75rem tall + small
   visual breathing room. */
main {
  padding-top: 3.5rem !important;
}

/* NAV-BREADCRUMB-1 — the drill-down trail above a deep page. */
.breadcrumbs { margin: 0 0 1rem; }
.breadcrumbs ol {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; align-items: center; gap: .1rem;
  font-size: .85rem;
}
.breadcrumbs li { display: inline-flex; align-items: center; }
.breadcrumbs li:not(:first-child)::before {
  content: "›"; color: var(--text-muted, #64748b);
  margin: 0 .5rem; font-size: .95em;
}
.breadcrumbs a {
  color: var(--text-muted, #94a3b8); text-decoration: none;
  border-radius: 5px; padding: .1rem .3rem;
}
.breadcrumbs a:hover { color: var(--accent, #22d3ee); }
.breadcrumbs [aria-current="page"] { color: var(--text, #e6edf5); font-weight: 600; }

/* IA-3MAPS — the "see also" cross-link line under a map/graph header. */
.related-views { margin: -.4rem 0 1rem; }
.related-views a { color: var(--accent, #22d3ee); text-decoration: none; }
.related-views a:hover { text-decoration: underline; }

/* FLEET-1 — keep a long truncated issue-detail off the cell's edge. */
.issues-table code.truncate,
td code.truncate { max-width: 46ch; padding-right: .4rem; }

/* NETWORK-VIZ — the switch device list: whole row opens the front panel. */
.switch-row-clickable { cursor: pointer; }
.switch-row-clickable:hover { background: var(--accent-glow, rgba(34,211,238,0.06)); }
.switch-row-clickable:hover .btn { border-color: var(--accent, #22d3ee); }
.site-chip {
  font: 500 .78rem/1 var(--mono, ui-monospace, monospace);
  padding: .12rem .4rem; border-radius: 5px;
  background: var(--panel-2, rgba(148,163,184,0.12));
  color: var(--text-muted, #94a3b8);
}
@media (max-width: 900px) {
  .topbar {
    left: 0;
    padding-left: 4rem; /* leave room for the burger */
  }
}

/* Wide screens — DASH-1-FOLLOWUP-2: was 76rem here, which still
   left a visible gutter on 1920px monitors. Drop the cap entirely
   above 1280px so the content fills the available viewport (sidebar
   + page padding still keep a sane measure). */
@media (min-width: 1280px) {
  main { max-width: none; }
}

/* Mobile: drawer mechanics */
@media (max-width: 640px) {
  .sidebar {
    transform: translateX(-100%);
    transition: transform .2s ease;
    width: min(80vw, 18rem);
    box-shadow: 0 0 24px rgba(0, 0, 0, .35);
  }
  /* Layer contract (mobile drawer): .topbar sits at 50, so everything
     the drawer needs must clear it — backdrop 55 < drawer 70 < burger
     80 (the burger stays tappable ON TOP of the open drawer, so one
     tap toggles it closed). Found live: the burger at z-index 3 was
     painted UNDER the topbar and unreachable. */
  .sidebar { z-index: 70; }
  #nav-toggle:checked ~ .sidebar {
    transform: translateX(0);
  }
  #nav-toggle:checked ~ .nav-backdrop {
    display: block;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.55);
    z-index: 55;
  }
  .nav-burger {
    display: inline-flex;
    position: fixed;
    top: 0.5rem;
    left: 0.5rem;
    z-index: 80;
    align-items: center;
    justify-content: center;
    width: 2.5rem;
    height: 2.5rem;
    background: var(--bg-surface);
    border: 1px solid var(--border-light);
    border-radius: var(--radius);
    color: var(--text-primary);
    cursor: pointer;
    user-select: none;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
  }
  .nav-burger-icon { font-size: 1.2rem; line-height: 1; }

  main {
    margin-left: 0;
    padding: 3.25rem 0.75rem 0.5rem;
  }
  .card { padding: 1rem; }
  .card h1 { font-size: 1.25rem; }
}

/* ============================================================
   4. Cards
   ============================================================ */
.card {
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 1.5rem;
  margin-bottom: 1.5rem;
  transition: border-color var(--transition), box-shadow var(--transition);
}
.card h1 { margin: 0 0 0.5rem 0; font-size: 1.5rem; }
.card .lede { color: var(--text-secondary); margin: 0 0 1rem 0; }
.muted { color: var(--text-muted); }

/* ============================================================
   5. Content links + truncation
   ============================================================ */
main a {
  color: var(--accent);
  text-decoration: none;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.15em;
  transition: color var(--transition-fast);
}
main a:hover { color: var(--accent-pop); text-decoration: underline; }
/* Consistency: an entity link often wraps a <code> chip
   (<a><code>slug</code></a>). Code has its own fixed accent colour + chip
   background, so without this a linked chip looked identical to a static
   one — no "clickable" signal. Make code inside a content link follow the
   link: brighten + underline on hover, share the link's focus ring. */
main a > code { transition: color var(--transition-fast); cursor: pointer; }
main a:hover > code { color: var(--accent-pop); text-decoration: underline; }
main a:focus-visible > code { outline: none; }
/* Was `color: var(--accent-alt)` — coloured visited links a
   different shade. The "have I clicked this?" signal isn't useful
   on an operator dashboard where every row is a thing-to-go-look-at,
   not bookmark navigation. Force visited to match unvisited so the
   table doesn't gradually turn into a colour-coded history. */
main a:visited { color: var(--accent); }
main a:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  box-shadow: var(--focus-ring);
  border-radius: var(--radius-sm);
}

.truncate {
  display: inline-block;
  max-width: 18ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  vertical-align: bottom;
}
td > .truncate, td > span.truncate { max-width: 100%; }

time.ts { font-variant-numeric: tabular-nums; }

/* UX-1: small glyph next to external links to signal "leaves the app". */
.ext-link-glyph {
  display: inline-block;
  margin-left: 0.15em;
  font-size: 0.85em;
  opacity: 0.6;
  vertical-align: super;
}

/* MCP-UI-1: numbered procedural step. Used on /ui/mcp to lift each
   action visually so the page reads as "do this" rather than "here's
   a thing." Pairs with the `step` Jinja macro in _macros.html.j2. */
.step {
  display: grid;
  grid-template-columns: 2rem 1fr;
  gap: 0.75rem;
  margin: 1.25rem 0;
  align-items: start;
}
.step-num {
  font-size: 1.05rem;
  font-weight: 600;
  color: var(--primary, var(--accent, #4a8));
  text-align: center;
  line-height: 1.7rem;
  background: var(--surface, transparent);
  border-radius: 50%;
  border: 2px solid var(--primary, var(--accent, #4a8));
  width: 2rem;
  height: 2rem;
  flex-shrink: 0;
}
.step-body {
  min-width: 0;
}
.step-title {
  margin: 0 0 0.5rem;
  font-size: 1rem;
  font-weight: 600;
}
.step blockquote {
  border-left: 4px solid var(--primary, var(--accent, #4a8));
  padding: 0.6rem 0.85rem;
  background: var(--surface-alt, var(--surface, rgba(0,0,0,0.03)));
  margin: 0.5rem 0;
  font-family: var(--font-mono, monospace);
  font-size: 0.875rem;
  border-radius: 0 4px 4px 0;
  overflow-x: auto;
}
/* Blockquotes across the app ARE command blocks (MCP quickstart
   steps, connect-page curl examples) — the wrapper is the surface,
   so the inline chip fully resets here too (same border/colour bug
   the pre>code reset fixed). Generic, not .step-scoped: the connect
   tabs use bare <blockquote><code>. */
blockquote code {
  background: none;
  padding: 0;
  border: none;
  color: inherit;
}
.step pre {
  margin: 0.5rem 0;
}

.error {
  color: var(--danger);
  background: var(--error-bg);
  border: 1px solid var(--error-border);
  border-radius: var(--radius);
  padding: 0.5rem 0.75rem;
}

/* ============================================================
   6. Tables — substrate's `.grid` class
   ============================================================ */
.table-scroll {
  /* overflow stays VISIBLE by default so an in-row dropdown (the Build
     split-menu popover) can float above the rows below it. An
     `overflow-x: auto` here creates a clip + stacking context that
     traps the absolutely-positioned popover INSIDE the scroller, where
     later rows paint over it (the "Dev environment" / next-row "Build"
     overlap). We only turn the horizontal scroller ON below the table's
     min-width, where the table genuinely can't fit and scrolling is
     needed anyway — the dropdown-clip trade-off only bites narrow
     screens, which already scroll. */
  margin: 0 -0.25rem;
}
@media (max-width: 48rem) {
  .table-scroll {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
  }
}
table.grid { min-width: 36rem; }

table.grid {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.9375rem;
}
table.grid th,
table.grid td {
  text-align: left;
  padding: 0.625rem 0.875rem;
  border-bottom: 1px solid var(--border);
  vertical-align: top;
}
table.grid th {
  background: var(--bg-surface-hover);
  color: var(--text-muted);
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-weight: 600;
}
table.grid tr:last-child td { border-bottom: none; }
table.grid tr:hover td { background: var(--accent-bg-tint); }

[data-theme="light"] table.grid tr:hover td { background: rgba(0, 0, 0, 0.04); }
@media (prefers-color-scheme: light) {
  :root[data-theme="auto"] table.grid tr:hover td,
  :root:not([data-theme]) table.grid tr:hover td { background: rgba(0, 0, 0, 0.04); }
}

table.grid td strong { color: var(--text-bright); font-weight: 600; }

tr.bucket-pass td:nth-child(2),
tr.bucket-ok td:nth-child(2) { color: var(--success); font-weight: 600; }
tr.bucket-fail td:nth-child(2),
tr.bucket-unreachable td:nth-child(2),
tr.bucket-drift td:nth-child(2) { color: var(--danger); font-weight: 600; }
tr.bucket-skip td:nth-child(2) { color: var(--text-muted); }

/* ============================================================
   7. Forms — labels, inputs, buttons, filters
   ============================================================ */
form label {
  display: block;
  margin-bottom: 0.75rem;
}
form label > span {
  display: block;
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--text-secondary);
  margin-bottom: 0.35rem;
}
form input[type="text"],
form input[type="password"],
form input[type="email"],
form input[type="number"],
form input[type="search"],
form input[type="url"],
form input[type="tel"],
form input[type="date"],
form textarea,
form select {
  width: 100%;
  padding: 0.625rem 0.875rem;
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text-primary);
  font: inherit;
  font-size: 0.9375rem;
  transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}
form input[type="text"]:focus,
form input[type="password"]:focus,
form input[type="email"]:focus,
form input[type="number"]:focus,
form input[type="search"]:focus,
form input[type="url"]:focus,
form input[type="tel"]:focus,
form input[type="date"]:focus,
form textarea:focus,
form select:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-glow);
}
form input::placeholder,
form textarea::placeholder { color: var(--text-dim); }
form textarea { resize: vertical; min-height: 6rem; }

form button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  padding: 0.55rem 1.25rem;
  background: var(--accent);
  color: var(--bg-abyss);
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 0.9375rem;
  line-height: 1.1;
  transition: background var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast), color var(--transition-fast), border-color var(--transition-fast);
  white-space: nowrap;
}
form button:hover {
  background: var(--accent-dim);
  box-shadow: 0 0 18px var(--accent-glow-md);
  transform: translateY(-1px);
}
form button:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

/* Secondary / outline buttons — used for less-emphatic table-action
 * buttons (Start/Stop/Reboot live inside table cells and would shout
 * if every one were filled-accent). Mark with `button.secondary`. */
form button.secondary {
  background: transparent;
  color: var(--text-primary);
  border-color: var(--border-light);
}
form button.secondary:hover {
  background: var(--accent-glow);
  border-color: var(--accent);
  color: var(--accent);
  box-shadow: none;
}

form.filters {
  display: flex;
  align-items: flex-end;
  gap: 1rem;
  margin-bottom: 1rem;
}
form.filters label { margin-bottom: 0; flex: 1; }

/* Build forms (/ui/builds/*) — STACKED submit forms with fieldsets.
   Not form.filters: that's the one-row horizontal filter-bar idiom
   (flex-row + label{flex:1}), which squashed these fieldset forms
   into a single ugly row with default browser fieldset bevels. */
form.build-form {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 1rem;
  max-width: 36rem;
  margin-bottom: 1.25rem;
}
form.build-form label { margin-bottom: 0; width: 100%; }
form.build-form fieldset {
  width: 100%;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.6rem 1rem 0.8rem;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
}
form.build-form legend {
  padding: 0 0.5em;
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-secondary);
}
form.build-form legend small.hint {
  text-transform: none;
  letter-spacing: normal;
  font-weight: 400;
  color: var(--text-muted);
}
/* Tick rows: input + text on ONE line. The global `form label > span`
   is display:block (stacked text-input labels) — undo it here. */
form.build-form label.radio,
form.build-form label.checkbox {
  display: flex;
  align-items: baseline;
  gap: 0.55em;
}
form.build-form label.radio > span,
form.build-form label.checkbox > span {
  display: inline;
  margin-bottom: 0;
  font-size: 0.9rem;
  font-weight: 400;
  color: var(--text-primary);
}

/* Artefact actions row (/ui/builds): the parameterised sync form and
   the "Fetch missing media" link sit on ONE flex baseline — without
   this the inline form left the link floating mid-air. */
/* Artefact actions (Sync / Fetch / Upload) as titled cards in a
   responsive grid — auto-fills as many columns as fit, wraps to a
   single stacked column on narrow screens. */
.build-actions {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: 0.75rem;
  align-items: stretch;
}
.action-card {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 0.9rem 1rem;
  border: 1px solid var(--border);
  border-radius: 0.5rem;
  background: var(--card);
}
.action-card__title {
  margin: 0;
  font-size: 0.95rem;
  font-weight: 600;
}
.action-card__hint {
  margin: 0;
  font-size: 0.8125rem;
  color: var(--muted);
  /* push the controls to the card bottom so buttons align across cards */
  flex: 1 1 auto;
}
.action-card__controls {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.4em;
  margin: 0;
}
.action-card__controls select {
  width: auto;
  padding: 0.35rem 0.6rem;
  font-size: 0.875rem;
}
.action-card__controls .btn,
.action-card__controls button {
  padding: 0.4rem 0.9rem;
  font-size: 0.875rem;
}

/* visually-hidden but present for screen readers (form labels) */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.login {
  max-width: 28rem;
  margin: 3rem auto;
}

code {
  background: var(--bg-code);
  padding: 0.1em 0.4em;
  border-radius: var(--radius-sm);
  font-family: var(--font-mono);
  font-size: 0.9em;
  color: var(--accent);
  border: 1px solid var(--border);
}
/* The chip treatment is for INLINE code only. Inside a <pre> (log
   tails, rendered SSH config, exec output, markdown fences) the
   wrapper provides the surface; per-line chips read as boxes around
   every line. */
pre code {
  background: none;
  padding: 0;
  border: none;
  color: inherit;
  font-size: inherit;
}

/* ============================================================
   8. Status pills + role chips (substrate's badge idiom)
   ============================================================ */
.status-pill {
  display: inline-block;
  padding: 0.15em 0.7em;
  border-radius: 999px;
  font-size: 0.78em;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  border: 1px solid transparent;
  line-height: 1.4;
}
/* Brand-mapped pills: blue accent for running/active, cooler blue for paused/queued,
 * success-green for succeeded, danger-red for failed, bright-blue for orphaned. */
.status-running   { background: var(--accent-glow-md);    color: var(--accent);     border-color: rgba(0, 229, 227, 0.45); }
.status-queued    { background: var(--accent-alt-glow);   color: var(--accent-alt); border-color: rgba(127, 222, 221, 0.35); }
.status-succeeded { background: rgba(52, 211, 153, 0.18); color: var(--success);    border-color: rgba(52, 211, 153, 0.40); }
.status-failed    { background: rgba(248, 113, 113, 0.18);color: var(--danger);     border-color: rgba(248, 113, 113, 0.40); }
.status-cancelled { background: var(--accent-alt-glow);   color: var(--accent-alt); border-color: rgba(127, 222, 221, 0.35); }
.status-orphaned  { background: var(--accent-pop-glow);   color: var(--accent-pop); border-color: rgba(77, 243, 240, 0.45); }
/* SAGA-1: a step failed but every completed step's undo succeeded —
 * the failure was COMPENSATED. Warn-amber, deliberately distinct from
 * failed-red (nothing stranded) and success-green (the job didn't do
 * its work). Also rescues refused, which shipped unstyled. */
.status-unwound   { background: rgba(251, 191, 36, 0.16); color: var(--warn);   border-color: rgba(251, 191, 36, 0.40); }
.status-refused   { background: rgba(248, 113, 113, 0.10);color: var(--danger); border-color: rgba(248, 113, 113, 0.45); border-style: dashed; }
/* SAGA-1 MR2: step ladder on the job page/flyout. Per-step statuses
 * reuse the pill shape; the journal vocabulary (pending / running /
 * succeeded / failed / cancelled / undone / undo-failed / no-undo)
 * maps onto saga-<status> classes below. */
.saga-ladder { margin-top: 0.9rem; }
.saga-hdr { margin: 0 0 0.35rem; font-size: 0.82em;
            text-transform: uppercase; letter-spacing: 0.04em;
            color: var(--muted, #8a93a6); }
.saga-steps { list-style: none; margin: 0; padding: 0;
              display: flex; flex-direction: column; gap: 0.3rem; }
.saga-step { display: flex; align-items: center; gap: 0.6rem; }
.saga-idx { min-width: 1.4em; text-align: right;
            color: var(--muted, #8a93a6);
            font-variant-numeric: tabular-nums; font-size: 0.85em; }
.saga-name { flex: 0 1 auto; }
.saga-pending   { color: var(--muted, #8a93a6);
                  border: 1px dashed var(--border, #3a4560);
                  background: transparent; }
.saga-running   { background: var(--accent-glow-md); color: var(--accent);
                  border-color: rgba(0, 229, 227, 0.45); }
.saga-succeeded { background: rgba(52, 211, 153, 0.18); color: var(--success);
                  border-color: rgba(52, 211, 153, 0.40); }
.saga-failed    { background: rgba(248, 113, 113, 0.18); color: var(--danger);
                  border-color: rgba(248, 113, 113, 0.40); }
.saga-cancelled { background: var(--accent-alt-glow); color: var(--accent-alt);
                  border-color: rgba(127, 222, 221, 0.35); }
.saga-undone    { background: rgba(251, 191, 36, 0.16); color: var(--warn);
                  border-color: rgba(251, 191, 36, 0.40); }
.saga-undo-failed { background: rgba(248, 113, 113, 0.18); color: var(--danger);
                    border-color: rgba(248, 113, 113, 0.60);
                    border-style: dashed; }
.saga-no-undo   { color: var(--muted, #8a93a6);
                  border: 1px dashed var(--border, #3a4560);
                  background: transparent; }
.saga-unwind-note { margin-top: 0.4rem; }

/* declared-only: the node is in the operator config but the substrate
 * isn't polling it live yet (no live enumeration merged in). Muted +
 * dashed so it reads as "known on paper, not confirmed live" — visibly
 * NOT an active/managed-running state. HV-DECLARED-PILL. */
.status-declared  { background: transparent; color: var(--muted, #8a93a6);
                    border: 1px dashed var(--border, #3a4560); }

/* Light-mode tweaks for the four hardcoded-rgba pills so they don't
 * wash out on white. */
[data-theme="light"] .status-running   { background: rgba(10, 138, 137, 0.12);  color: var(--accent-dim); }
[data-theme="light"] .status-succeeded { background: rgba(15, 157, 88, 0.14);  color: var(--success); }
[data-theme="light"] .status-failed    { background: rgba(196, 50, 50, 0.12);  color: var(--danger); }
[data-theme="light"] .status-orphaned  { background: rgba(59, 130, 246, 0.10); color: var(--accent-pop); }
@media (prefers-color-scheme: light) {
  :root[data-theme="auto"] .status-running,
  :root:not([data-theme]) .status-running   { background: rgba(10, 138, 137, 0.12);  color: var(--accent-dim); }
  :root[data-theme="auto"] .status-succeeded,
  :root:not([data-theme]) .status-succeeded { background: rgba(15, 157, 88, 0.14);  color: var(--success); }
  :root[data-theme="auto"] .status-failed,
  :root:not([data-theme]) .status-failed    { background: rgba(196, 50, 50, 0.12);  color: var(--danger); }
  :root[data-theme="auto"] .status-orphaned,
  :root:not([data-theme]) .status-orphaned  { background: rgba(59, 130, 246, 0.10); color: var(--accent-pop); }
}

/* ----- SLICE 0: rescue classes that ship UNSTYLED -----
 * These were referenced by live templates but had no CSS rule, so they
 * rendered as bare text (config badges, connect-page chips, action cols).
 * Route them onto the real pill / column idioms. Later slices replace the
 * markup with kit_pill(); this makes them correct now with zero risk. */

/* .pill / .pill-ok / .pill-muted retired by UI-DS Slice 4 — the Hooks/
 * Notifications/Schedules enabled|paused chips now go through the kit's
 * status_pill() macro (succeeded|declared), so these hand-rolled badge
 * classes had zero remaining callers. */

/* .badge — a small count / role chip (connect page, acknowledged-issues
 * count). Lavender to match the "queued/tag" family; not a status. */
.badge {
  display: inline-block;
  padding: 0.1em 0.55em;
  border-radius: 999px;
  font-size: 0.75em;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  background: var(--accent-alt-glow);
  color: var(--accent-alt);
  border: 1px solid rgba(127, 222, 221, 0.35);
  line-height: 1.4;
}

/* .actions-col — 4 of 5 Fleet tabs used this instead of the styled
 * .col-actions, so their action columns got no width/alignment. Alias. */
.actions-col { width: 10em; white-space: nowrap; text-align: right; }

dl.meta {
  display: grid;
  grid-template-columns: 8rem 1fr;
  gap: 0.4rem 1rem;
  margin: 1rem 0 1.5rem;
}
dl.meta dt { font-weight: 600; color: var(--text-muted); font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.04em; }
dl.meta dd { margin: 0; color: var(--text-primary); }

pre.log {
  background: var(--bg-code);
  color: var(--text-primary);
  padding: 1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-family: var(--font-mono);
  font-size: 0.85em;
  line-height: 1.55;
  max-height: 32rem;
  overflow: auto;
  white-space: pre-wrap;
  word-break: break-word;
}

button.danger {
  background: var(--danger);
  border-color: var(--danger);
  color: var(--bg-abyss);
}
/* Job-detail Cancel — breathing room between the saga ladder above
   and the Log heading below; icon sits inline with the label. */
.cancel-form { margin: 1.25rem 0; }
.cancel-form button {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
}
.cancel-form button svg { width: 1rem; height: 1rem; }
button.danger:hover {
  background: #d24a4a;
  border-color: #d24a4a;
  box-shadow: 0 0 18px rgba(248, 113, 113, 0.30);
}

p.empty {
  color: var(--text-muted);
  font-style: italic;
}

/* ============================================================
   9. Theme switcher widget (substrate-specific UX, reskinned)
   ============================================================ */
.theme-switch {
  display: flex;
  gap: 0;
  border: 1px solid var(--border-light);
  border-radius: var(--radius);
  overflow: hidden;
  margin-top: 0.5rem;
  font-size: 0.78rem;
}
.theme-switch button {
  flex: 1;
  padding: 0.35rem 0.45rem;
  background: transparent;
  color: var(--text-muted);
  border: none;
  cursor: pointer;
  font: inherit;
  border-left: 1px solid var(--border);
  transition: color var(--transition-fast), background var(--transition-fast);
}
.theme-switch button:first-child { border-left: none; }
.theme-switch button[aria-pressed="true"] {
  background: var(--accent-glow);
  color: var(--accent);
  font-weight: 600;
}
.theme-switch button:hover { color: var(--text-bright); }
.theme-switch button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* ============================================================
   10. Fleet table (substrate-specific)
   ============================================================ */
table.grid td .status-pill {
  margin-right: 0.3em;
}
table.grid td .status-pill:last-child {
  margin-right: 0;
}

.role-chip {
  display: inline-block;
  padding: 0.15em 0.6em;
  border-radius: 999px;
  font-size: 0.78em;
  font-weight: 500;
  background: var(--role-default-glow);
  color: var(--role-default);
  border: 1px solid var(--role-default-border);
  margin-right: 0.25em;
  white-space: nowrap;
}
.role-chip:last-child { margin-right: 0; }

/* Per-role overrides — only the three "loud" classifications shift
 * off the default lavender. Bastion / control-plane / appliance /
 * workload inherit the default chip styling (matches today's look).
 * Unknown role names (operator-defined custom roles) also fall back
 * to the default — the template's class generation is opt-in: only
 * a class exactly matching one of the rules below picks up a colour
 * shift. */
.role-chip.role-firewall {
  background: var(--role-firewall-glow);
  color: var(--role-firewall);
  border-color: var(--role-firewall-border);
}
.role-chip.role-nas {
  background: var(--role-nas-glow);
  color: var(--role-nas);
  border-color: var(--role-nas-border);
}
/* temp = ephemeral (ROLES-2) — amber so machines slated for the
 * 24h reaper stand out in every table. */
.role-chip.role-temp {
  background: rgba(251, 191, 36, 0.12);
  color: var(--warn);
  border-color: rgba(251, 191, 36, 0.35);
}
.role-chip.role-gateway {
  background: var(--role-gateway-glow);
  color: var(--role-gateway);
  border-color: var(--role-gateway-border);
}
/* Explicit aliases for the lavender-default roles. Same tokens as
 * the base .role-chip rule; declared so future re-skinning has a
 * single place to override per-role if we change our mind. */
.role-chip.role-bastion,
.role-chip.role-control-plane,
.role-chip.role-appliance,
.role-chip.role-workload,
.role-chip.role-cargo-server,
.role-chip.role-generic-server {
  background: var(--role-default-glow);
  color: var(--role-default);
  border-color: var(--role-default-border);
}

td.fleet-actions {
  white-space: nowrap;
  /* Inline children baseline-align against whatever else the row
     contains, so a taller cell elsewhere (a drawn sparkline vs an
     empty one) pushed the buttons down a few px and the column read
     as ragged between rows. `vertical-align: top` on the cell itself
     pins the group to the cell's top edge instead of the row's text
     baseline — the fix without making a <td> a flex container, which
     browsers treat inconsistently for table cells. */
  vertical-align: top;
}
td.fleet-actions form {
  /* inline-flex + middle to match .split-action — an inline-block
     form baselines against the split groups' vertical-align:middle
     and the row of buttons wobbles (found live on the VMs tab:
     Reprovision sat a couple of px above the Stop/Reboot/Rebuild
     split pairs). */
  display: inline-flex;
  vertical-align: middle;
  margin-right: 0.35em;
}
td.fleet-actions form:last-child { margin-right: 0; }
td.fleet-actions button {
  padding: 0.3em 0.75em;
  font-size: 0.82em;
  background: transparent;
  color: var(--text-primary);
  border: 1px solid var(--border-light);
}
td.fleet-actions button:hover {
  background: var(--accent-glow);
  border-color: var(--accent);
  color: var(--accent);
  box-shadow: none;
  transform: none;
}

tr.fleet-declared-only td {
  opacity: 0.7;
}

/* Split-action: primary form + a tiny <details> dropdown */
.split-action {
  display: inline-flex;
  align-items: stretch;
  margin-right: 0.35em;
  vertical-align: middle;
}
.split-action > form { margin-right: 0; display: inline-flex; }
.split-action > form > button {
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
  margin-right: 0;
}
.split-menu {
  position: relative;
  display: inline-flex;
  align-items: stretch;
}
/* The caret shares the .btn metrics (same padding-block, inherited
   font) and stretches to the main button's full height, so the pair
   reads as ONE control instead of a button with a smaller tag
   floating next to it. */
.split-menu > summary {
  list-style: none;
  cursor: pointer;
  user-select: none;
  padding: 0.5rem 0.55rem;
  font: inherit;
  background: var(--bg-surface);
  border: 1px solid var(--border-light);
  border-left: none;
  border-top-right-radius: var(--radius);
  border-bottom-right-radius: var(--radius);
  color: var(--text-primary);
  display: inline-flex;
  align-items: center;
  transition: background var(--transition-fast), color var(--transition-fast);
}
.split-menu > summary::-webkit-details-marker { display: none; }
/* In-table split pairs: `td.fleet-actions form/button` outrank the
   generic .split-action rules on specificity (re-adding the 0.35em
   inter-form gap and shrinking the button but not the caret). Re-
   assert the pairing at matching specificity: no gap, and the caret
   shares the compact row-button metrics + transparent background. */
td.fleet-actions .split-action > form {
  display: inline-flex;
  margin-right: 0;
}
td.fleet-actions .split-menu > summary {
  padding: 0.3em 0.55em;
  font-size: 0.82em;
  background: transparent;
}
td.fleet-actions .split-popover form { display: block; margin: 0; }
/* When open, lift the whole control's stacking context above later
   rows. The absolutely-positioned popover overflows into the NEXT
   row(s); per CSS table painting order, a positioned element inside an
   earlier cell does NOT automatically rise above a later row's cell
   content, so the dropdown items painted BEHIND the row below into an
   unreadable mush. The reliable fix is to lift the whole CELL that
   contains the open menu (`:has()`) into its own raised stacking
   context — then the popover, a descendant, clears every later row. */
.split-menu[open] { z-index: 50; }
td:has(> .split-action .split-menu[open]),
td:has(> .split-menu[open]),
td.fleet-actions:has(.split-menu[open]),
.fleet-detail-actions:has(.split-menu[open]) {
  position: relative;
  z-index: 50;
}
.split-menu[open] > summary { background: var(--accent-glow); color: var(--accent); }
.split-menu > summary:hover { background: var(--accent-glow); color: var(--accent); }
.split-menu > summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.split-popover {
  position: absolute;
  right: 0;
  top: calc(100% + 0.25em);
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.3em;
  z-index: 51;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
  white-space: nowrap;
  min-width: 8rem;
}
.split-popover > form { display: block; margin: 0; }
.split-popover > form + form { margin-top: 0.25em; }
.split-popover button {
  width: 100%;
  text-align: left;
  font-size: 0.85em;
  padding: 0.35em 0.65em;
}

.split-popover button.danger {
  background: transparent;
  border-color: var(--danger);
  color: var(--danger);
}
.split-popover button.danger:hover {
  background: rgba(248, 113, 113, 0.14);
  box-shadow: none;
  transform: none;
}

/* Fleet-specific table sizing */
table.fleet-grid { min-width: 0; }
table.fleet-grid td,
table.fleet-grid th {
  padding: 0.5rem 0.7rem;
}

table.fleet-grid .row-name {
  font-weight: 500;
  max-width: 20ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text-bright);
}
/* VMS-2: the name may ellipsise, but the #vmid must stay visible — it's
   the disambiguator. Lay name + vmid side by side; only the name clips. */
table.fleet-grid .row-name-wrap {
  display: inline-flex; align-items: baseline; gap: .3rem;
  max-width: 100%;
}
table.fleet-grid .row-vmid { flex: none; white-space: nowrap; }
table.fleet-grid .row-sub {
  font-size: 0.78em;
  color: var(--text-muted);
  max-width: 18ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

@media (max-width: 900px) {
  table.fleet-grid .col-mem { display: none; }
}
@media (max-width: 1100px) {
  table.fleet-grid .col-cpu { display: none; }
}

/* UI-VIZ-1 — the micro-visualisation kit (meters, sparks, chips).
   Rendered by _viz.html.j2; colours come ONLY from --viz-* tokens.
   Design: config docs/plans/UI-VIZ-1.md. */

/* meter / meter3 — the gauge family */
.gauge {
  display: inline-block;
  position: relative;
  width: 6rem;
  height: 1.15rem;
  border-radius: var(--radius-sm);
  background: var(--viz-track);
  overflow: hidden;
  vertical-align: middle;
  font-size: 0.78em;
  line-height: 1.15rem;
  font-variant-numeric: tabular-nums;
}
/* Instrument ticks at 25/50/75% of the track — over the fill, under
   the label — so a glance reads position, not just colour. */
.gauge::after {
  content: '';
  position: absolute;
  inset: 0;
  background:
    linear-gradient(to right, transparent calc(25% - 1px), var(--viz-tick) calc(25% - 1px) 25%, transparent 25%),
    linear-gradient(to right, transparent calc(50% - 1px), var(--viz-tick) calc(50% - 1px) 50%, transparent 50%),
    linear-gradient(to right, transparent calc(75% - 1px), var(--viz-tick) calc(75% - 1px) 75%, transparent 75%);
  pointer-events: none;
}
.gauge-fill {
  position: absolute;
  inset: 0 auto 0 0;
  border-radius: var(--radius-sm);
  transition: width 0.15s linear;
}
/* The needle: a bright leading edge on the fill — the "live reading"
   affordance every hardware VU meter has. */
.gauge-fill::after {
  content: '';
  position: absolute;
  right: 0;
  top: 0;
  bottom: 0;
  width: 2px;
  background: rgba(255, 255, 255, 0.55);
}
.gauge-label {
  position: relative;
  z-index: 1;
  display: block;
  text-align: center;
  font-weight: 600;
  /* OSD treatment: white text + dark outline reads on EVERY surface
     the label crosses (dark track, green/amber/red fill, both
     themes). The old blend/invert trick rendered dark-on-dark in
     dark mode — unreadable percentages (found live). The contrast
     contract test pins this rule to the outline treatment. */
  color: #fff;
  text-shadow: 0 0 3px rgba(0, 0, 0, 0.9), 0 1px 1px rgba(0, 0, 0, 0.7);
}
.gauge-green .gauge-fill { background: var(--viz-ok); }
.gauge-amber .gauge-fill { background: var(--viz-warn); }
.gauge-red   .gauge-fill { background: var(--viz-crit); }
/* A hot meter glows — the one-glance "look here" signal. */
.gauge-red .gauge-fill { box-shadow: 0 0 8px var(--viz-crit); }
/* "over" — the used metric read slightly above its configured cap
   (page cache / balloon accounting). A benign, expected state on a busy
   VM: full bar in the warn hue, NOT the red-alarm treatment, so it
   doesn't cause alarm fatigue (VMS-RAM-1). */
.gauge-over .gauge-fill { background: var(--viz-warn); opacity: .8; }
.gauge-over .gauge-label { opacity: .85; }

/* CONSOLE-2a serial console page */
.console-topbar {
  display: flex;
  align-items: baseline;
  gap: 0.9rem;
  flex-wrap: wrap;
  margin-bottom: 0.6rem;
}
.console-status {
  font-size: 0.85em;
  font-weight: 500;
}
.console-status.is-pending { color: var(--warn); }
.console-status.is-ok { color: var(--success); }
.console-status.is-err { color: var(--danger); }
.console-host {
  height: min(70vh, 42rem);
  background: #101014;
  border-radius: var(--radius-sm);
  padding: 0.4rem;
}

/* HUD-1 — the Quake-style drop-down console dock (hud.js). Fixed under
   the top edge, slides in/out on .is-open; hiding does not close the
   session. The terminal keeps the console-host dark background in both
   themes (a terminal is a terminal). */
.hud-dock {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 300;
  display: flex;
  flex-direction: column;
  height: min(45vh, 30rem);
  background: var(--surface);
  border-bottom: 1px solid var(--border-light);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
  transform: translateY(-102%);
  transition: transform 140ms ease-out;
}
.hud-dock.is-open { transform: translateY(0); }
.hud-bar {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  padding: 0.35rem 0.8rem;
  border-bottom: 1px solid var(--border);
}
.hud-title { font-weight: 600; font-size: 0.9em; }
.hud-spacer { flex: 1; }
.hud-host {
  flex: 1;
  min-height: 0;
  background: #101014;
  padding: 0.3rem 0.5rem;
}
.hud-host .xterm { height: 100%; }
/* SUBSTRATE-CONSOLE-1: the resumable-session badge — a server-held
   console session exists; Ctrl+` (or a click) reattaches + replays. */
.hud-badge {
  position: fixed;
  top: 0.4rem;
  right: 0.8rem;
  z-index: 290;
  font-size: 0.75em;
  padding: 0.15rem 0.55rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  color: var(--text-muted);
  cursor: pointer;
}
.hud-badge:hover { color: var(--text); border-color: var(--border-light); }

/* Split button — primary action + a narrow caret alternative. */
.btn-split { display: inline-flex; }
.btn-split > .btn {
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
}
.btn-split > .btn-split-caret {
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
  margin-left: -1px;
  padding-left: 0.45rem;
  padding-right: 0.45rem;
}

/* CONSOLE-2b capacity view — used fill + allocated tick vs physical */
.capacity-bar { width: 9rem; }
.capacity-alloc-tick {
  position: absolute;
  top: -1px;
  bottom: -1px;
  width: 2px;
  margin-left: -1px;
  background: var(--text);
  z-index: 2;
}
.capacity-over .capacity-alloc-tick { background: var(--danger); }
.capacity-over { box-shadow: 0 0 0 1px var(--danger); }
.capacity-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
  gap: var(--space-3, 0.75rem);
  margin: 0.5rem 0 1rem;
}
.capacity-card {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.6rem 0.75rem;
}
.capacity-card h4 {
  margin: 0 0 0.35rem;
  font-size: 0.9em;
}
.capacity-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0.2rem 0;
  font-size: 0.82em;
}
.capacity-row .capacity-res {
  width: 2.6rem;
  color: var(--text-dim, var(--text));
}
.capacity-rollup {
  display: flex;
  flex-wrap: wrap;
  gap: 1.25rem;
  align-items: center;
  margin: 0.25rem 0 0.5rem;
  font-size: 0.85em;
}

/* Sparkline */
.cell-stack {
  display: inline-flex;
  flex-direction: column;
  align-items: stretch;
  gap: 0.2em;
}
.sparkline {
  width: 6rem;
  height: 1.1rem;
  display: inline-block;
  vertical-align: middle;
}
.sparkline polyline {
  fill: none;
  stroke: var(--viz-accent);
  stroke-width: 1.4;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.sparkline circle { fill: var(--viz-accent); }
/* The empty state must occupy the SAME box as a drawn sparkline.
   It was a bare inline span with only an opacity, so a row with no
   samples was ~1.1rem shorter than one with them — and because the
   action buttons sit on the row's baseline, the Stop/Reboot/Rebuild
   group drifted vertically between rows (spotted live on the VMs
   tab: two maps rows, one sampled, one not). Match the svg's box. */
.sparkline-empty {
  opacity: 0.5;
  display: inline-block;
  width: 6rem;
  height: 1.1rem;
  line-height: 1.1rem;
  vertical-align: middle;
}
/* UI-VIZ-1: soft area wash under the line + a last-value dot (both
   spark families) — trend + current reading in one glance. */
.sparkline .spark-area,
.dash-spark .spark-area {
  fill: var(--viz-accent);
  opacity: 0.14;
  stroke: none;
}
.sparkline .spark-dot,
.dash-spark .spark-dot {
  fill: var(--viz-accent);
  stroke: none;
}
/* delta() — ▲/▼ change affix. Direction-neutral colour by design:
   whether "up" is good is the caller's semantics. */
.viz-delta {
  font-size: 0.45em;
  font-weight: 600;
  vertical-align: super;
  margin-left: 0.35em;
  letter-spacing: 0.02em;
  font-variant-numeric: tabular-nums;
  color: var(--text-secondary);
  white-space: nowrap;
}
.viz-delta-flat { opacity: 0.5; }
/* .viz-delta-up / .viz-delta-down carry NO colour, deliberately. A
   class-audit will flag them as "used but undefined" — they are not a
   bug. The macro is shared, and up is not universally good: a delta on
   this page could be CPU, RAM, pending updates or cost, where a rise
   is bad. Colouring the arrow green would make a spike read as an
   improvement. If a caller ever has an unambiguous good direction,
   give delta() a polarity argument (`good='down'`) rather than
   colouring by direction here. As of 2026-09-11 nothing in the live UI
   passes delta_pct at all — the only use is the viz-kit swatch. */

/* Row-expand toggle */
.row-toggle {
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  display: inline-flex;
  align-items: center;
  gap: 0.25em;
  color: var(--text-bright);
  cursor: pointer;
  font: inherit;
  text-align: left;
  width: 100%;
}
.row-toggle:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}
.row-toggle:hover .row-name {
  text-decoration: underline;
  text-decoration-color: var(--accent);
}
.row-toggle-caret {
  font-size: 0.75em;
  color: var(--text-muted);
  transition: transform 0.12s ease;
  display: inline-block;
  width: 1ch;
}
.row-toggle.row-toggle-open .row-toggle-caret {
  transform: rotate(90deg);
}
/* Inline modifier — for VM-name / job-id occurrences in narrow
 * table cells (fleet-history, audit, dashboard recent activity).
 * Drops the row-width filling + decorates with a subtle underline
 * so operators can spot "this is clickable, opens a flyout". */
.row-toggle.row-toggle-inline {
  width: auto;
  display: inline;
  color: var(--accent, #6aa);
  text-decoration: underline dotted;
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
}
.row-toggle.row-toggle-inline:hover {
  text-decoration-style: solid;
}

/* HV-ICON-ROW: compact icon-button cluster for per-hypervisor actions
 * (Credentials / Onboard / Edit / Remove). Each .icon-btn is also a
 * .row-toggle that opens the global flyout with that action's form. */
.hv-action-icons {
  display: flex;
  gap: 0.25rem;
  align-items: center;
}
.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.9rem;
  height: 1.9rem;
  padding: 0;
  border: 1px solid var(--border, #2a3550);
  border-radius: 6px;
  background: transparent;
  color: var(--fg, #cdd6e4);
  cursor: pointer;
  line-height: 0;
}
.icon-btn:hover {
  background: var(--surface-2, #1c2436);
  border-color: var(--accent, #6aa);
  color: var(--accent, #6aa);
}
.icon-btn:focus-visible {
  outline: 2px solid var(--accent, #6aa);
  outline-offset: 1px;
}
.icon-btn svg {
  width: 1.05rem;
  height: 1.05rem;
}
.icon-btn.icon-btn-danger:hover {
  border-color: var(--danger, #c73030);
  color: var(--danger, #c73030);
}

/* Job flyout (job_flyout.fragment.html.j2). Lighter weight than the
 * full /ui/jobs/<id> page: meta + tail log + "View full job" link. */
.job-flyout-id { margin: 0 0 0.6em 0; font-size: 1rem; }
.job-flyout-log-hdr { margin: 1em 0 0.3em 0; font-size: 0.9rem; }
.job-flyout-log {
  max-height: 18em;
  font-size: 0.8rem;
}
.job-flyout-footer {
  margin-top: 1em;
  display: flex;
  align-items: center;
  gap: 0.6em;
  flex-wrap: wrap;
}

.fleet-row-active > td:first-child {
  box-shadow: inset 3px 0 0 var(--accent);
}
.fleet-row-active > td {
  background: var(--accent-glow);
}

/* CATALOG-HARMONISE-1 — the service-template catalogue as a right-side
   slide-in DRAWER (Stack Map: drag a card onto the canvas that stays
   visible; Configs: declutter the long page). Narrower than the fleet
   detail panel — it's a palette, not a wide table — so the canvas/page
   behind stays usable for drag-out. */
.cat-drawer {
  position: fixed;
  top: 0;
  right: 0;
  height: 100dvh;
  /* Wide enough for ~3–4 card columns on desktop so you scan many
     services at once; the 62vw cap keeps a strip of the Stack Map
     canvas visible on the left for drag-out. */
  width: min(60rem, 62vw);
  background: var(--bg-elevated);
  border-left: 1px solid var(--border);
  box-shadow: -6px 0 24px rgba(0, 0, 0, 0.30);
  transform: translateX(100%);
  transition: transform 0.18s ease;
  z-index: 60;
  display: flex;
  flex-direction: column;
}
.cat-drawer.cat-drawer-open { transform: translateX(0); }
.cat-drawer-hdr {
  display: flex; align-items: center; justify-content: space-between;
  padding: 0.85rem 1rem; border-bottom: 1px solid var(--border); gap: 0.5rem;
}
.cat-drawer-hdr h4 { margin: 0; font-size: 1.05rem; }
.cat-drawer-body { overflow-y: auto; padding: 0.85rem 1rem; flex: 1; }
/* In the drawer the cards flow into a responsive multi-column grid
   (~3–4 columns on desktop) so you can scan the catalogue quickly and
   drag any card onto the canvas. Auto-fills to as many ~14rem columns
   as fit; collapses to one column on a narrow drawer. */
.cat-drawer .catalogue-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
  gap: 0.6rem;
}
.cat-drawer-backdrop {
  position: fixed; inset: 0; background: rgba(0, 0, 0, 0.25);
  z-index: 59; opacity: 0; pointer-events: none;
  transition: opacity 0.18s ease;
}
.cat-drawer-backdrop.cat-drawer-open { opacity: 1; pointer-events: auto; }
/* CATALOG-DRAG-FIX: the open backdrop is a full-viewport pointer-events
   layer at z-index 59 — it sits ON TOP of the map canvas and swallowed
   every dragover during a catalogue-card drag, so the card could never
   reach the drop overlay. While a card is being dragged (body class set
   on dragstart) the backdrop must let drag events pass THROUGH to the
   canvas. It's non-interactive during the drag anyway (you're dropping,
   not clicking to dismiss). */
body.sm-card-dragging .cat-drawer-backdrop.cat-drawer-open {
  pointer-events: none;
}
@media (max-width: 640px) { .cat-drawer { width: 100vw; } }

/* Right-side slide-in detail panel.
   Width tuned for the post-REMOTE-LINK-1-UI flyout: log tails, exec
   stdout, container tables, and reach diagrams all want more
   horizontal space than the original 28rem allowed. The 60vw cap
   stops it dominating wide monitors. Mobile gets the full screen via
   the media query below. */
.fleet-side-panel {
  position: fixed;
  top: 0;
  right: 0;
  height: 100dvh;
  /* Operator wants the panel to dominate — wide tables (Containers,
     History) + multi-homed IP chips need the room. 72rem (~1150px)
     gives that on a big monitor; the 92vw cap keeps a sliver of the
     fleet table visible behind it as context on any width. Earlier
     stops (44rem → 56rem) were too timid. */
  width: min(72rem, 92vw);
  background: var(--bg-elevated);
  border-left: 1px solid var(--border);
  box-shadow: -6px 0 24px rgba(0, 0, 0, 0.30);
  transform: translateX(100%);
  transition: transform 0.18s ease;
  /* Was z-index: 5 — the .topbar sits at 50, so the topbar overlapped
     the panel's top edge and made the close X unclickable. The mobile
     .nav-burger at z-index: 3 has the same overlap symptom.
     Lift the panel above both: it's an operator-driven modal-ish
     surface, so it should win over passive chrome. */
  z-index: 60;
  display: flex;
  flex-direction: column;
}
.fleet-side-panel.fleet-side-panel-open {
  transform: translateX(0);
}
.fleet-side-panel-hdr {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.85rem 1rem;
  border-bottom: 1px solid var(--border);
  gap: 0.5rem;
}
.fleet-side-panel-hdr h3 {
  margin: 0;
  font-size: 1.05rem;
  font-weight: 600;
  color: var(--text-bright);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.fleet-side-panel-close {
  background: none;
  border: none;
  cursor: pointer;
  font-size: 1.4rem;
  line-height: 1;
  color: var(--text-muted);
  padding: 0.1em 0.4em;
  border-radius: var(--radius-sm);
}
.fleet-side-panel-close:hover { background: var(--accent-glow); color: var(--accent); }
.fleet-side-panel-close:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.fleet-side-panel-body {
  padding: 0.75rem 1rem 1.25rem;
  overflow-y: auto;
  flex: 1 1 auto;
}
.fleet-side-panel-body .fleet-detail {
  padding: 0;
}
.fleet-side-panel-body .fleet-detail-hdr {
  display: none;
}
.fleet-side-panel-body .fleet-detail-graphs {
  grid-template-columns: 1fr;
  max-width: 100%;
}
@media (max-width: 640px) {
  .fleet-side-panel { width: 100vw; }
}

/* Detail sub-row + panel */
.fleet-detail-row > td {
  padding: 0 !important;
  background: var(--accent-bg-tint);
  border-bottom: 1px solid var(--border);
}
.fleet-detail {
  padding: 1rem 1.25rem 1.25rem 2.25rem;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}
.fleet-detail-hdr {
  display: flex;
  align-items: baseline;
  gap: 0.75rem;
  flex-wrap: wrap;
}
.fleet-detail-hdr h3 { margin: 0; font-size: 1.05rem; }
.fleet-detail-meta {
  margin: 0;
  grid-template-columns: 7rem 1fr;
  gap: 0.3rem 0.75rem;
}
.fleet-detail-graphs {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.75rem;
  max-width: 36rem;
}
.fleet-detail-graphs .big-spark .sparkline {
  width: 100%;
  height: 3rem;
}
.fleet-detail-config { margin-top: 0.25rem; }
.fleet-detail-config > summary {
  cursor: pointer;
  user-select: none;
  font-size: 0.9em;
  color: var(--text-muted);
}
.fleet-detail-config table.grid {
  margin-top: 0.5rem;
  font-size: 0.85em;
}
.fleet-detail-config th {
  font-weight: 500;
  color: var(--text-muted);
  width: 10rem;
  vertical-align: top;
}
.ip-chip {
  display: inline-block;
  padding: 0.1em 0.6em;
  border-radius: var(--radius-sm);
  background: var(--accent-glow);
  color: var(--accent);
  font-family: var(--font-mono);
  font-size: 0.85em;
  margin-right: 0.3em;
  margin-bottom: 0.15em;
  border: 1px solid rgba(0, 229, 227, 0.22);
}
[data-theme="light"] .ip-chip { color: var(--accent-dim); border-color: rgba(10, 138, 137, 0.30); }
@media (prefers-color-scheme: light) {
  :root[data-theme="auto"] .ip-chip,
  :root:not([data-theme]) .ip-chip { color: var(--accent-dim); border-color: rgba(10, 138, 137, 0.30); }
}

.banner {
  display: block;
  padding: 0.45em 0.75em;
  border-radius: var(--radius);
  font-size: 0.88em;
}
.banner-error {
  background: var(--error-bg);
  border: 1px solid var(--error-border);
  color: var(--danger);
}
.banner-ok {
  background: var(--success-bg);
  border: 1px solid rgba(52, 211, 153, 0.40);
  color: var(--success);
}
/* UI-DS gap-fill: the warn/info banner tones + the .notice / .callout
   message-box family several pages already reference but that had no
   rule (they rendered as unstyled text). Modelled on .banner above,
   theme-aware via the semantic tokens. */
.banner-warn, .notice-warn, .callout-warning {
  background: color-mix(in srgb, var(--warn, #d29922) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--warn, #d29922) 42%, transparent);
  color: var(--warn, #d29922);
}
.banner-info {
  background: var(--accent-bg-tint, rgba(88,166,255,0.12));
  border: 1px solid color-mix(in srgb, var(--accent, #58a6ff) 40%, transparent);
  color: var(--accent, #58a6ff);
}
/* .notice / .callout — the low-key inline message box (info by default;
   the -warn / -warning / -muted modifiers tint it). Same box metrics as
   .banner so the two families read consistently. */
.notice, .callout {
  display: block;
  padding: 0.5em 0.8em;
  border-radius: var(--radius);
  border: 1px solid var(--border);
  background: var(--bg-elevated, var(--card-bg));
  color: var(--text-secondary, var(--muted));
  font-size: 0.88em;
}
.callout-warning { border-color: color-mix(in srgb, var(--warn, #d29922) 42%, transparent); }
.notice-muted { color: var(--text-muted, var(--muted)); opacity: 0.9; }

/* UI-DS gap-fill: small text/number/tag utilities used across many
   fragments with no backing rule. */
.mono { font-family: var(--font-mono, monospace); }
.tnum { font-variant-numeric: tabular-nums; }
/* `.small` was a no-op everywhere; make it the intended slightly-smaller
   text (paired with .muted on most call sites). */
.small { font-size: 0.82em; }
/* Standalone tag chip — same shape as .config-chip, neutral tone. */
.chip {
  display: inline-block; padding: 0.05em 0.55em;
  border-radius: 999px; font-size: 0.72em; font-weight: 600;
  border: 1px solid var(--border);
  background: var(--bg-elevated, var(--card-bg));
  color: var(--text-secondary, var(--muted));
}
/* A label-over-input form row + a key/value definition grid, both used
   by several pages with no rule (they fell back to browser defaults). */
/* .input / .input-sm — the class form of the canonical form-input look
   (so an input styled by class, e.g. the Stack-Map wire controls,
   matches the `form input[type=text]` rule even outside a <form>).
   .input-sm is the compact variant for dense inline control rows. */
.input {
  padding: 0.5rem 0.75rem;
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text-primary);
  font: inherit;
  font-size: 0.9375rem;
}
.input-sm { padding: 0.3rem 0.55rem; font-size: 0.85rem; }
.form-row { display: flex; flex-direction: column; gap: 0.25rem; margin-bottom: 0.75rem; }
.kv-grid { display: grid; grid-template-columns: auto 1fr; gap: 0.2rem 0.75rem; margin: 0; }
.kv-grid dt { color: var(--text-muted, var(--muted)); font-size: 0.85em; }
.kv-grid dd { margin: 0; }

/* ============================================================
   11. Build-type cards (Builds index grid)
   ============================================================ */
.build-cards {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
}
main a.build-card {
  flex: 1 1 16rem;
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 1.25rem 1.5rem;
  text-decoration: none;
  color: var(--text-primary);
  display: block;
  transition: border-color var(--transition), background var(--transition), box-shadow var(--transition), transform var(--transition);
}
main a.build-card:hover {
  border-color: var(--accent);
  background: var(--bg-surface-hover);
  box-shadow: 0 0 24px var(--accent-glow);
  transform: translateY(-2px);
  text-decoration: none;
}
main a.build-card:visited { color: var(--text-primary); }
main a.build-card:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  box-shadow: var(--focus-ring);
}

/* DASH-1 / KPI cards + donut widgets — same "anchor styled as
   button-shaped card" pattern as .build-card above. Opt them out
   of the global `main a` text-link rule so the accent colour +
   underline-on-hover that belongs to inline links (Recent
   activity, project slugs in the below-the-fold tables) doesn't
   leak onto the whole-card click targets in the dashboard strip.

   Border colour + box-shadow on hover are still defined on
   `.kpi-card:hover` / `.dash-widget:hover` in the DASH-1-MR3
   block; this rule just stops the text inside the card from
   inheriting link colours. */
main a.kpi-card,
main a.dash-widget {
  color: var(--fg, inherit);
  text-decoration: none;
}
main a.kpi-card:hover,
main a.dash-widget:hover {
  color: var(--fg, inherit);
  text-decoration: none;
}
main a.kpi-card:visited,
main a.dash-widget:visited {
  color: var(--fg, inherit);
}

.build-card h2 { margin: 0 0 0.4rem 0; font-size: 1.05rem; color: var(--text-bright); font-family: var(--font-display); }
.build-card p { margin: 0; color: var(--text-secondary); font-size: 0.92em; }

/* ============================================================
   12. Project-detail tab nav
   ============================================================ */
.tab-nav {
  display: flex;
  gap: 0.25rem;
  border-bottom: 1px solid var(--border);
  margin: 0 0 1rem 0;
  flex-wrap: wrap;
}
main .tab-nav a {
  padding: 0.55rem 0.95rem;
  text-decoration: none;
  color: var(--text-muted);
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  font-size: 0.9375rem;
  transition: color var(--transition-fast), border-color var(--transition-fast);
}
main .tab-nav a:hover {
  color: var(--text-bright);
  text-decoration: none;
  border-bottom-color: var(--accent-glow-strong);
}
main .tab-nav a.tab-active {
  color: var(--text-bright);
  border-bottom-color: var(--accent);
  font-weight: 500;
}
/* :visited overrides removed — see the `main a:visited` rule
   higher up; we now treat visited and unvisited as the same
   colour fleet-wide. The unvisited tab-nav + tab-active styles
   apply to :visited automatically. */

/* Count pill after a tab label (e.g. the Configs tab's pending-commit
   count so operators don't forget to Push). Only rendered when the
   count is nonzero. Amber = "needs your attention", distinct from the
   accent so it reads as a nudge, not decoration. */
main .tab-nav a .tab-badge {
  display: inline-block;
  margin-left: 0.4em;
  min-width: 1.25em;
  padding: 0 0.35em;
  border-radius: 999px;
  font-size: 0.72rem;
  font-weight: 600;
  line-height: 1.45;
  text-align: center;
  color: #1a1205;
  background: var(--warn, #fbbf24);
  vertical-align: baseline;
}

/* Amber count pill on the /ui/projects list, next to a project's slug,
   for unpushed Studio commits. Same amber = "needs your attention" cue
   as the Configs-tab badge; it's a link straight to the Configs tab.
   Only rendered when the count is nonzero. */
a.pending-badge {
  display: inline-block;
  margin-left: 0.5em;
  min-width: 1.25em;
  padding: 0 0.35em;
  border-radius: 999px;
  font-size: 0.72rem;
  font-weight: 600;
  line-height: 1.45;
  text-align: center;
  color: #1a1205;
  background: var(--warn, #fbbf24);
  vertical-align: baseline;
  text-decoration: none;
}
a.pending-badge:hover { filter: brightness(1.08); }
/* `main a:visited` / `main a:hover` (0,1,2) outrank a.pending-badge
   (0,1,1) and painted the pill text cyan on the /ui/projects list —
   unreadable on the amber. Re-assert the ink at winning specificity
   for every state. */
main a.pending-badge,
main a.pending-badge:visited,
main a.pending-badge:hover {
  color: #1a1205;
  text-decoration: none;
}
/* Sidebar variant: the pill rides inside the nav <a>, so it's a span
   (no nested-anchor) and sits flush with the label baseline. */
span.nav-badge {
  color: #1a1205;
  background: var(--warn, #fbbf24);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.15rem;
  height: 1.15rem;
  padding: 0 0.3rem;
  margin-left: 0.35rem;
  border-radius: 999px;
  font-size: 0.7rem;
  font-weight: 700;
  vertical-align: middle;
}
/* At count 0 the span stays in the DOM (it carries the HTMX poll) but
   must not paint — same specificity tier as span.nav-badge, so this
   rule must come AFTER it or inline-flex wins the tie. */
span.nav-badge.nav-badge-empty {
  display: none;
}

/* GOVERN-1: the blueprint Governance edit form. A responsive 2/3-column
   grid of labelled inputs; notes span full width. */
.governance-form { margin: 0.5rem 0 1rem; }
.governance-form .gov-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: 0.6rem 1rem;
  margin-bottom: 0.6rem;
}
.governance-form label {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  font-size: 0.85rem;
  font-weight: 600;
}
.governance-form input,
.governance-form select,
.governance-form textarea {
  font-weight: 400;
  width: 100%;
}
.governance-form .gov-notes { display: block; margin-bottom: 0.6rem; }

/* BLUEPRINT-COMPLETE-1: the "Complete blueprint" edit panel. Reuses the
   governance-form grid for the identity half; the context-row editor is a
   stack of fieldsets, each a mini 2-column grid. */
.blueprint-meta { margin: 0.5rem 0 1rem; }
.blueprint-meta > summary { cursor: pointer; font-weight: 600; }
.blueprint-meta .bp-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: 0.6rem 1rem;
  margin: 0.6rem 0;
}
.blueprint-meta label {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  font-size: 0.85rem;
  font-weight: 600;
}
.blueprint-meta input,
.blueprint-meta select,
.blueprint-meta textarea { font-weight: 400; width: 100%; }
.blueprint-meta .context-row {
  border: 1px solid var(--border, #d0d0d8);
  border-radius: 0.4rem;
  padding: 0.6rem 0.8rem;
  margin-bottom: 0.6rem;
}
.blueprint-meta .context-row legend {
  font-size: 0.8rem;
  font-weight: 600;
  padding: 0 0.35rem;
}
.blueprint-meta .context-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
  gap: 0.5rem 0.8rem;
}
.blueprint-meta .ctx-remove { align-self: flex-end; }

/* Nested sub-nav (e.g. the MCP tab's Quickstart / Reference) — a
   lighter, smaller second row under a top-level .tab-nav. */
.tab-nav-sub {
  border-bottom-width: 1px;
  margin-top: -0.25rem;
  gap: 0.15rem;
}
main .tab-nav-sub a { font-size: 0.875rem; padding: 0.4rem 0.7rem; }

/* Rendered markdown docs (the Connect → CLI tab renders
   docs/ui/connect-cli.md). Give it comfortable reading typography +
   fenced-code / table styling that matches the rest of the UI. Scoped
   to .md-body so it never leaks into app chrome. */
.md-body { max-width: 74ch; line-height: 1.6; }
.md-body h2 { margin: 1.6rem 0 0.6rem; font-size: 1.3rem; }
.md-body h3 { margin: 1.3rem 0 0.5rem; font-size: 1.08rem; }
.md-body p { margin: 0.6rem 0; }
.md-body ul, .md-body ol { margin: 0.6rem 0; padding-left: 1.4rem; }
.md-body li { margin: 0.25rem 0; }
.md-body code {
  background: var(--bg-code);
  padding: 0.1rem 0.35rem;
  border-radius: var(--radius-sm);
  font-size: 0.88em;
}
.md-body pre {
  background: var(--bg-code);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space-md);
  overflow-x: auto;
  margin: 0.8rem 0;
}
/* Inside a fenced block the inline-code chip treatment must fully
   reset — the global `code` rule adds a BORDER + accent colour, which
   drew a box around every line of every code block. */
.md-body pre code {
  background: none;
  padding: 0;
  border: none;
  color: inherit;
  font-size: 0.86rem;
}
.md-body table { border-collapse: collapse; margin: 0.8rem 0; width: 100%; }
.md-body th, .md-body td {
  border: 1px solid var(--border);
  padding: 0.4rem 0.65rem;
  text-align: left;
}
.md-body th { background: var(--bg-elevated); font-weight: 600; }
.md-body .md-fallback { white-space: pre-wrap; }
main .tab-nav a:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  box-shadow: var(--focus-ring);
  border-radius: var(--radius-sm);
}

/* ============================================================
   13. Tab empty-state card
   ============================================================ */
div.empty {
  border: 1px dashed var(--border-light);
  border-radius: var(--radius);
  padding: 1rem 1.2rem;
  margin: 0 0 1rem 0;
  color: var(--text-muted);
  background: var(--accent-bg-tint);
}
div.empty p { margin: 0.25rem 0; }
div.empty p:first-child { font-weight: 500; color: var(--text-primary); }

/* Reduced-motion */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* FLEET-6c: inline Tabler-svg icon glyphs. The macro lib at
 * api/templates/_icons.html.j2 emits <svg class="icon-glyph"
 * stroke="currentColor"> so styling is just baseline align +
 * subtle muted colour by default. Override per context with
 * a more specific selector if needed. */
.icon-glyph {
  display: inline-block;
  vertical-align: -0.18em;
  flex-shrink: 0;
  color: var(--text-muted);
}
/* Field-label rows in flyouts: icon + label, with the icon sitting
 * to the left of the dt text. The dt is a grid cell of width 7rem
 * (see .fleet-detail-meta above) — give it just enough flex shape
 * to keep the icon-text gap consistent. */
.fleet-detail-meta dt {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  color: var(--text-muted);
  font-weight: 500;
}
.fleet-detail-meta dt .icon-glyph {
  color: var(--text-muted);
}
/* Section h4s in the flyout: small accent icon to the left of the
 * heading text, same visual weight as the muted dt rows. */
.fleet-detail h4 {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
}

/* REACH-1c.1: inline bastion popover. <details> wraps an underlined
   bastion name; click opens the body which loads via HTMX once and
   floats above subsequent content so the surrounding sentence flow
   stays intact. */
.bastion-popover {
  position: relative;
  display: inline;
}
.bastion-popover > summary {
  list-style: none;
  cursor: pointer;
  display: inline;
}
.bastion-popover > summary::-webkit-details-marker { display: none; }
.bastion-popover-trigger code {
  text-decoration: underline dotted;
  text-underline-offset: 0.2em;
}
.bastion-popover-trigger:hover code { color: var(--accent, #0a66c2); }
.bastion-popover[open] > summary .bastion-popover-trigger code {
  color: var(--accent, #0a66c2);
}
.bastion-popover-content {
  position: absolute;
  top: 1.4em;
  left: 0;
  z-index: 20;
  min-width: 14em;
  max-width: 22em;
  padding: 0.6em 0.8em;
  background: var(--bg, #fff);
  border: 1px solid var(--border, #ccc);
  border-radius: 6px;
  box-shadow: 0 4px 14px rgba(0,0,0,0.12);
  font-size: 0.85rem;
  line-height: 1.4;
}
.bastion-popover-body p { margin: 0.3em 0; }
.bastion-popover-head { display: flex; align-items: center; gap: 0.5em; }
.bastion-popover-actions { display: flex; gap: 0.4em; margin: 0.5em 0; }
.bastion-popover-actions form { display: inline; margin: 0; }
.bastion-popover-foot {
  border-top: 1px solid var(--border, #eee);
  padding-top: 0.4em;
  margin-top: 0.3em;
}

/* TOPO-1-MR3 — Topology tab */
.topology-card { padding: 1rem; }
.topology-toolbar {
  display: flex;
  gap: 1.25rem;
  align-items: center;
  flex-wrap: wrap;
  margin: 0.5em 0 0.75em 0;
}
.topology-layers {
  display: inline-flex;
  gap: 1em;
  align-items: center;
  border: 1px solid var(--border, #444);
  padding: 0.2em 0.6em;
  border-radius: 6px;
}
.topology-layers legend {
  padding: 0 0.4em;
  font-size: 0.85em;
  color: var(--muted, #999);
}
.topology-layers label {
  font-size: 0.9em;
  cursor: pointer;
}
.topology-project { font-size: 0.9em; }
.topology-project input { margin-left: 0.4em; padding: 0.2em 0.4em; }
.topology-canvas-wrap {
  position: relative;
  background: var(--card-bg-deep, #111);
  border: 1px solid var(--border, #333);
  border-radius: 6px;
  overflow: hidden;
}
#topology-canvas { display: block; cursor: grab; }
#topology-canvas:active { cursor: grabbing; }
.topology-side {
  position: absolute;
  right: 1rem;
  top: 1rem;
  width: 20rem;
  max-height: 32rem;
  overflow-y: auto;
  background: var(--card-bg, #1a1a1a);
  border: 1px solid var(--border, #333);
  border-radius: 6px;
  padding: 0.75em 1em;
  z-index: 5;
}
.topology-side-close {
  float: right;
  background: none;
  border: none;
  color: var(--muted, #999);
  font-size: 1.4em;
  cursor: pointer;
  margin-top: -0.4em;
}
.topology-side .meta dt { font-size: 0.8em; color: var(--muted, #999); }
.topology-side .meta dd { margin: 0 0 0.5em 0; word-break: break-all; }
.topo-kind {
  display: inline-block;
  font-size: 0.7em;
  padding: 0.1em 0.4em;
  border: 1px solid #555;
  border-radius: 4px;
}

/* REMOTE-LINK-1-UI: container action panels (Logs / Exec) appended
   into the flyout's Containers section. */
.cargo-container-actions { white-space: nowrap; }
.cargo-container-actions .btn { margin-right: 0.25em; }
.fleet-detail-container-logs,
.fleet-detail-container-exec {
  margin-top: 0.75em;
  padding: 0.6em 0.8em;
  border: 1px solid var(--border, #333);
  border-radius: 6px;
  background: var(--card-bg, rgba(255,255,255,0.02));
}
.fleet-detail-container-logs h5,
.fleet-detail-container-exec h5 {
  margin: 0 0 0.4em 0;
  font-size: 0.95rem;
  display: flex;
  align-items: center;
  gap: 0.4em;
  flex-wrap: wrap;
}
.fleet-detail-container-logs h5 .btn,
.fleet-detail-container-exec h5 .btn { margin-left: auto; }
.fleet-detail-container-exec textarea {
  display: block;
  font-family: ui-monospace, monospace;
  font-size: 0.85em;
  width: 100%;
  max-width: 40em;
  margin: 0.25em 0 0.5em 0;
}
.log-tail {
  background: var(--code-bg, rgba(0,0,0,0.25));
  color: var(--code-fg, #ddd);
  padding: 0.5em 0.7em;
  border-radius: 4px;
  overflow-x: auto;
  max-height: 24em;
  overflow-y: auto;
  font-size: 0.85em;
  white-space: pre;
  margin: 0.25em 0;
}
.exec-result { margin-top: 0.5em; }
.exec-result details { margin-top: 0.4em; }
.exec-result details summary {
  cursor: pointer;
  font-size: 0.85em;
  color: var(--muted, #999);
}

/* ============================================================
   ISSUES-1-MR4 — Dismiss popover + Show acknowledged expander
   ============================================================ */

/* Action column on the Issues table — narrow, right-aligned. */
.col-actions { width: 10em; white-space: nowrap; text-align: right; }

/* <details>-based popover. Native CSS toggle; no JS for open/close. */
.issue-dismiss { position: relative; display: inline-block; }
.issue-dismiss-trigger {
  cursor: pointer;
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: 0.25em;
  font-size: 0.85em;
  padding: 0.25em 0.6em;
  border: 1px solid var(--border, #333);
  border-radius: 4px;
  background: var(--card-bg, rgba(255,255,255,0.02));
  color: var(--fg, inherit);
}
.issue-dismiss-trigger::-webkit-details-marker { display: none; }
.issue-dismiss[open] .issue-dismiss-trigger {
  background: var(--card-bg, rgba(255,255,255,0.05));
  border-color: var(--accent, #6aa);
}
.issue-dismiss-content {
  position: absolute;
  right: 0;
  top: calc(100% + 4px);
  min-width: 14em;
  z-index: 10;
  padding: 0.6em 0.8em;
  background: var(--bg, #1a1a1a);
  border: 1px solid var(--border, #333);
  border-radius: 6px;
  box-shadow: 0 4px 16px rgba(0,0,0,0.3);
  text-align: left;
  white-space: normal;
}
.issue-dismiss-content .hint { margin: 0 0 0.4em 0; font-size: 0.85em; }
.issue-dismiss-options {
  display: flex;
  flex-direction: column;
  gap: 0.3em;
}
.issue-dismiss-options button {
  text-align: left;
  font-size: 0.85em;
  padding: 0.35em 0.6em;
  border: 1px solid var(--border, #333);
  border-radius: 4px;
  background: transparent;
  color: var(--fg, inherit);
  cursor: pointer;
}
.issue-dismiss-options button:hover {
  background: var(--card-bg, rgba(255,255,255,0.05));
}
.issue-dismiss-status { margin: 0.5em 0 0 0; min-height: 1.2em; }

/* "Show acknowledged" expander below the Issues table. */
.issues-ack-expander { margin-top: 0.75em; }
.issues-ack-expander > summary {
  cursor: pointer;
  font-size: 0.9em;
  color: var(--muted, #999);
  padding: 0.3em 0;
}
.issues-ack-expander > summary .badge {
  display: inline-block;
  margin-left: 0.4em;
  padding: 0 0.5em;
  border-radius: 999px;
  background: var(--card-bg, rgba(255,255,255,0.06));
  font-size: 0.8em;
}
.issues-ack-expander > summary::-webkit-details-marker { display: inline; }
.issue-undismiss-btn {
  font-size: 0.85em;
  padding: 0.25em 0.6em;
  border: 1px solid var(--border, #333);
  border-radius: 4px;
  background: transparent;
  color: var(--fg, inherit);
  cursor: pointer;
}
.issue-undismiss-btn:hover {
  background: var(--card-bg, rgba(255,255,255,0.05));
}
.issue-undismiss-btn:disabled { opacity: 0.6; cursor: progress; }

/* ============================================================
   DASH-1-MR3 — dashboard dials layout
   ============================================================
   Three-row stacking: greeting / KPI strip / donut strip / below-fold.
   Each KPI card is a clickable button (anchor with .kpi-card). The
   sparkline + spark-empty classes share the same theme tokens as the
   existing per-VM sparkline. */

/* `.kpi-row` — the COMPACT stat strip used by the fleet tabs
   (Containers / Data protection / Billing / Swarm), as opposed to
   `.dash-kpis`, which is the dashboard's row of big clickable cards.
   Same `stat_chip` markup, different context: a tab's chips summarise
   the data BELOW them, so they must not own the first screen.

   This rule did not exist. Four templates used the class and CSS had
   never defined it, so the chips fell back to block layout and stacked
   FULL-WIDTH — four numbers consuming ~600px, pushing every actual
   table below the fold at 1512x807. Found by looking at the rendered
   page; nothing in the templates hints the class is undefined.

   Kept beside `.dash-kpis` deliberately: they are siblings, and the
   next person adding a stat strip should see both and pick. */
.kpi-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 0.6em;
  margin: 0 0 1em 0;
}
/* A tab chip is a readout, not a target: no reserved sparkline height
   and tighter padding, so it lands ~78px instead of 155px. */
.kpi-row .kpi-card {
  min-height: 0;
  padding: 0.65em 0.7em;
}
.kpi-row .kpi-card .kpi-number { font-size: 1.6rem; line-height: 1.15; }
.kpi-row .kpi-card .kpi-sub { opacity: 0.75; }
/* A non-clickable chip must not pretend to be a button. */
.kpi-row div.kpi-card:hover {
  transform: none;
  box-shadow: none;
  border-color: var(--border, #333);
}

.dash-kpis {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 0.75em;
  margin: 0 0 1.25em 0;
}
.kpi-card {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  text-align: center;
  gap: 0.15em;
  padding: 1em 0.8em;
  border: 1px solid var(--border, #333);
  border-radius: 8px;
  background: var(--card-bg, rgba(255,255,255,0.02));
  color: var(--fg, inherit);
  text-decoration: none;
  transition: transform 80ms ease, box-shadow 80ms ease, border-color 120ms ease;
  /* Reserve the filled-card height so the skeleton (2 spans) doesn't
   * reflow when it swaps to the populated card (number + label + sub
   * + 18px sparkline). */
  min-height: 155px;
}
.kpi-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0,0,0,0.18);
  border-color: var(--accent, #6aa);
}
.kpi-card .kpi-number {
  font-size: 2.2rem;
  font-weight: 700;
  line-height: 1.0;
  font-variant-numeric: tabular-nums;
}
.kpi-card .kpi-label {
  font-size: 0.85rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  opacity: 0.7;
}
.kpi-card .kpi-sub {
  font-size: 0.8rem;
  opacity: 0.65;
  margin-top: 0.2em;
}
.kpi-card .kpi-spark { display: block; margin-top: 0.4em; width: 100%; max-width: 80px; }
.kpi-card .kpi-spark .dash-spark {
  width: 100%; height: 18px;
  stroke: var(--accent, #6aa); stroke-width: 1.4; fill: none;
}
.kpi-card .kpi-spark .dash-spark-empty { opacity: 0.5; font-size: 0.8em; }

/* Loading skeleton: dim text, no hover lift. */
.kpi-card-loading { opacity: 0.55; cursor: progress; }
.kpi-card-loading .kpi-number { opacity: 0.35; }

/* Attention / spinning / error variants — colour the border so the
   strip reads at a glance. UI-VIZ-1: on the --viz-* tokens (the old
   var(--warning) never existed — the fallback literal was doing the
   work). */
.kpi-card-attention { border-color: var(--viz-warn); }
.kpi-card-attention .kpi-number { color: var(--viz-warn); }
.kpi-card-spinning  { border-color: var(--viz-accent); }
.kpi-card-error     { border-color: var(--viz-crit); }
.kpi-card-error .kpi-number { color: var(--viz-crit); }
.kpi-card-error .kpi-err {
  display: block; font-size: 0.75em; opacity: 0.7; margin-top: 0.3em;
  color: var(--viz-crit);
}

/* Donut widgets row. */
.dash-widgets {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 0.75em;
  margin: 0 0 1.25em 0;
}
.dash-widget {
  display: block;
  padding: 1em 1.2em;
  border: 1px solid var(--border, #333);
  border-radius: 8px;
  background: var(--card-bg, rgba(255,255,255,0.02));
  text-decoration: none;
  color: var(--fg, inherit);
  transition: transform 80ms ease, box-shadow 80ms ease, border-color 120ms ease;
}
.dash-widget:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0,0,0,0.18);
  border-color: var(--accent, #6aa);
}
.dash-widget-loading { opacity: 0.55; cursor: progress; }
.dash-widget-error   { border-color: var(--danger, #c33); }
/* Reserve the rendered donut card height so the load -> populated swap
 * doesn't reflow the page (skeleton has only title + "loading…"; the
 * filled card carries a 140px donut + a ~5-item legend). */
.dash-widget-donut   { min-height: 290px; }
.dash-widget-title {
  margin: 0 0 0.5em 0;
  font-size: 0.9rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  opacity: 0.8;
}
.dash-widget-err {
  margin: 0.3em 0 0 0; color: var(--danger, #c33); font-size: 0.75em;
}

.dash-donut {
  display: block; margin: 0.3em auto 0.4em auto;
  max-width: 140px; height: auto;
}
.dash-donut-number {
  fill: currentColor; font-size: 1.8rem; font-weight: 700;
  font-variant-numeric: tabular-nums; opacity: 0.95;
}
.dash-donut-label {
  fill: currentColor; font-size: 0.7rem; opacity: 0.6;
  text-transform: uppercase; letter-spacing: 0.06em;
}
.dash-donut-legend {
  list-style: none; padding: 0; margin: 0.4em 0 0 0;
  display: grid; grid-template-columns: 1fr 1fr; gap: 0.2em 0.6em;
  font-size: 0.8rem;
}
.dash-donut-legend li {
  display: flex; align-items: center; gap: 0.4em;
}
.dash-legend-swatch {
  display: inline-block; width: 0.7em; height: 0.7em; border-radius: 50%;
  flex-shrink: 0;
}

/* Donut slice colours (paired with .dash-legend-swatch via the same
   classes for legend↔slice symmetry). */
.donut-ok    { stroke: #34d399; background: #34d399; }
.donut-cool  { stroke: #6aa;    background: #6aa;    }
.donut-warn  { stroke: #c80;    background: #c80;    }
.donut-bad   { stroke: #c33;    background: #c33;    }
.donut-mute  { stroke: #888;    background: #888;    }

/* Below-the-fold downsized tables. Two-column grid on wide screens,
   stack on narrow. */
.dash-belowfold {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
  gap: 0.75em;
}

@media (max-width: 640px) {
  .dash-kpis { grid-template-columns: repeat(2, 1fr); }
  .kpi-row   { grid-template-columns: repeat(2, 1fr); }
  .kpi-card .kpi-number { font-size: 1.6rem; }
}

/* ============================================================
   CONFIG-PAGE-TIDY — definition-list layout for /ui/admin/config
   ============================================================
   Replaces the per-section 5-column table with a per-row
   definition list. Auto-save on input change; the submit button
   stays in the markup as a no-JS fallback but is visually
   hidden. */

.config-jump {
  display: flex; flex-wrap: wrap; gap: 0.4em; margin: 1em 0 0.25em;
}
.config-jump a {
  display: inline-flex; align-items: center; gap: 0.35em;
  padding: 0.25em 0.7em; border: 1px solid var(--border, #333);
  border-radius: 999px; font-size: 0.85rem; text-decoration: none;
  background: var(--card-bg, rgba(255,255,255,0.02));
}
.config-jump a:hover { border-color: var(--accent, #6af); }

.config-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(30rem, 1fr));
  gap: 1em; align-items: start; margin-top: 1em;
}
@media (max-width: 700px) { .config-grid { grid-template-columns: 1fr; } }

.config-section {
  margin: 0; padding: 0.9em 1em;
  border: 1px solid var(--border, #333); border-radius: 8px;
  background: var(--card-bg, rgba(255,255,255,0.02));
  scroll-margin-top: 4.5rem;  /* anchor jumps clear the topbar */
}
.config-section h2 {
  display: flex; align-items: center; gap: 0.4em;
  font-size: 1.05rem; margin: 0 0 0.5em 0;
  border-bottom: 1px solid var(--border, #333);
  padding-bottom: 0.35em;
}

.config-list {
  display: flex; flex-direction: column;
  gap: 0.6em; margin: 0 0 0.5em 0; padding: 0;
}

.config-row {
  display: grid;
  grid-template-columns: 1fr;   /* stacked: sections now sit in a
                                   two-up grid, so rows are narrow */
  align-items: start; gap: 0.5em;
  padding: 0.6em 0.8em;
  border: 1px solid var(--border, #333);
  border-radius: 6px;
  background: var(--card-bg, rgba(255,255,255,0.02));
}
.config-row-cold {
  border-left: 3px solid var(--warning, #c80);
}

.config-field {
  display: flex; flex-direction: column; gap: 0.2em;
  margin: 0; min-width: 0;
}
.config-field-name {
  display: flex; align-items: center; gap: 0.5em;
  font-weight: 600;
}
.config-field-name code {
  font-family: ui-monospace, monospace;
  font-size: 0.95em;
}
.config-field-desc {
  font-size: 0.85em; opacity: 0.8;
}
.config-field-default {
  font-size: 0.75em; opacity: 0.55;
}
.config-field-default code {
  font-family: ui-monospace, monospace;
  font-size: 0.95em;
}

.config-chip {
  display: inline-block; padding: 0 0.5em;
  border-radius: 999px; font-size: 0.7em;
  font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em;
}
.config-chip-cold {
  background: rgba(204, 136, 0, 0.18);
  color: var(--warning, #c80);
  border: 1px solid rgba(204, 136, 0, 0.4);
}

.config-value { margin: 0; }
.config-value form { display: flex; align-items: center; gap: 0.5em; margin: 0; }
.config-value input,
.config-value select { flex: 1; min-width: 0; }

/* Hide the no-JS fallback button visually but keep it in the
   accessibility tree (keyboard + curl + screen-reader can still use
   it). When JS is off / disabled, surface it again. */
.config-save-fallback {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0, 0, 0, 0); white-space: nowrap;
  border: 0;
}

/* Inline state indicator after a row auto-save. */
.config-save-state { font-size: 1.2em; line-height: 1; opacity: 0; transition: opacity 0.25s; }
.config-edit[data-status="ok"]  .config-save-state::before { content: "✓"; color: var(--success, #34d399); }
.config-edit[data-status="err"] .config-save-state::before { content: "!"; color: var(--danger,  #c33); }
.config-edit[data-status]       .config-save-state { opacity: 1; }

@media (max-width: 720px) {
  .config-row { grid-template-columns: 1fr; }
}

/* Fleet > Overview triage panel — two columns on wide, stacks under 60rem.
   The wrapper is a plain grid; the two children are .card blocks so they
   inherit the normal card chrome and only need their default margin
   cleared so the grid gap controls spacing. */
.overview-triage {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1rem;
}
.overview-triage .overview-triage-col { margin: 0; }
@media (max-width: 60rem) {
  .overview-triage { grid-template-columns: 1fr; }
}

/* Flyout lifecycle-actions strip — mirrors the row's `.fleet-actions`
   button look so the operator sees consistent affordances whether
   they're acting from the row or from inside the flyout. */
.fleet-detail-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35em;
  margin: 0.5em 0 1em;
  white-space: nowrap;
}
.fleet-detail-actions form { display: inline-block; margin: 0; }
.fleet-detail-actions button {
  padding: 0.3em 0.75em;
  font-size: 0.82em;
  background: transparent;
  color: var(--text-primary);
  border: 1px solid var(--border-light);
}
.fleet-detail-actions button:hover {
  background: var(--accent-glow);
  border-color: var(--accent);
  color: var(--accent);
  box-shadow: none;
  transform: none;
}
/* The split caret is a <summary>, not a <button>, so the compact
   `.fleet-detail-actions button` metrics above don't reach it and
   the Rebuild pair rendered taller than its sibling buttons (found
   live in the VM flyout). Same fix td.fleet-actions already carries. */
.fleet-detail-actions .split-menu > summary {
  padding: 0.3em 0.55em;
  font-size: 0.82em;
  background: transparent;
}
.fleet-detail-actions button.danger:hover {
  background: var(--danger, #c33);
  border-color: var(--danger, #c33);
  color: #fff;
}

/* Flyout Hardware + Network summary cards — parses live.config
   into a human-friendly two-column block. Stacks under 50rem so
   the narrow flyout (~28rem on small screens) stays readable. */
.flyout-summary {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.75rem;
  margin: 0.6em 0;
}
.flyout-summary-card { margin: 0; padding: 0.6em 0.8em; }
.flyout-summary-card h4 { margin: 0 0 0.3em; font-size: 0.95em; }
.flyout-summary-card dl.meta { margin: 0; }
.flyout-summary-card dl.meta dt { font-size: 0.82em; opacity: 0.75; }
.flyout-summary-card dl.meta dd { margin: 0 0 0.4em; }
.flyout-disk-row {
  display: flex; gap: 0.4em; align-items: baseline;
  font-size: 0.85em; margin-bottom: 0.2em;
}
.flyout-disk-row .truncate { max-width: 18em; }
.flyout-iso-row {
  display: flex; gap: 0.4em; align-items: baseline; flex-wrap: wrap;
}
.flyout-iso-row .truncate { max-width: 14em; }
.flyout-iso-eject { display: inline-block; margin-left: auto; }
.flyout-iso-eject button { font-size: 0.78em; padding: 0.15em 0.5em; }
.flyout-declared-strip {
  margin: 0.4em 0 1em;
  padding: 0.4em 0.6em;
  background: var(--accent-glow);
  border-radius: 4px;
  font-size: 0.9em;
}
.flyout-declared-strip .role-chip { vertical-align: middle; }

/* FLYOUT-DESC-1: the declared `description` is free-form prose, not a
   label. It used to ride the end of the one-line chip strip above,
   producing an unbroken wall of text the full width of the flyout.
   Own block, own measure, author's paragraphs kept. */
.flyout-desc {
  display: flex;
  gap: 0.5em;
  margin: -0.6em 0 1em;
  padding: 0.6em 0.75em;
  background: var(--surface-2, rgba(128, 128, 128, 0.06));
  border-left: 3px solid var(--accent);
  border-radius: 0 4px 4px 0;
}
.flyout-desc > svg { flex: none; margin-top: 0.15em; opacity: 0.7; }
.flyout-desc-body { min-width: 0; }          /* let long tokens wrap */
.flyout-desc-body p {
  margin: 0 0 0.6em;
  max-width: 68ch;                            /* readable measure */
  font-size: 0.9em;
  line-height: 1.5;
  color: var(--text-muted, inherit);
  overflow-wrap: anywhere;                    /* URLs/MACs can't overflow */
}
.flyout-desc-body p:last-child { margin-bottom: 0; }

/* FLYOUT-CARDS: the probe-backed sections (Reach / Harness key /
   Containers / Backups / Stack / History) each get the same .card
   chrome as the Hardware/Network summary cards above them, so the
   flyout reads as a tidy stack of cards instead of a flat run of
   bare <h4> sections. Tighter padding than a page card — the flyout
   is a dense surface. */
.flyout-card {
  padding: 0.7em 0.9em;
  margin: 0 0 0.75rem;
}
.flyout-card:last-child { margin-bottom: 0; }
/* Header row: the section h4 on the left, its single primary action
   (Refresh / Probe key) pushed to the right. */
.flyout-card-hdr {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6em;
  margin-bottom: 0.4em;
}
.flyout-card-hdr h4 {
  margin: 0;
  font-size: 0.95em;
}
.flyout-card-hdr .flyout-card-action { margin: 0; }
.flyout-card-hdr .flyout-card-action button { white-space: nowrap; }

/* Collapsible flyout cards (Containers / Backups / Stack / History).
   The heavy sections default collapsed so the panel opens short; the
   operator expands only what they need. A caret toggle sits first in
   the header; the h4 stays clickable-adjacent. Everything BELOW the
   header is hidden while `.is-collapsed`. Open-state is preserved
   across the 5s morph refresh by the flyout JS (a per-card key set). */
.flyout-card-toggle {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  margin-right: 0.35rem;
  padding: 0;
  background: none;
  border: none;
  cursor: pointer;
  color: var(--text-muted);
  border-radius: var(--radius-sm);
}
.flyout-card-toggle:hover { background: var(--accent-glow); color: var(--accent); }
.flyout-card-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* Pure-CSS caret (no icon dependency): a right-pointing triangle that
   rotates to point down when the card is open. */
.flyout-card-toggle::before {
  content: "";
  display: inline-block;
  width: 0;
  height: 0;
  border-top: 4px solid transparent;
  border-bottom: 4px solid transparent;
  border-left: 6px solid currentColor;
  transition: transform 0.15s ease;
}
.flyout-card:not(.is-collapsed) > .flyout-card-hdr .flyout-card-toggle::before {
  transform: rotate(90deg);
}
/* The header itself becomes the click affordance. */
.flyout-card[data-collapsible] > .flyout-card-hdr { cursor: pointer; }
/* Hide the body when collapsed — everything in the card except its header. */
.flyout-card.is-collapsed > :not(.flyout-card-hdr) { display: none; }
/* Collapsed cards need no bottom margin gap beyond the compact stack. */
.flyout-card.is-collapsed { padding-bottom: 0.5em; }

/* BACKUP-1: the Backups card's action buttons (Refresh / Take now /
 * Restore latest) each live in their own <form> — block-level, so
 * without this they stack. Lay the forms out in a wrapping row. */
.backups-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  align-items: center;
  margin: 0.2rem 0 0.6rem;
}
.backups-actions form { margin: 0; }
/* A small count chip beside the section title (containers / snapshots /
   events) so the operator sees the size before reading the table. */
.flyout-card-count {
  display: inline-block;
  min-width: 1.4em;
  padding: 0 0.4em;
  margin-left: 0.1em;
  border-radius: 999px;
  background: var(--accent-glow);
  color: var(--text-secondary);
  font-size: 0.78em;
  font-weight: 600;
  text-align: center;
  vertical-align: middle;
}
/* Tables inside a flyout card shouldn't add their own top margin —
   the card padding already spaces them off the header. */
.flyout-card > table.grid,
.flyout-card > dl { margin-top: 0.2em; }

@media (max-width: 50rem) {
  .flyout-summary { grid-template-columns: 1fr; }
  /* On the narrow flyout, let the action wrap under the title rather
     than squeezing both onto one cramped line. */
  .flyout-card-hdr { flex-wrap: wrap; }
}

/* Job-action toasts — fixed-position stack bottom-right. Each toast
   is a self-contained card with a status pill that morphs live as
   the job progresses; on success it auto-dismisses, on failure it
   sticks until the operator clicks ×. Replaces the old "POST →
   303 → /ui/jobs/<id>" redirect that ripped operators out of
   context. The harness lives in base.html.j2. */
#toast-stack {
  position: fixed;
  bottom: 1rem;
  right: 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.5em;
  z-index: 1000;
  pointer-events: none;   /* let clicks pass through gaps */
  max-width: min(28rem, calc(100vw - 2rem));
}
.job-toast {
  pointer-events: auto;
  /* `--bg-card` doesn't exist in this codebase — the canonical
     surface token is `--card`. The old rule fell through to
     unset and the toast read as transparent against the page
     bg. Use `--card` + an opaque fallback so dark + light both
     render solidly. */
  background: var(--card, var(--bg-elevated, #1a2940));
  color: var(--text-primary, inherit);
  border: 1px solid var(--border-light, rgba(255,255,255,0.12));
  border-radius: 6px;
  padding: 0.6em 0.8em;
  box-shadow: 0 6px 18px rgba(0,0,0,0.35);
  animation: job-toast-in 0.18s ease-out;
}
.job-toast[data-terminal="failed"],
.job-toast[data-terminal="refused"] {
  border-color: var(--danger, #c33);
}
.job-toast-hdr {
  display: flex;
  align-items: center;
  gap: 0.5em;
  margin: 0 0 0.3em;
  font-size: 0.9em;
}
.job-toast-title { flex: 1; min-width: 0; }
.job-toast-link {
  font-family: var(--mono, monospace);
  font-size: 0.85em;
  margin-left: 0.3em;
  opacity: 0.85;
}
.job-toast-close {
  background: transparent;
  border: 0;
  font-size: 1.2em;
  line-height: 1;
  padding: 0 0.2em;
  cursor: pointer;
  opacity: 0.6;
}
.job-toast-close:hover { opacity: 1; }
.job-toast-status { margin: 0; font-size: 0.85em; }
@keyframes job-toast-in {
  from { transform: translateY(0.5em); opacity: 0; }
  to   { transform: translateY(0);     opacity: 1; }
}

/* Housekeeping tab — grid of per-store cards. Each card carries
 * its own action row (Vacuum / Prune / Truncate / etc). The grid
 * uses auto-fit so the layout collapses cleanly on narrow viewports
 * without a media query. */
.hk-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
  gap: 0.8rem;
  margin-top: 0.6em;
}
.hk-store { margin: 0; padding: 0.8em 1em; }
.hk-store h3 { margin-top: 0; }
.hk-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35em;
  margin-top: 0.6em;
  align-items: center;
}
.hk-actions form {
  display: inline-flex;
  align-items: center;
  gap: 0.3em;
  margin: 0;
}

/* Per-row action cell for the Schedules / Notifications / Hooks tables.
   Each action is its own <form> (a block element), so without this they
   stacked vertically. Lay them out side-by-side like every other
   row-action cell on the site (mirrors .hk-actions). */
.sched-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35em;
  align-items: center;
}
.sched-actions form {
  display: inline-flex;
  align-items: center;
  margin: 0;
}
.hk-days {
  font-size: 0.82em;
  color: var(--text-muted);
  display: inline-flex;
  gap: 0.3em;
  align-items: center;
}
.hk-truncate summary {
  list-style: none;
  cursor: pointer;
}
.hk-truncate[open] summary { margin-bottom: 0.4em; }

/* USERS-1: user-card action area — the mint form and the account
   controls each get their own row with real gaps (one flat
   .hk-actions line crammed six controls together). */
.us-actions {
  margin-top: 0.9em;
  padding-top: 0.8em;
  border-top: 1px solid var(--border-light);
  display: flex;
  flex-direction: column;
  gap: 0.65em;
}
.us-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5em 0.8em;
  align-items: center;
}
.us-row form {
  display: inline-flex;
  align-items: center;
  gap: 0.45em;
  margin: 0;
  flex-wrap: wrap;
}
.us-row .us-spacer { flex: 1 1 auto; }
.us-row-label {
  flex: 0 0 4.5rem;
  font-size: 0.72rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-muted);
}

/* -------------------------------------------------------------------------
   WIZ-UX-1: shared button hierarchy.
   Tokens-only — inherits the dark default + the light override on
   `[data-theme="light"]` for free.
   ------------------------------------------------------------------------- */
/* Button rows under forms / above tables. Was unstyled — templates
   carried their own inline flex; this is the shared baseline. */
.form-actions {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
  align-items: center;
}
.projects-actions { margin-bottom: 1rem; }

.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.5rem 0.95rem;
  border: 1px solid var(--border-light);
  border-radius: var(--radius);
  font: inherit;
  font-weight: 500;
  text-decoration: none;
  background: var(--bg-surface);
  color: var(--text-primary);
  cursor: pointer;
  transition: background-color var(--transition-fast),
              border-color var(--transition-fast),
              box-shadow var(--transition-fast);
}
.btn:hover {
  background: var(--bg-surface-hover);
  border-color: var(--accent-dim);
}
.btn:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}
.btn[disabled],
.btn[aria-disabled="true"] {
  opacity: 0.55;
  cursor: not-allowed;
}

/* `.btn-primary` competes with `main a` (line ~686) for `<a>` links
   used as buttons. `main a` has element-only specificity (0,0,2)
   while a single class is (0,1,0), so .btn-primary normally wins on
   specificity. Some pages wrap the button in a more specific
   context (`.card`, `.dashboard-grid`, the wizard chrome) where
   inherited link rules can still leak through, so we double-specify
   on the anchor case below. */
.btn-primary,
a.btn-primary,
main a.btn-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--bg-deep);
}
.btn-primary:hover,
a.btn-primary:hover,
main a.btn-primary:hover {
  background: var(--accent-dim);
  border-color: var(--accent-dim);
  color: var(--bg-deep);
}

.btn-secondary {
  background: transparent;
}

/* Canonical destructive button. Use `class="btn btn-danger"` (add
   `btn-sm` for the compact row variant) for every delete / destroy /
   truncate action so they're a consistent full-red button site-wide.
   `.danger` (bare class, incl. `button.danger`) is kept as an alias so
   older markup renders identically — prefer `btn btn-danger` in new
   templates. The anchor guard mirrors `.btn-primary`: `<a>` used as a
   button needs the extra specificity to beat the `main a` link rules. */
.btn-danger,
a.btn-danger,
main a.btn-danger,
.btn.danger {
  background: var(--danger);
  border-color: var(--danger);
  color: var(--bg-deep);
}
.btn-danger:hover,
a.btn-danger:hover,
main a.btn-danger:hover,
.btn.danger:hover {
  filter: brightness(0.92);
}

/* Amber "caution, but not destructive" button — a big/beta operation that
   is nonetheless SAFE for data (e.g. in-place OS upgrade: keeps root+data,
   just reboots). Distinct from btn-danger (red = destructive/wipes). */
.btn-warn,
a.btn-warn,
main a.btn-warn {
  background: var(--warn);
  border-color: var(--warn);
  color: var(--bg-deep);
}
.btn-warn:hover,
a.btn-warn:hover,
main a.btn-warn:hover {
  filter: brightness(0.92);
}

/* -------------------------------------------------------------------------
   WIZ-UX-1: multi-step wizard chrome
   Used by `_wizard.html.j2` + `_wizard_body.html.j2` for every wizard
   (PROJ-CREATE-1 first, future TOPO-1/etc inherit it). Visual rhythm
   mirrors the dashboard cards: a single `.card` shell, a header, a
   progress strip, then the active pane's body and a sticky-feel footer.
   ------------------------------------------------------------------------- */
/* The wizard card spans the full content width like every other
   page; the progress strip + step body stay a readable form column. */
.wizard {
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
}
.wizard .wizard-progress,
.wizard #wizard-body { max-width: 56rem; }
.wizard-hdr h1 { margin: 0; }

/* Section breadcrumb (cfg-head) — "Projects › Create" above the h1. */
.cfg-head .crumb {
  margin: 0 0 0.15rem;
  font-size: 0.8rem;
  color: var(--text-secondary);
}
.cfg-head .crumb a { color: inherit; }
.cfg-head .crumb a:hover { color: var(--accent); }

.wizard-progress {
  display: flex;
  list-style: none;
  margin: 0;
  padding: 0.55rem 0.55rem;
  gap: 0.25rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-elevated);
  flex-wrap: wrap;
}
.wizard-progress-step {
  flex: 1 1 auto;
  min-width: 9rem;
  border-radius: var(--radius-sm);
  padding: 0.55rem 0.75rem;
  display: flex;
  align-items: center;
  gap: 0.55rem;
  color: var(--text-muted);
  font-size: 0.92rem;
}
.wizard-progress-step a {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  color: inherit;
  text-decoration: none;
  width: 100%;
}
.wizard-progress-step a:hover {
  background: var(--bg-surface-hover);
  color: var(--text-primary);
}
.wizard-progress-num {
  flex: 0 0 1.6rem;
  height: 1.6rem;
  border-radius: 999px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--bg-surface);
  border: 1px solid var(--border-light);
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}
.wizard-progress-label { flex: 1 1 auto; }

.wizard-progress-active {
  background: var(--accent-glow);
  color: var(--accent);
  font-weight: 600;
}
.wizard-progress-active .wizard-progress-num {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--bg-deep);
}

.wizard-progress-done {
  color: var(--text-primary);
}
.wizard-progress-done .wizard-progress-num {
  background: transparent;
  border-color: var(--success);
  color: var(--success);
  font-size: 0;
}
.wizard-progress-done .wizard-progress-num::before {
  content: "✓";
  font-size: 1rem;
}

.wizard-progress-todo { opacity: 0.7; }

.wizard-form {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}
.wizard-form label {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}
.wizard-form label > span:first-child {
  font-weight: 500;
  color: var(--text-primary);
}
.wizard-form input[type="text"],
.wizard-form input[type="email"],
.wizard-form input[type="number"],
.wizard-form input[type="password"],
.wizard-form select,
.wizard-form textarea {
  padding: 0.55rem 0.7rem;
  border: 1px solid var(--border-light);
  border-radius: var(--radius-sm);
  font: inherit;
  background: var(--bg-surface);
  color: var(--text-primary);
}
.wizard-form input:focus-visible,
.wizard-form select:focus-visible,
.wizard-form textarea:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: var(--focus-ring);
}
.wizard-form small.hint {
  color: var(--text-muted);
  font-size: 0.85rem;
}
.wizard-form .field-error {
  color: var(--danger);
  font-size: 0.85rem;
}

.wizard-footer {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding-top: 0.5rem;
  border-top: 1px solid var(--border);
}
.wizard-footer-spacer { flex: 1 1 auto; }

.wizard-cancel-form {
  margin-top: 0.5rem;
  text-align: right;
}
.wizard-cancel-link {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  color: var(--text-muted);
  text-decoration: underline;
  cursor: pointer;
}
.wizard-cancel-link:hover { color: var(--danger); }

@media (max-width: 36rem) {
  .wizard-progress-label { display: none; }
  .wizard-progress-step { min-width: 0; }
}

/* -------------------------------------------------------------------------
   WIZ-UX-1-MR2: project-create wizard specifics
   ------------------------------------------------------------------------- */
.starter-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: 0.75rem;
  border: none;
  padding: 0;
}
.starter-card {
  display: flex;
  gap: 0.65rem;
  align-items: flex-start;
  border: 1.5px solid var(--border-light);
  border-radius: var(--radius);
  padding: 0.85rem 0.95rem;
  cursor: pointer;
  background: var(--bg-surface);
  color: var(--text-primary);
  transition: border-color var(--transition-fast),
              background-color var(--transition-fast),
              box-shadow var(--transition-fast);
}
.starter-card:hover {
  background: var(--bg-surface-hover);
  border-color: var(--accent-dim);
}
.starter-card input[type="radio"] {
  margin: 0.15rem 0 0 0;
}
.starter-card-body {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}
.starter-card-title { font-weight: 600; }
.starter-card-summary {
  color: var(--text-muted);
  font-size: 0.9rem;
}
.starter-card-selected {
  border-color: var(--accent);
  background: var(--accent-glow);
  box-shadow: 0 0 0 1px var(--accent);
}

.wizard-review {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 0.55rem 1rem;
  margin: 0;
}
.wizard-review dt {
  color: var(--text-muted);
  font-size: 0.92rem;
}
.wizard-review dd {
  margin: 0;
  display: flex;
  align-items: baseline;
  gap: 0.55rem;
  flex-wrap: wrap;
  color: var(--text-primary);
}
.wizard-review-edit {
  font-size: 0.82rem;
  color: var(--accent);
  text-decoration: none;
}
.wizard-review-edit:hover { text-decoration: underline; }
.wizard-review-edit::before { content: "↪ "; }

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

/* REACH-1d/e: structured-error modal. Shown when a fleet
 * lifecycle POST returns 4xx with a known structured `code`
 * (bastion_unreachable, bastion_cascade_confirm_required) and the
 * UI's fetch-intercept needs to ask the operator how to proceed.
 */
.reach-modal-backdrop {
  position: fixed; inset: 0;
  background: rgba(0, 0, 0, 0.5);
  z-index: 2000;
  display: flex; align-items: center; justify-content: center;
  padding: 1rem;
}
.reach-modal {
  background: var(--card, #fff);
  color: var(--ink, #111);
  border: 1px solid var(--rule, #ccc);
  border-radius: 6px;
  padding: 1.5rem;
  max-width: 36rem; width: 100%;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.25);
}
.reach-modal h2 { margin: 0 0 0.5rem 0; font-size: 1.1em; }
.reach-modal .reach-modal-body { margin: 0.75rem 0 1rem 0; font-size: 0.95em; }
.reach-modal .reach-modal-body code {
  font-size: 0.9em; padding: 0.1em 0.35em;
  background: var(--surface-alt, #f2f2f2); border-radius: 3px;
}
.reach-modal .reach-modal-list {
  margin: 0.5em 0 0.75em 1.2em; padding: 0; font-size: 0.9em;
}
.reach-modal .reach-modal-actions {
  display: flex; gap: 0.5rem; flex-wrap: wrap;
  justify-content: flex-end; margin-top: 1rem;
}
.reach-modal .reach-modal-actions button {
  padding: 0.45em 0.9em; font-size: 0.95em; border-radius: 4px;
  border: 1px solid var(--rule, #ccc);
  background: var(--surface-alt, #f2f2f2);
  color: var(--ink, #111); cursor: pointer;
}
.reach-modal .reach-modal-actions button.reach-modal-primary {
  background: var(--accent, #00e5e3); color: #fff; border-color: transparent;
}
.reach-modal .reach-modal-actions button.reach-modal-danger {
  background: #dc2626; color: #fff; border-color: transparent;
}
.reach-modal .reach-modal-actions button:focus-visible {
  outline: 2px solid var(--accent, #00e5e3); outline-offset: 1px;
}
.reach-modal-confirm-input {
  margin: 0.5em 0; padding: 0.4em 0.6em;
  font-family: monospace; font-size: 0.95em;
  width: 100%; box-sizing: border-box;
  border: 1px solid var(--rule, #ccc); border-radius: 4px;
}

/* --- UI-SCALE-1: shared list controls (search + facet chips + pager) -- */
.list-controls { margin: 0.6em 0 0.8em; }
.list-controls .list-search {
  display: flex; align-items: center; gap: 0.5em; margin-bottom: 0.4em;
}
.list-controls .list-search input[type="search"] {
  padding: 0.35em 0.6em; min-width: 16em;
  border: 1px solid var(--rule, #ccc); border-radius: 4px;
  background: inherit; color: inherit;
}
.list-controls .list-facets,
.list-controls .list-pager { margin: 0.3em 0; }
.list-controls .list-pager a[aria-disabled="true"] {
  opacity: 0.4; pointer-events: none;
}

/* --- UI-SCALE-1-MR4: topbar search palette ---------------------------- */
/* Sits AFTER the icon cluster; NO auto margins — the icons stay on
 * the deliberate left side and the topbar's right stays empty so the
 * global flyout's slide-in never covers live controls. */
.topbar .palette { position: relative; flex: 0 1 22rem; margin-left: 0.75em; }
.topbar .palette input[type="search"] {
  width: 100%; padding: 0.35em 0.6em;
  border: 1px solid var(--rule, #ccc); border-radius: 4px;
  background: inherit; color: inherit;
}
.palette-results {
  position: absolute; top: 100%; left: 0; right: 0; z-index: 60;
}
.palette-list {
  list-style: none; margin: 0.2em 0 0; padding: 0.25em;
  background: var(--card-bg, var(--bg, #fff));
  border: 1px solid var(--rule, #ccc); border-radius: 6px;
  box-shadow: 0 6px 18px rgba(0,0,0,0.25);
  max-height: 60vh; overflow-y: auto;
}
.palette-list li { margin: 0; }
.palette-hit {
  display: flex; align-items: center; gap: 0.5em;
  padding: 0.35em 0.5em; border-radius: 4px; text-decoration: none;
}
.palette-hit:hover, .palette-hit:focus {
  background: var(--accent-soft, rgba(127,127,255,0.15));
}
.palette-empty {
  margin: 0.2em 0 0; padding: 0.4em 0.6em;
  background: var(--card-bg, var(--bg, #fff));
  border: 1px solid var(--rule, #ccc); border-radius: 6px;
}

/* Admin Schedules/Notifications/Hooks tabs (UI-TRIAD-POLISH).
 * .summary-chip retired by UI-DS Slice 4 (no remaining callers); the
 * click-to-copy button below is still live. */
.copy-btn {
  font: inherit;
  font-size: 0.78rem;
  line-height: 1.4;
  padding: 0.05rem 0.45rem;
  margin-left: 0.4rem;
  cursor: pointer;
  border-radius: 6px;
  border: 1px solid var(--border, rgba(127,127,127,0.35));
  background: transparent;
  color: var(--text-secondary);
  vertical-align: baseline;
}
.copy-btn:hover { border-color: var(--accent, #7cc); color: var(--text); }
.copy-btn.copy-ok {
  border-color: var(--ok, #3a9);
  color: var(--ok, #3a9);
}
/* The hook ingress URL + its copy button share a row that can wrap on
 * narrow viewports without breaking the table cell. */
.hook-ingress { display: inline-flex; align-items: baseline; flex-wrap: wrap; gap: 0.15rem; }
.hook-ingress code { word-break: break-all; }

/* TOPO-2-MR3: VM path trace ("dancing ants"). Clicking a VM dims the
 * rest of the graph and animates the path edges with a marching-ants
 * stroke flow so the operator can SEE the wire a VM's traffic takes. */
.topo-dimmed { opacity: 0.18; transition: opacity 0.2s; }
.topo-path-node > rect,
.topo-path-node > circle,
.topo-path-node > ellipse {
  stroke: var(--accent, #7cc) !important;
  stroke-width: 2.5px !important;
}
.topo-path-edge path {
  stroke: var(--accent, #7cc) !important;
  stroke-width: 2.5px !important;
  stroke-dasharray: 6 4 !important;
  animation: topo-ants 0.6s linear infinite;
}
@keyframes topo-ants {
  to { stroke-dashoffset: -10; }
}
@media (prefers-reduced-motion: reduce) {
  .topo-path-edge path { animation: none; }
}
ol.topo-hops { margin: 0.2rem 0 0.6rem 1.1rem; padding: 0; }
ol.topo-hops li { margin: 0.15rem 0; font-size: 0.85rem; }

/* CONN-STRING-1: SSH connection-string card in the VM flyout. The
 * marching-ant path mirrors the topology trace so the operator reads
 * the same "wire" metaphor — control-plane → bastion → VM. Each
 * conn-row pairs a copyable <code> with the shared .copy-btn. */
.conn-path {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.25rem;
  margin: 0.1rem 0 0.6rem;
  font-size: 0.82rem;
}
.conn-hop {
  padding: 0.05rem 0.45rem;
  border-radius: 6px;
  background: var(--surface-2, rgba(127,127,127,0.14));
  border: 1px solid var(--border, rgba(127,127,127,0.25));
  white-space: nowrap;
}
.conn-hop-origin { color: var(--text-secondary); }
.conn-hop-target {
  border-color: var(--accent, #7cc);
  color: var(--text);
}
/* The animated dashes between hops — same flow direction + cadence as
 * the topology trace's topo-ants keyframe. */
.conn-ant {
  flex: 0 0 1.6rem;
  height: 0;
  border-top: 2px dashed var(--accent, #7cc);
  animation: topo-ants 0.6s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  .conn-ant { animation: none; }
}
.conn-row { margin: 0.35rem 0; }
.conn-row-label {
  font-size: 0.82rem;
  font-weight: 500;
  margin-bottom: 0.15rem;
}
.conn-row-label small.hint { font-weight: 400; margin-left: 0.4rem; }
.conn-row-cmd {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.3rem;
}
.conn-row-cmd code {
  flex: 1 1 auto;
  word-break: break-all;
  padding: 0.15rem 0.4rem;
  border-radius: 6px;
  background: var(--surface-2, rgba(127,127,127,0.1));
}

/* --- Admin schedules cron presets + notify multi-select (UI polish) --- */
.cron-presets {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
  margin: 0.35rem 0 0.2rem;
}
.btn-mini {
  font-size: 0.78rem;
  padding: 0.12rem 0.5rem;
  border: 1px solid var(--border, #444);
  border-radius: 0.35rem;
  background: var(--surface-2, #2a2a2a);
  color: var(--text, #ddd);
  cursor: pointer;
  line-height: 1.4;
}
.btn-mini:hover { border-color: var(--accent, #7cc); color: var(--text); }
.cron-desc { display: block; margin-top: 0.25rem; font-style: italic; }

.ntf-any { display: inline-flex; align-items: center; gap: 0.3rem;
           margin-bottom: 0.3rem; font-weight: 600; }
.ntf-checks {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 0.9rem;
  padding: 0.3rem 0.4rem;
  border: 1px solid var(--border, #444);
  border-radius: 0.4rem;
  background: var(--surface-2, #242424);
  max-height: 9rem;
  overflow-y: auto;
}
.ntf-checks.is-disabled { opacity: 0.45; }
.ntf-chk { display: inline-flex; align-items: center; gap: 0.3rem;
           font-size: 0.85rem; white-space: nowrap; }

/* ==========================================================================
 * UI-KIT-1: Component kit (_kit.html.j2)
 *
 * Shared building blocks, styled on the design tokens above. The point
 * is affordance: a collapsible section must LOOK collapsible (header
 * bar, chevron, hover), not read as a bare line of text with a
 * browser-default triangle — the exact confusion that hid the Configs
 * tab's context panels from the operator.
 * ========================================================================== */

/* --- kit-panel: collapsible card ---------------------------------------- */
details.kit-panel {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 8px;
  margin: 0.6rem 0;
  overflow: hidden;
}
details.kit-panel > summary {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.55rem 0.9rem;
  cursor: pointer;
  user-select: none;
  list-style: none;             /* drop the default marker… */
  background: var(--bg-surface);
  transition: background 0.12s ease;
}
details.kit-panel > summary::-webkit-details-marker { display: none; }
details.kit-panel > summary::before {              /* …and draw our own */
  content: "";
  width: 0.55em;
  height: 0.55em;
  flex: 0 0 auto;
  border-right: 2px solid var(--text-secondary);
  border-bottom: 2px solid var(--text-secondary);
  transform: rotate(-45deg);    /* ▸ closed */
  transition: transform 0.15s ease;
  margin-right: 0.1rem;
}
details.kit-panel[open] > summary::before {
  transform: rotate(45deg);     /* ▾ open */
}
details.kit-panel > summary:hover {
  background: var(--bg-surface-hover);
}
details.kit-panel[open] > summary {
  border-bottom: 1px solid var(--border);
}
.kit-panel-title { font-weight: 600; color: var(--text-primary); }
.kit-panel-title code { font-size: 0.95em; }
.kit-panel-meta {
  color: var(--text-muted);
  font-size: 0.85em;
  margin-left: auto;
  text-align: right;
}
.kit-panel-body { padding: 0.75rem 0.9rem; }
/* Header tints for posture-carrying panels. */
details.kit-panel--bad  > summary { box-shadow: inset 3px 0 0 var(--danger); }
details.kit-panel--warn > summary { box-shadow: inset 3px 0 0 var(--accent-alt); }
details.kit-panel--ok   > summary { box-shadow: inset 3px 0 0 var(--success); }
/* Nested panels sit flatter — one card level is enough. */
.kit-panel-body details.kit-panel { background: transparent; }

/* --- kit-actions: inline control cluster --------------------------------- */
.kit-actions {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  flex-wrap: wrap;
  vertical-align: middle;
}

/* --- kit-rows: list of entity rows (credential keys etc.) ---------------- */
ul.kit-rows { list-style: none; padding: 0; margin: 0.25rem 0; }
ul.kit-rows > li {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
  padding: 0.35rem 0.2rem;
  border-bottom: 1px solid var(--border);
}
ul.kit-rows > li:last-child { border-bottom: none; }
ul.kit-rows > li > code:first-child { min-width: 14rem; }

/* --- kit-tree: nested YAML tree ------------------------------------------ */
ul.kit-tree, ul.kit-tree ul {
  list-style: none;
  margin: 0.15rem 0;
  padding-left: 0;
}
ul.kit-tree ul {
  padding-left: 1.1rem;
  border-left: 1px solid var(--border);
  margin-left: 0.35rem;
}
ul.kit-tree li { padding: 0.12rem 0; }
ul.kit-tree details { display: block; }
ul.kit-tree details > summary {
  cursor: pointer;
  user-select: none;
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.1rem 0.3rem;
  border-radius: 4px;
  transition: background 0.12s ease;
}
ul.kit-tree details > summary::-webkit-details-marker { display: none; }
ul.kit-tree details > summary::before {
  content: "";
  width: 0.4em;
  height: 0.4em;
  flex: 0 0 auto;
  border-right: 2px solid var(--text-muted);
  border-bottom: 2px solid var(--text-muted);
  transform: rotate(-45deg);
  transition: transform 0.15s ease;
}
ul.kit-tree details[open] > summary::before { transform: rotate(45deg); }
ul.kit-tree details > summary:hover { background: var(--accent-bg-tint); }
/* The inline "edit" affordance on tree leaves keeps its button look. */
ul.kit-tree details.kit-inline { display: inline-block; }
ul.kit-tree details.kit-inline > summary::before { content: none; }

/* --- kit-inline: inline expandable (Update / edit affordances) ----------- */
details.kit-inline { display: inline-block; vertical-align: middle; }
details.kit-inline > summary {
  list-style: none;
  cursor: pointer;
  user-select: none;
}
details.kit-inline > summary::-webkit-details-marker { display: none; }
details.kit-inline[open] > summary { opacity: 0.7; }

/* --- kit-fold: lightweight inline disclosure (UI-KIT-2) ------------------ */
/* For table cells and small in-flow folds where a full kit-panel card is
 * too heavy: drawn chevron + hover tint, no card chrome. */
details.kit-fold > summary {
  cursor: pointer;
  user-select: none;
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.1rem 0.3rem;
  border-radius: 4px;
  transition: background 0.12s ease;
}
details.kit-fold > summary::-webkit-details-marker { display: none; }
details.kit-fold > summary::before {
  content: "";
  width: 0.4em;
  height: 0.4em;
  flex: 0 0 auto;
  border-right: 2px solid var(--text-muted);
  border-bottom: 2px solid var(--text-muted);
  transform: rotate(-45deg);
  transition: transform 0.15s ease;
}
details.kit-fold[open] > summary::before { transform: rotate(45deg); }
details.kit-fold > summary:hover { background: var(--accent-bg-tint); }

/* Artefacts tab: the Checksum + Nodes cells are single click-to-copy pills
 * (data-copy handler in base.html.j2), NOT inline expanders. Nothing grows
 * on interaction, so the table keeps its natural content-driven auto layout
 * and just fills the available width — no fixed-layout caps needed. A pill
 * carrying data-copy reads as interactive (pointer + hover lift); clicking
 * copies the digest / node list and the pill briefly flashes "copied". */
.status-pill[data-copy] { cursor: pointer; transition: filter 0.1s ease; }
.status-pill[data-copy]:hover { filter: brightness(1.1); }
.status-pill.copy-ok {
  background: var(--success-bg);
  color: var(--success);
}

/* --- LIST-CHIP-1: facet filter pills (shared list controls) ------------- */
/* The list_controls facet strip. These were class="reach-chip" with NO
 * matching CSS — they rendered as bare text links, so active filters
 * were invisible and clearing a drill-down meant un-clicking each one.
 * Now: bordered toggle pills; active = accent fill + ×; a leading
 * "× clear filters" pill drops every facet in one click. */
.list-facets { display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: center; }
.list-chip {
  display: inline-flex; align-items: center; gap: 0.3em;
  padding: 0.12rem 0.65rem;
  border: 1px solid var(--rule, rgba(127,127,127,0.4));
  border-radius: 999px;
  font-size: 0.82rem; line-height: 1.4;
  text-decoration: none; color: inherit;
  transition: border-color 0.12s ease, background 0.12s ease;
}
.list-chip:hover { border-color: var(--accent, #6ea8fe); }
.list-chip-count {
  font-size: 0.72rem; opacity: 0.7; font-variant-numeric: tabular-nums;
}
.list-chip.is-active {
  background: var(--accent, #6ea8fe);
  border-color: var(--accent, #6ea8fe);
  color: var(--bg, #0d1420);
  font-weight: 600;
}
.list-chip.is-active .list-chip-count { opacity: 0.85; }
.list-chip-x { font-weight: 700; }
.list-chip--clear {
  border-style: dashed;
  color: var(--text-muted, inherit);
}
.list-chip--clear:hover {
  border-color: var(--bad, #e05252);
  color: var(--bad, #e05252);
}

/* ------------------------------------------------------------------ *
 * Config-section tab surfaces (OPERATOR-BACKUP-1 / SSH-CONFIG-1).
 * A scan-first treatment for the read-only Backup + SSH tabs: a strip
 * of stat tiles up top (summary before detail), an accent-rail posture
 * callout, and copyable command rows. Reuses the existing token
 * system + the .copy-btn / conn-row clipboard wiring in base.html.j2;
 * only the layout scaffolding below is new.
 * ------------------------------------------------------------------ */

/* Header row: an icon-chip title + a lede that shares the baseline. */
.cfg-head {
  display: flex;
  align-items: flex-start;
  gap: var(--space-md);
  margin: 0 0 var(--space-lg);
}
.cfg-head-icon {
  flex: none;
  display: grid;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: var(--radius-lg);
  background: var(--accent-glow);
  color: var(--accent);
  border: 1px solid var(--accent-glow-md);
}
.cfg-head-icon svg { width: 1.4rem; height: 1.4rem; }
.cfg-head-text { min-width: 0; }
.cfg-head-text h1,
.cfg-head-text h2 { margin: 0 0 0.2rem; font-size: 1.4rem; }
.cfg-head-text .lede { margin: 0; max-width: 62ch; }

/* Scan strip — 2-4 stat tiles surfacing the summary numbers. */
.cfg-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: var(--space-sm);
  margin: 0 0 var(--space-lg);
}
.cfg-stat {
  padding: var(--space-md);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--bg-elevated);
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}
.cfg-stat-value {
  font-size: 1.6rem;
  line-height: 1.1;
  font-weight: 650;
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}
.cfg-stat-label {
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.cfg-stat-sub { font-size: 0.78rem; color: var(--text-secondary); }
.cfg-stat--accent .cfg-stat-value { color: var(--accent); }
.cfg-stat--ok     .cfg-stat-value { color: var(--success); }
.cfg-stat--warn   .cfg-stat-value { color: var(--warn); }
.cfg-stat--bad    .cfg-stat-value { color: var(--danger); }

/* Accent-rail posture callout — "this write runs on your workstation". */
.cfg-note {
  display: flex;
  gap: var(--space-sm);
  align-items: flex-start;
  padding: var(--space-md);
  margin: 0 0 var(--space-lg);
  border-radius: var(--radius);
  border: 1px solid var(--border);
  border-left: 3px solid var(--accent);
  background: var(--accent-bg-tint);
  font-size: 0.9rem;
  color: var(--text-secondary);
}
.cfg-note svg { flex: none; width: 1.05rem; height: 1.05rem; color: var(--accent); margin-top: 0.1rem; }
.cfg-note strong { color: var(--text-primary); }

/* Section label — an eyebrow above each command / table block. */
.cfg-section-label {
  display: flex;
  align-items: baseline;
  gap: var(--space-sm);
  margin: var(--space-lg) 0 var(--space-sm);
  font-size: 0.8rem;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.cfg-section-label::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--border);
}
.cfg-section:first-of-type .cfg-section-label { margin-top: 0; }

/* Copyable command row — a labelled command with a one-click copy. */
.cfg-cmd {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding: 0.55rem 0.7rem;
  background: var(--bg-code);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  margin-bottom: var(--space-xs);
}
.cfg-cmd + .cfg-cmd { margin-top: 0; }
.cfg-cmd code {
  flex: 1;
  min-width: 0;
  overflow-x: auto;
  white-space: pre;
  font-size: 0.86rem;
  color: var(--text-primary);
  background: none;
  padding: 0;
}
.cfg-cmd .cfg-cmd-note {
  flex: none;
  font-size: 0.76rem;
  color: var(--text-muted);
}

/* Rendered include preview — a scrollable, framed code block. */
.cfg-render {
  margin: 0;
  max-height: 22rem;
  overflow: auto;
  padding: var(--space-md);
  background: var(--bg-code);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-size: 0.82rem;
  line-height: 1.5;
}
.cfg-render code { background: none; padding: 0; color: var(--text-secondary); }

.cfg-empty {
  padding: var(--space-lg);
  text-align: center;
  border: 1px dashed var(--border-light);
  border-radius: var(--radius-lg);
  color: var(--text-secondary);
  background: var(--bg-mid);
}
.cfg-empty svg { width: 1.6rem; height: 1.6rem; color: var(--text-muted); margin-bottom: var(--space-xs); }

/* Inline spinner — used for background work whose result fills in on a
   later poll (e.g. the Artefacts tab hashing a freshly-cached ISO). A
   small spinning ring that sits inline with text. */
@keyframes spinner-rotate { to { transform: rotate(360deg); } }
.spinner {
  display: inline-block;
  width: 0.8em;
  height: 0.8em;
  vertical-align: -0.1em;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: spinner-rotate 0.7s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  .spinner { animation: none; opacity: 0.6; }
}

/* ============================================================
   UI-DS Slice 1 — canonical component CSS
   Backs the new kit macros (status_pill / empty_state /
   page_header / kit_dl / kit_copy / op_form). Additive; pages
   adopt these in Slices 2-4. Built on the existing tokens.
   ============================================================ */

/* page_header — the ONE page header (promotes the .cfg-head idiom). */
.page-header { margin: 0 0 var(--space-lg); }
.page-header-title {
  font-family: var(--font-display);
  font-size: 1.5rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  margin: 0;
}
.page-header-lede {
  color: var(--text-secondary);
  margin: 0.35rem 0 0;
  max-width: 70ch;
}

/* empty_state — the ONE "nothing here" treatment. */
.empty-state {
  padding: var(--space-lg);
  text-align: center;
  color: var(--text-muted);
  background: var(--bg-mid);
  border: 1px dashed var(--border);
  border-radius: var(--radius-lg);
}
.empty-state-msg  { margin: 0; font-weight: 500; color: var(--text-secondary); }
.empty-state-hint { margin: 0.35rem 0 0; font-size: 0.88em; color: var(--text-muted); }

/* status pill: link variant (no underline). */
.status-pill--link { text-decoration: none; }

/* A status-pill rendered as an <a> (projects-list blueprint column, the
   status_pill(href=…) macro) must NOT inherit the text-link hover from
   `main a:hover` — that recoloured the pill text + underlined it, which
   read as a spurious "mouse-over effect". The pill's colour comes from
   its status-* class; keep it stable and only lift the whole chip a hair
   on hover so it still reads as clickable. */
main a.status-pill,
main a.status-pill:hover,
main a.status-pill:visited {
  text-decoration: none;
}
main a.status-pill:hover { filter: brightness(1.12); }
main a.status-pill { cursor: pointer; transition: filter var(--transition-fast); }

/* The pill's colour comes from its .status-* class (specificity 0,1,0),
   but `main a` (0,1,1) sets --accent and wins on the projects-list
   anchor pills — so the pill text rendered accent-blue in dark mode
   (light mode has a `[data-theme=light] .status-*` rule at 0,2,0 that
   already beats it, which is why only dark was wrong). Re-assert each
   status colour at anchor specificity so the pill keeps its own hue.
   These reference the SAME design-system tokens the .status-* classes
   use — `--success`/`--danger`/`--accent`/`--accent-alt`/`--muted` each
   resolve per-theme automatically, so one set of rules is correct in
   both dark and light (no hard-coded hex, no per-theme duplication). */
main a.status-pill.status-running,
main a.status-pill.status-running:visited    { color: var(--accent); }
main a.status-pill.status-succeeded,
main a.status-pill.status-succeeded:visited  { color: var(--success); }
main a.status-pill.status-failed,
main a.status-pill.status-failed:visited     { color: var(--danger); }
main a.status-pill.status-queued,
main a.status-pill.status-queued:visited,
main a.status-pill.status-cancelled,
main a.status-pill.status-cancelled:visited  { color: var(--accent-alt); }
main a.status-pill.status-declared,
main a.status-pill.status-declared:visited   { color: var(--muted); }

/* kit_dl — the ONE key/value definition list (canonical dl.meta). */
.kit-dl {
  display: grid;
  grid-template-columns: minmax(6rem, max-content) 1fr;
  gap: 0.4rem 1rem;
  margin: 0 0 var(--space-md);
}
.kit-dl dt { color: var(--text-muted); font-size: 0.9em; }
.kit-dl dd { margin: 0; }

/* kit_copy — inline text + copy button (reuses global .copy-btn). */
.kit-copy { display: inline-flex; align-items: center; gap: 0.3rem; }

/* form-inline / button-row — the ONE inline single-button form and the
   ONE horizontal button cluster (retires the ~6 per-feature action-row
   rules + the inline display:inline forms). */
.form-inline { display: inline-block; margin: 0; }
.button-row  { display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: center; }

/* popover — the ONE anchored-popover base. Feature popovers
   (split-menu / bastion / issue-dismiss) can compose off this. */
.popover {
  position: absolute;
  z-index: 20;
  min-width: 12rem;
  padding: 0.6rem 0.75rem;
  background: var(--bg-surface);
  border: 1px solid var(--border-light);
  border-radius: var(--radius);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
}

/* ============================================================
   Substrate login — the mycelium auth stage. Full-bleed
   atmospheric page for anonymous visitors (no sidebar chrome).
   The Canvas filament field is drawn by static/mycelium.js;
   everything here is the card + footer over it. All colour from
   the design tokens so it tracks the theme.
   ============================================================ */
.auth-stage {
  position: fixed;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background: var(--bg-abyss);
  overflow: hidden;
}
.auth-stage .myc-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  display: block;
}
/* radial vignette + a faint cyan bloom low-centre (spore glow) */
.auth-veil {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background:
    radial-gradient(120% 80% at 50% 118%, var(--accent-glow), transparent 55%),
    radial-gradient(100% 100% at 50% 0%, transparent 60%,
                    color-mix(in srgb, var(--bg-abyss) 70%, transparent) 100%);
}

.auth-card {
  position: relative;
  z-index: 2;
  width: min(26rem, 92vw);
  padding: 2.4rem 2.2rem 2rem;
  border: 1px solid var(--border);
  border-radius: 16px;
  background: linear-gradient(180deg,
              color-mix(in srgb, var(--bg-surface) 82%, transparent),
              color-mix(in srgb, var(--bg-mid) 90%, transparent));
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  box-shadow: 0 24px 60px -20px rgba(0, 0, 0, 0.7),
              inset 0 1px 0 rgba(255, 255, 255, 0.03);
  overflow: hidden;
}
/* a single cyan filament creeping along the card's top edge */
.auth-card::before {
  content: "";
  position: absolute;
  top: 0;
  left: 12%;
  right: 12%;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--accent), transparent);
  box-shadow: 0 0 8px var(--accent);
  opacity: 0.85;
}

.auth-brand {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  margin: 0 0 0.5rem;
}
.auth-spore { width: 34px; height: 34px; flex: none; }

/* Brandmark watermark behind the login card — expanded + muted.
   Sits between the mycelium canvas/veil and the card (the card has
   its own stacking context via .auth-card). pointer-events none so
   it never eats clicks. */
.auth-brand-bg {
  position: absolute;
  top: 50%;
  left: 50%;
  width: min(108vmin, 960px);
  transform: translate(-50%, -50%);
  opacity: 0.07;
  filter: saturate(0.6);
  pointer-events: none;
  user-select: none;
}
.auth-wordmark {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1.5rem;
  letter-spacing: 0.01em;
  line-height: 1;
  color: var(--text-bright);
}
.auth-wordmark span { color: var(--accent); }
.auth-lede {
  margin: 0.1rem 0 1.6rem;
  color: var(--text-secondary);
  font-size: 0.9rem;
  line-height: 1.5;
}
.auth-lede code {
  font-family: var(--font-mono);
  font-size: 0.82em;
  color: var(--accent-pop);
  background: var(--accent-glow);
  padding: 0.1em 0.4em;
  border-radius: 4px;
}

.auth-hint {
  margin: 1.1rem 0 0;
  color: var(--text-muted);
  font-size: 0.8rem;
  line-height: 1.5;
}
.auth-hint code {
  font-family: var(--font-mono);
  font-size: 0.85em;
  color: var(--accent-pop);
  background: var(--accent-glow);
  padding: 0.1em 0.4em;
  border-radius: 4px;
}

.auth-form { display: block; }
.auth-label { display: block; margin: 0 0 1.1rem; }
.auth-label > span {
  display: block;
  margin: 0 0 0.45rem;
  font-size: 0.72rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--text-muted);
}
.auth-label input[type="password"] {
  width: 100%;
  padding: 0.75rem 0.9rem;
  border: 1px solid var(--border);
  border-radius: 9px;
  background: color-mix(in srgb, var(--bg-abyss) 60%, transparent);
  color: var(--text-primary);
  font-family: var(--font-mono);
  font-size: 0.95rem;
  transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}
.auth-label input[type="password"]:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-glow-md),
              0 0 20px -4px var(--accent-glow-strong);
}
.auth-label input::placeholder { color: var(--text-dim); }

.auth-submit {
  width: 100%;
  padding: 0.8rem;
  border: none;
  border-radius: 9px;
  cursor: pointer;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 0.95rem;
  letter-spacing: 0.02em;
  color: #04191a;
  background: linear-gradient(180deg, var(--accent-pop), var(--accent));
  box-shadow: 0 6px 18px -6px var(--accent-glow-strong);
  transition: transform var(--transition-fast), box-shadow var(--transition);
}
.auth-submit:hover {
  box-shadow: 0 8px 26px -6px var(--accent-glow-strong),
              0 0 0 1px var(--accent);
}
.auth-submit:active { transform: translateY(1px); }
.auth-submit:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.auth-foot {
  position: relative;
  z-index: 2;
  margin-top: 1.6rem;
  padding: 0 1rem;
  text-align: center;
  color: var(--text-muted);
  font-size: 0.76rem;
  letter-spacing: 0.02em;
}
.auth-foot b {
  color: var(--text-bright);
  font-weight: 600;
  font-family: var(--font-display);
}
.auth-foot a {
  text-decoration: none;
}
.auth-foot a:hover b {
  text-decoration: underline;
}
.auth-fil {
  display: inline-block;
  width: 1.4rem;
  height: 1px;
  vertical-align: middle;
  margin: 0 0.55rem;
  background: linear-gradient(90deg, transparent, var(--accent), transparent);
  box-shadow: 0 0 6px var(--accent);
  opacity: 0.7;
}
@media (prefers-reduced-motion: reduce) {
  .auth-stage .myc-canvas { opacity: 0.6; }
}

/* ---- App-wide atmosphere: the same filament field as a whisper
   behind every signed-in page, plus a wordmark footer. Fixed at
   z-index:-1 so it sits UNDER all content + chrome without touching
   their positioning (an earlier version forced position:relative on
   the chrome, which knocked the fixed slide-in flyout into normal
   flow — a giant "Details" panel at the page foot). Negative z + a
   painted body background keeps it purely decorative. ---- */
.app-atmosphere {
  position: fixed;
  inset: 0;
  z-index: -1;          /* behind everything; body bg is made transparent below */
  pointer-events: none;
  opacity: 0.3;
}
.app-atmosphere .myc-canvas { position: absolute; inset: 0; width: 100%; height: 100%; }
/* For the negative-z canvas to be visible, the body must not paint an
   opaque fill over it. Move the ground colour to an html fill and let
   body be transparent — only when the atmosphere is present (signed-in
   pages carry .has-sidebar). Chrome + content keep their positioning
   untouched (this was the bug: forcing position on the fixed flyout
   dropped it into flow as a giant panel). */
body.has-sidebar { background: transparent; }
html { background: var(--bg-abyss); }

/* Stack the two footer lines + give the brand line breathing room so
   it reads as its own designed strip, not a run-on of the connect line. */
main > footer {
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
}
/* One flex row so the mark, the powered-by text and the version share
   a centreline — an inline-flex link beside plain inline text sits off
   the baseline (the footer-on-the-wonk bug, 2026-09-01). */
main > footer .foot-brand {
  display: flex;
  align-items: center;
  /* The footer centres by text-align, which a flex row ignores —
     centre the row explicitly. */
  justify-content: center;
  gap: 0.6rem;
  padding-top: 0.5rem;
  letter-spacing: 0.03em;
  color: var(--text-muted);
}
/* "Built on Microcelium" — the house attribution row (same shape as
   the hive-mind footer). The mark carries the brand gradient of its
   own, so hover changes only opacity — tinting the row would fight it;
   the wordmark wears the cyan→magenta brand gradient as text. */
main > footer .foot-powered {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  color: var(--text-muted);
  text-decoration: none;
  opacity: 0.8;
  transition: opacity 0.15s;
}
main > footer .foot-powered:hover { opacity: 1; }
main > footer .foot-powered img { display: block; }
/* The flex gap spaces the row — the inline-era margin would double it.
   A pipe separates attribution from version, as on the hive footer. */
main > footer .foot-brand .foot-version { margin-left: 0; }
main > footer .foot-brand .foot-version::before {
  content: "|";
  margin-right: 0.6rem;
  opacity: 0.4;
}
main > footer .foot-powered strong {
  font-family: var(--font-display);
  font-weight: 600;
  background: linear-gradient(100deg, #00e5e3, #f500ff);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
main > footer .foot-fil {
  display: inline-block;
  width: 2rem;
  height: 1px;
  vertical-align: middle;
  margin: 0 0.6rem;
  background: linear-gradient(90deg, transparent, var(--accent), transparent);
  box-shadow: 0 0 8px var(--accent);
  opacity: 0.85;
}

/* STACK-MAP-1 (C2) — the per-project Stack Map. Fresh D3-force render;
   not the dagre topology styling. */
.stackmap-toolbar {
  display: flex; align-items: center; gap: 1rem;
  margin: 0.75rem 0; flex-wrap: wrap;
}
/* STACK-MAP-2 (D2): write-half editor panel. */
.stackmap-editor {
  margin: 0.75rem 0; padding: 0.75rem 1rem;
  border: 1px solid var(--border, #333); border-radius: 6px;
  background: var(--card-bg, #1a1a1a);
}
.stackmap-editor > summary { cursor: pointer; font-weight: 600; }
.stackmap-editor h4 { margin: 0.9rem 0 0.4rem; font-size: 0.9rem; }
.stackmap-cat-item {
  display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap;
  padding: 0.25rem 0;
}
.stackmap-cat-item .sm-cat-name { font-weight: 600; }
.stackmap-cat-actions { margin-left: auto; display: inline-flex; gap: 0.4rem; }
.stackmap-wire-row {
  display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;
}
.stackmap-pending { margin: 0.75rem 0; }

.stackmap-canvas-wrap {
  position: relative;
  background: var(--card-bg-deep, #111);
  border: 1px solid var(--border, #333);
  border-radius: 6px;
  overflow: hidden;
}
#stackmap-canvas { display: block; cursor: grab; }
#stackmap-canvas:active { cursor: grabbing; }
.sm-node.sm-dim { opacity: 0.22; transition: opacity 0.12s; }
.sm-links line.sm-dim { opacity: 0.12; transition: opacity 0.12s; }
.sm-links line.sm-trace {
  stroke-width: 2.6;
  stroke-dasharray: 6 4;
  animation: sm-march 0.5s linear infinite;
}
@keyframes sm-march { to { stroke-dashoffset: -20; } }
.stackmap-legend {
  display: flex; gap: 1.1rem; margin-top: 0.6rem; flex-wrap: wrap;
  font-size: 0.85rem; color: var(--muted, #9ca3af);
}
.stackmap-legend-item { display: inline-flex; align-items: center; gap: 0.4rem; }
.sm-swatch { display: inline-block; width: 14px; height: 14px; border-radius: 3px; }
.sm-swatch.sm-ingress { background: #7c3aed; }
.sm-swatch.sm-service { background: #2563eb; }
.sm-swatch.sm-network { background: #0891b2; border-radius: 7px; }
.sm-swatch.sm-volume  { background: #b45309; }

/* =====================================================================
   MapKit (MAP-VIS-1) — shared canvas language for Stack Map + Topology.
   The canvas is DELIBERATELY dark in both themes (network diagrams read
   best on a deep ground); the chrome around it follows theme tokens.
   ===================================================================== */
.mk-canvas-wrap {
  position: relative;
  border-radius: 14px;
  overflow: hidden;
  background:
    radial-gradient(1100px 500px at 30% -10%, rgba(0,229,227,0.055), transparent 60%),
    radial-gradient(900px 480px at 85% 110%, rgba(124,58,237,0.075), transparent 55%),
    var(--bg-abyss, #070c17);
  border: 1px solid rgba(148,163,184,0.14);
}
.mapkit-svg { display: block; width: 100%; cursor: grab; }
.mapkit-svg:active { cursor: grabbing; }

/* NETWORK-VIZ-1 per-site map blocks — one stacked canvas per site. */
.sitemap-block { margin: 0 0 1.6rem; }
.sitemap-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; margin: 0 0 .5rem;
}
.sitemap-title {
  margin: 0; font-size: 1.05rem; display: flex; align-items: center;
  gap: .45rem;
}
.sitemap-title svg { width: 18px; height: 18px; opacity: .8; }
.sitemap-title .hint { font-weight: 400; }
.sitemap-canvas-wrap { min-height: 300px; }
.sitemap-block .sm-node { transition: opacity .12s ease; }
.sitemap-links {
  list-style: none; margin: .4rem 0 0; padding: 0;
  display: flex; flex-direction: column; gap: .28rem;
}
.sitemap-links li { font-size: .88rem; }
.sitemap-links .hint { opacity: .7; }

/* IPAM occupants — the expandable "what holds each address" list that
   replaced the hidden title= tooltip. */
.ipam-occupants > summary { cursor: pointer; }
.ipam-occupant-list {
  list-style: none; margin: .3rem 0 0; padding: 0;
  display: flex; flex-direction: column; gap: .2rem;
  font-size: .85rem; color: var(--text-muted, #94a3b8);
}
.ipam-occupant-list code { font-size: .82em; }

/* Agent host listening ports — a first-class (no longer collapsed)
   security list in the VM flyout. */
.host-ports-head { font-weight: 600; margin: .6rem 0 .3rem; font-size: .9rem; }
.host-ports-list {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: .25rem; font-size: .85rem;
}
.host-ports-list li { display: flex; align-items: center; gap: .5rem; }
.host-ports-list .hint { display: inline-flex; align-items: center; gap: .4rem; }

/* FW-STUDIO-1 MR2 — Firewall Studio editor cards. */
.fw-studio .fw-card { margin: 1rem 0; }
.fw-studio-err {
  margin: .6rem 0; padding: .5rem .7rem;
  border-left: 3px solid var(--viz-crit, #f87171);
  background: color-mix(in srgb, var(--viz-crit, #f87171) 10%, transparent);
  font-size: .85rem; white-space: pre-wrap;
}
.fw-add { margin-top: .6rem; }
.fw-add summary { cursor: pointer; }
.fw-add-form {
  display: flex; flex-wrap: wrap; gap: .4rem; align-items: center;
  margin-top: .5rem;
}
.fw-add-form input, .fw-add-form select { font-size: .85rem; }
.fw-studio .col-actions { white-space: nowrap; }
.fw-studio .col-actions .btn { margin-left: .2rem; }

/* ROLE-REGISTRY-1 — the machine-role reference on the VMs tab. */
.roles-ref { margin: 1rem 0 0; }
.roles-ref summary { cursor: pointer; font-weight: 600; }
.roles-ref .table-scroll { margin-top: .6rem; }

/* FW-DETAIL — per-interface list inside the Firewalls tab detail. */
.fw-ifaces summary { cursor: pointer; }
.fw-ifaces-list {
  list-style: none; margin: .3rem 0 0; padding: 0;
  display: flex; flex-direction: column; gap: .2rem; font-size: .82rem;
}
.fw-ifaces-list li { display: flex; align-items: center; gap: .4rem; }

/* NETSCAN — one block per swept prefix on Network ▸ Scan. */
.netscan-prefix { margin: 0 0 1.4rem; }
.netscan-prefix-head {
  display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
  margin: 0 0 .5rem; font-size: 1rem;
}

/* NETWORK-VIZ-1 MR3 switch front panel — a physical port grid. */
.sw-panel-head { margin: 0 0 .6rem; }
.sw-panel-meta { color: var(--text-muted, #94a3b8); font-size: .9rem; }
.sw-panel-grid {
  padding: 1rem; border-radius: 12px;
  background: var(--bg-abyss, #070c17);
  border: 1px solid rgba(148,163,184,0.14);
  overflow-x: auto;
}
.sw-board {
  display: grid; grid-auto-flow: column; gap: 5px;
  width: max-content; grid-template-rows: repeat(2, auto);
}
.sw-port {
  min-width: 30px; height: 30px; padding: 0;
  border-radius: 5px; border: 1px solid rgba(148,163,184,0.30);
  background: rgba(148,163,184,0.10);
  color: var(--text-muted, #94a3b8);
  font: 600 .72rem/1 var(--mono, ui-monospace, monospace);
  cursor: pointer; transition: transform .1s ease, box-shadow .1s ease;
}
.sw-port:hover { transform: translateY(-1px);
  box-shadow: 0 0 0 2px var(--accent, #22d3ee); }
.sw-port.sw-up      { background: rgba(52,211,153,0.22);
                      border-color: #34d399; color: #d1fae5; }
.sw-port.sw-down    { background: rgba(148,163,184,0.10);
                      border-color: rgba(148,163,184,0.30); }
.sw-port.sw-unknown { background: rgba(251,191,36,0.10);
                      border-color: rgba(251,191,36,0.35); }
.sw-port.sw-drift   { background: rgba(248,113,113,0.22);
                      border-color: #f87171; color: #fee2e2; }
.sw-port.sw-declared{ background: transparent;
                      border-style: dashed; border-color: #fbbf24; }
.sw-port.sw-poe     { box-shadow: inset 0 0 0 2px rgba(251,191,36,0.85); }
.sw-port.sw-uplink  { outline: 2px solid var(--accent, #22d3ee);
                      outline-offset: 1px; }
.sw-panel-legend {
  display: flex; flex-wrap: wrap; gap: .9rem; margin: .8rem 0 0;
  font-size: .82rem; color: var(--text-muted, #94a3b8);
}
.sw-leg { display: inline-flex; align-items: center; gap: .35rem; }
.sw-swatch { width: 14px; height: 14px; border-radius: 3px;
  border: 1px solid rgba(148,163,184,0.30); display: inline-block; }
.sw-swatch.sw-up { background: rgba(52,211,153,0.4); border-color: #34d399; }
.sw-swatch.sw-down { background: rgba(148,163,184,0.15); }
.sw-swatch.sw-poe { box-shadow: inset 0 0 0 2px rgba(251,191,36,0.85); }
.sw-swatch.sw-drift { background: rgba(248,113,113,0.4); border-color: #f87171; }
.sw-swatch.sw-declared { border-style: dashed; border-color: #fbbf24; }
.sw-swatch.sw-uplink { outline: 2px solid var(--accent, #22d3ee);
  outline-offset: -1px; }

/* Tiles */
.mk-tile {
  fill: rgba(19,32,56,0.92);
  stroke-width: 1.5;
  filter: drop-shadow(0 2px 6px rgba(0,0,0,0.45));
  transition: stroke-width 120ms ease;
}
.mk-node { cursor: pointer; }
.mk-node:hover .mk-tile { stroke-width: 2.5; }
.mk-node.mk-selected .mk-tile { stroke-width: 2.5; filter: url(#mk-glow); }
.mk-tile-icon { fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.mk-status { stroke: #070c17; stroke-width: 2; }
.mk-label { fill: #e2e8f0; font-size: 11.5px; font-weight: 600; letter-spacing: 0.01em; }
.mk-sublabel { fill: #64748b; font-size: 9.5px; }

/* Edges */
.mk-edge { fill: none; stroke-width: 1.6; opacity: 0.75; transition: opacity 140ms ease; }
.mk-edge.mk-dim { opacity: 0.12; }
.mk-node.mk-dim { opacity: 0.25; }
.mk-edge.mk-trace { stroke-width: 2.4; opacity: 1; filter: url(#mk-glow); }
.mk-edge-label { fill: #7d8aa0; font-size: 9px; }

/* Overlay chrome — control cluster, chips, legend live ON the canvas */
.mk-controls {
  position: absolute; top: 12px; left: 12px;
  display: flex; gap: 6px; z-index: 3;
}
.mk-controls button {
  width: 34px; height: 34px; display: grid; place-items: center;
  border-radius: 9px;
  border: 1px solid rgba(148,163,184,0.22);
  background: rgba(15,26,46,0.85);
  color: #cbd5e1; cursor: pointer;
  backdrop-filter: blur(6px);
  transition: background 120ms ease, color 120ms ease;
}
.mk-controls button:hover { background: rgba(26,44,73,0.95); color: #fff; }
.mk-controls button svg { width: 17px; height: 17px; }

.mk-chip {
  position: absolute; top: 12px; right: 12px; z-index: 3;
  display: flex; align-items: center; gap: 8px;
  padding: 7px 14px; border-radius: 10px;
  border: 1px solid rgba(148,163,184,0.22);
  background: rgba(15,26,46,0.85);
  backdrop-filter: blur(6px);
  font-size: 0.82rem; color: #cbd5e1; font-weight: 600;
}
.mk-chip-sep { color: #475569; }
.mk-chip-ok { color: #34d399; }
.mk-chip-warn { color: #fbbf24; }
.mk-chip-bad { color: #f87171; }

/* Config-disabled service (enable_<svc>: false for the viewed
   context): keep the shape, kill the presence. */
.mk-node-off { opacity: 0.35; }
.mk-edge-off { opacity: 0.25; }

/* --- GRAPH-1 canvas (the entity-graph node-link view) --------------- */
.graph-canvas-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 12px; flex-wrap: wrap;
}
.graph-canvas-controls {
  display: flex; align-items: center; gap: 6px;
  position: static;          /* header chip, not the absolute mk-chip */
}
.graph-canvas-controls .mk-chip {
  position: static; backdrop-filter: none; background: transparent;
  border: none; padding: 0 6px 0 0; color: var(--muted, #94a3b8);
}
.graph-canvas-wrap {
  position: relative; margin-top: 10px;
  background: var(--card-bg-deep, #0d1526);
  border: 1px solid var(--border, #26334d);
  border-radius: 8px; overflow: hidden;
}
#graph-canvas { display: block; width: 100%; height: 620px; cursor: grab; }
#graph-canvas:active { cursor: grabbing; }
.graph-legend {
  display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px;
}
.graph-legend-item {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 9px; border-radius: 999px; cursor: pointer;
  border: 1px solid var(--border, #26334d);
  background: var(--chip-bg, rgba(148,163,184,0.08));
  color: var(--fg, #cbd5e1); font-size: 0.78rem; font-weight: 600;
  text-transform: capitalize; transition: opacity 120ms ease;
}
.graph-legend-item.is-off { opacity: 0.4; text-decoration: line-through; }
.graph-legend-sw {
  width: 11px; height: 11px; border-radius: 3px; display: inline-block;
}
.graph-detail {
  position: absolute; top: 12px; left: 12px; z-index: 3; max-width: 20rem;
  padding: 10px 14px; border-radius: 10px; line-height: 1.5;
  border: 1px solid rgba(148,163,184,0.22);
  background: rgba(15,26,46,0.9); backdrop-filter: blur(6px);
  color: #e2e8f0; font-size: 0.82rem;
}
.graph-status {
  position: absolute; bottom: 12px; right: 14px; z-index: 3; margin: 0;
}
/* impact-trace dim/highlight (reuses the mk-glow filter defs) */
.graph-node.graph-dim { opacity: 0.2; transition: opacity 140ms ease; }
.graph-node.graph-trace .mk-tile { stroke-width: 2.6; filter: url(#mk-glow); }

/* health-rollup tally strip: a pill + its count, laid out inline. */
.graph-tally { margin-bottom: 0.6rem; }
.graph-tally-item { display: inline-flex; align-items: center; gap: 0.35rem; }
.graph-tally-item strong { font-variant-numeric: tabular-nums; }
/* a healthy entity is the quiet majority — recede it so the sick rows
 * (degraded/warn) read first. Was the intent of the old inert
 * `.row-muted`; this is the real rule. */
.graph-row-quiet { opacity: 0.55; }
.graph-row-quiet:hover { opacity: 1; }

.mk-legend {
  position: absolute; bottom: 12px; left: 12px; z-index: 3;
  display: grid; gap: 5px;
  padding: 10px 14px; border-radius: 10px;
  border: 1px solid rgba(148,163,184,0.18);
  background: rgba(15,26,46,0.85);
  backdrop-filter: blur(6px);
  font-size: 0.76rem; color: #94a3b8;
}
.mk-legend-item { display: flex; align-items: center; gap: 8px; }
.mk-legend-swatch {
  width: 22px; height: 0; border-top-width: 2px; border-top-style: solid;
}
.mk-legend-swatch.mk-dashed { border-top-style: dashed; }
.mk-legend-swatch.mk-uplink { border-top-width: 3.2px; }

/* SWITCH-VIZ-1 MR3: a cable carrying flow marches a dash pattern so busy
   links read at a glance. Reduced-motion users get a static thicker
   line (the stroke-width already encodes utilisation). */
.fab-edge.fab-flowing {
  stroke-dasharray: 6 5;
  animation: fab-flow-march 1.2s linear infinite;
}
@keyframes fab-flow-march { to { stroke-dashoffset: -22; } }
@media (prefers-reduced-motion: reduce) {
  .fab-edge.fab-flowing { animation: none; stroke-dasharray: none; }
}
.mk-legend-tile {
  width: 10px; height: 10px; border-radius: 3px;
  border: 1.5px solid; background: rgba(19,32,56,0.9);
}
/* Status-dot swatch — mirrors the on-tile mk-status circle so the
   running / degraded / stopped / unknown colours read at a glance. */
.mk-legend-dot {
  width: 9px; height: 9px; border-radius: 50%;
  box-shadow: 0 0 0 1px rgba(15,26,46,0.9);
}
/* Faint rule separating the node-kind legend from the status legend. */
.mk-legend-rule {
  height: 1px; margin: 2px 0;
  background: rgba(148,163,184,0.16);
}

/* Loading shimmer over the canvas while a graph fetch is in flight */
.mk-loading[hidden] { display: none; }
.mk-loading {
  position: absolute; inset: 0; z-index: 2;
  display: grid; place-items: center;
  color: #64748b; font-size: 0.85rem;
  background: rgba(7,12,23,0.55);
  backdrop-filter: blur(2px);
}

/* MapKit on the Topology tab — nic/port sub-node pills + trace states
   (the marching-ants keyframe already exists as topo-ants). */
.mk-pill {
  fill: rgba(19,32,56,0.85);
  stroke: #64748b; stroke-width: 1;
}
.mk-pill-label { fill: #94a3b8; font-size: 9.5px; }
g.node.topo-dimmed { opacity: 0.16; }
g.edgePath.topo-dimmed .mk-edge { opacity: 0.06; }
g.node.topo-path-node .mk-tile { stroke-width: 2.5; filter: url(#mk-glow); }
g.edgePath.topo-path-edge .mk-edge {
  stroke: #34d399; stroke-width: 2.6; opacity: 1;
  stroke-dasharray: 7 5;
  animation: topo-ants 0.7s linear infinite;
}

/* MAP-VIS-2 — drag-drop affordances on the Stack Map */
.mk-node.mk-drop-ok .mk-tile {
  stroke-width: 3; filter: url(#mk-glow);
}
/* CATALOG-DRAG-FIX — a transparent drop overlay above the d3-zoom SVG.
   Inert by default (pointer-events:none) so pan/zoom/click pass through;
   only while a catalogue card is being dragged (body.sm-card-dragging)
   does it capture the native drag events the zoomed SVG would otherwise
   suppress. Covers the full canvas wrap. */
.mk-drop-overlay {
  position: absolute; inset: 0; z-index: 4;
  pointer-events: none;
}
body.sm-card-dragging .mk-drop-overlay { pointer-events: auto; }

/* A bold, unmistakable drop target while a card is dragged over the
   map — the thin dashed line was easy to miss, so the whole canvas
   tints + a heavier ring + a "drop to add" cue. */
.mk-canvas-wrap.mk-dragover {
  /* background-color only — the wrap's radial-gradient lives in
     background-image and must survive the drag. */
  outline: 3px dashed var(--ok, #34d399); outline-offset: -3px;
  background-color: color-mix(in srgb, var(--ok, #34d399) 8%, transparent);
}
.mk-canvas-wrap.mk-dragover::after {
  content: "Drop to add to the stack";
  position: absolute; top: 0.6rem; left: 50%; transform: translateX(-50%);
  z-index: 5; pointer-events: none;
  font-size: 0.8rem; font-weight: 600;
  color: var(--ok, #34d399);
  background: var(--bg-elevated); border: 1px solid var(--ok, #34d399);
  border-radius: 999px; padding: 0.15rem 0.7rem;
}
/* Draggable catalogue cards advertise the grab + a dotted drag rail on
   hover so it's obvious they can be pulled onto the canvas. */
.stackmap-cat-item[draggable="true"] { cursor: grab; }
.stackmap-cat-item[draggable="true"]:active { cursor: grabbing; }
.stackmap-cat-item[draggable="true"]:hover {
  border-style: dashed;
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 40%, transparent);
}

/* Edge labels rest hidden; the hover trace reveals the relevant ones. */
.mk-edge-label { opacity: 0; transition: opacity 140ms ease; }
.mk-edge-label.mk-label-on { opacity: 1; fill: #cbd5e1; }

/* MAP-VIS-3-MR3: topology drag-gesture affordances */
g.mk-node.mk-dragging { cursor: grabbing; }
g.mk-node.mk-dragging .mk-tile { filter: url(#mk-glow); }
g.mk-node.mk-drop-ok .mk-tile { stroke-width: 3; filter: url(#mk-glow); }
g.mk-node-vm { cursor: grab; }

/* MAP-VIS-3 follow-up: new-machine palette on the topology toolbar */
.topology-palette {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin: 0 0 10px; font-size: 0.85rem;
}
.topo-chip {
  padding: 3px 12px; border-radius: 999px; cursor: grab;
  border: 1px solid rgba(148,163,184,0.3);
  background: rgba(74,222,128,0.10); color: #86efac;
  user-select: none; transition: background 120ms ease;
}
.topo-chip:hover { background: rgba(74,222,128,0.2); }
.topo-chip:active { cursor: grabbing; }

/* ADD-FLYOUT: the add-machine form rendered into the shared right
   flyout when a palette chip is dropped onto a hypervisor tile. */
.topo-add-form { display: flex; flex-direction: column; gap: 14px; }
.topo-add-field { display: flex; flex-direction: column; gap: 4px; }
.topo-add-field > span { font-size: 0.82rem; color: #cbd5e1; }
.topo-add-field .input { width: 100%; }
.topo-add-field small.hint { font-size: 0.72rem; }
.topo-add-actions { display: flex; gap: 10px; margin-top: 4px; }

/* MAP-VIS-3 follow-up: keyboard Add-machine form (Machines tab). The
   accessible sibling of the topology palette. Fields flow in a row on
   wide screens; the global `form label` rule stacks label-over-input. */
.add-machine { margin-top: 18px; }
.add-machine > summary { cursor: pointer; font-weight: 600; }
.add-machine .form-grid {
  display: flex; flex-wrap: wrap; align-items: flex-end; gap: 12px;
  margin-top: 12px;
}
.add-machine .form-grid label { margin-bottom: 0; }
.add-machine .form-grid input,
.add-machine .form-grid select { min-width: 9rem; }

/* RESULT-URL: jump-off button on a succeeded job page (a job that
   printed `RESULT_URL: /ui/...`, e.g. project-create → topology). */
.job-result-link { margin: 0.6rem 0 0; }

/* VMs-tab project picker — rides inside the shared search form. */
.vm-project-input { max-width: 12rem; }

/* VMs-tab key-health strip: one slim row (counts as pills, probe
   fan-outs pushed right) replacing the old 3-tile + button-row
   block that ate the top of the tab. */
.kh-strip {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5em;
  margin: 0.25em 0 0.9em;
}
.kh-strip-label {
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-secondary);
}
.kh-strip-spacer { flex: 1 1 auto; }
.kh-strip form { margin: 0; }


/* TOPO-3: physical palette + connect mode */
.topo-chip-phys { border-style: dashed; }
.topo-checklist {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem 0.9rem;
  max-height: 9rem;
  overflow-y: auto;
}
.topo-checklist label {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  font-size: 0.85rem;
}
#topo-connect-toggle.btn-active {
  background: var(--accent);
  color: var(--bg);
}

/* Fleet > Networks: collapsible section cards. The tab is six stacked
   tables; folding the quiet ones lets the page scan as a table of
   contents instead of a wall. */
.net-section {
  margin: 1.1rem 0;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}
.net-section > summary {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.65rem 0.9rem;
  cursor: pointer;
  list-style: none;
  user-select: none;
  font-weight: 600;
  border-radius: var(--radius);
}
.net-section > summary::-webkit-details-marker { display: none; }
.net-section > summary::after {
  content: '›';
  margin-left: auto;
  color: var(--text-dim);
  transition: transform 0.12s ease;
}
.net-section[open] > summary::after { transform: rotate(90deg); }
.net-section > summary:hover { background: var(--accent-glow); }
.net-section[open] > summary {
  border-bottom: 1px solid var(--border);
  border-radius: var(--radius) var(--radius) 0 0;
}
.net-section > .net-section-body { padding: 0.35rem 0.9rem 0.9rem; }
.net-count {
  font-size: 0.78em;
  font-weight: 500;
  padding: 0.05rem 0.5rem;
  border-radius: 999px;
  background: var(--border);
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}
.net-section > summary .hint { font-weight: 400; }

/* Tab-strip group divider (Fleet: compute family | network family). */
.tab-nav-gap {
  display: inline-block;
  width: 1px;
  align-self: stretch;
  margin: 0.35rem 0.55rem;
  background: var(--border, rgba(128,128,128,.35));
}

/* ============================================================
   UI-MOBILE-1 responsive primitives
   Design: config/docs/plans/UI-MOBILE-1.md. Mobile (<640px) is the
   baseline shape; these rules only fire under media queries, so a
   revert returns every page to its desktop-only behaviour.
   ============================================================ */
:root {
  --tap-min: 44px;
}

/* --- Tab strip: horizontal scroll + snap instead of wrapping.
   Wrapping 8+ fleet tabs on a phone stacked them 3 rows deep; a
   snap-scrolled single row keeps the strip one thumb-swipe tall.
   The active tab is scrolled into view by base.html.j2 JS. --- */
@media (max-width: 640px) {
  .tab-nav {
    flex-wrap: nowrap;
    overflow-x: auto;
    scroll-snap-type: x proximity;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    /* Soft fade on the right edge hints there are more tabs. */
    mask-image: linear-gradient(to right, #000 92%, transparent);
    -webkit-mask-image: linear-gradient(to right, #000 92%, transparent);
  }
  .tab-nav::-webkit-scrollbar { display: none; }
  main .tab-nav a {
    scroll-snap-align: start;
    white-space: nowrap;
    min-height: var(--tap-min);
    display: inline-flex;
    align-items: center;
  }
}

/* --- Tables → cards. Opt-in via class="responsive-table" on the
   <table>; each <td> carries data-label (its column header) which
   the card renders as a leading label. Desktop is untouched. --- */
@media (max-width: 640px) {
  table.responsive-table { min-width: 0; }
  table.responsive-table,
  table.responsive-table tbody,
  table.responsive-table tr,
  table.responsive-table td { display: block; width: 100%; }
  table.responsive-table thead { display: none; }
  table.responsive-table tbody tr {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    margin-bottom: 0.75rem;
    padding: 0.5rem 0.75rem;
    background: var(--bg-surface);
  }
  table.responsive-table tbody td {
    border-bottom: none;
    padding: 0.3rem 0;
  }
  table.responsive-table tbody td[data-label]::before {
    content: attr(data-label);
    display: block;
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-muted);
    margin-bottom: 0.1rem;
  }
  /* A td that declares itself the card title reads bigger, no label. */
  table.responsive-table tbody td.card-title-cell {
    font-size: 1.05rem;
    padding-bottom: 0.4rem;
  }
  /* Empty cells (a sparkline that didn't render, a blank actions
     column) collapse rather than showing a dangling label. */
  table.responsive-table tbody td:empty { display: none; }
}

/* --- Flyout → bottom sheet. Same open/close JS; only the transform
   axis changes under the breakpoint. 88dvh leaves a strip of the
   page visible behind the sheet as context + a tap-to-close target. --- */
@media (max-width: 640px) {
  .fleet-side-panel {
    right: 0;
    left: 0;
    top: auto;
    bottom: 0;
    width: 100vw;
    height: 88dvh;
    transform: translateY(100%);
    border-left: none;
    border-top: 1px solid var(--border);
    border-radius: 12px 12px 0 0;
  }
  .fleet-side-panel.fleet-side-panel-open { transform: translateY(0); }
  /* Drag-handle visual cue at the sheet's top edge. Pure CSS — the
     v1 dismiss affordances are the close button / backdrop / Esc. */
  .fleet-side-panel::before {
    content: "";
    display: block;
    width: 2.5rem;
    height: 4px;
    border-radius: 999px;
    background: var(--border);
    margin: 0.5rem auto 0.25rem;
  }
}

/* --- Touch targets + iOS zoom guard. 16px input font stops iOS
   auto-zooming the viewport on focus; 44px min-height is the Apple
   HIG tap-target floor. Mobile only — desktop density stays. --- */
@media (max-width: 640px) {
  main input:not([type="checkbox"]):not([type="radio"]),
  main select,
  main textarea {
    min-height: var(--tap-min);
    font-size: 16px;
  }
  main button, main .btn {
    min-height: 2.25rem; /* .btn-sm rows stay compact but tappable */
  }
  main form button[type="submit"], main .btn:not(.btn-sm) {
    min-height: var(--tap-min);
  }
  pre, .log-tail { max-height: 50dvh; }
}

/* --- Canvas graphs (Topology / Stack Map) are desktop-class; below
   tablet width show the placeholder note instead. Templates carry
   both nodes; CSS picks one. --- */
.mobile-canvas-note { display: none; }
@media (max-width: 1024px) {
  .mobile-canvas-note {
    display: block;
    color: var(--text-muted);
    padding: 1.5rem 0;
  }
  .canvas-desktop-only { display: none; }
}

/* Services overview — row icon before the service name, tinted like
   the section-header glyph so the row visually indexes its tab. */
.service-row-link { display: inline-flex; align-items: center; gap: 0.45em; }
.service-row-icon { color: var(--accent); display: inline-flex; }
.service-row-icon .icon { width: 1.1em; height: 1.1em; }

/* Services > Registry action row — the on/off toggle + job buttons sit
   side by side, not stacked (each is its own <form>, which is
   block-level without this). Wraps on narrow screens. */
.cfg-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  align-items: center;
}

/* BP-READ-1: Blueprint tab read-first panels — one bordered box per
   declared-posture section so the read view scans as cards, and a
   header row for the context editor's otherwise-anonymous inputs. */
.bp-panel {
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 0.25rem 1.1rem 1rem;
  margin: 1rem 0;
}
.bp-panel > h3:first-child { margin-top: 0.85rem; }
.context-grid-head {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin-bottom: 0.25rem;
}
/* Hide the editor column labels when the row grid stacks (mobile) —
   the per-input placeholders take over there. */
@media (max-width: 720px) {
  .context-grid-head { display: none; }
}

/* --- STAGED-DIFF-1 — expandable per-commit diff in the pending bar */
.pending-commits { display: flex; flex-direction: column; gap: 2px; }
.pending-commit > summary {
  cursor: pointer;
  padding: 0.3rem 0.4rem;
  border-radius: 6px;
  list-style-position: inside;
}
.pending-commit > summary:hover { background: var(--surface-2, rgba(128,128,128,0.08)); }
.pending-commit-subject { margin-left: 0.35rem; }
.pending-commit-diff { margin: 0.35rem 0 0.75rem 1.25rem; }

.diff-file {
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
  margin-bottom: 0.6rem;
}
.diff-file-head {
  display: flex; align-items: center; gap: 0.6rem;
  padding: 0.35rem 0.6rem;
  border-bottom: 1px solid var(--border);
  font-size: 0.85rem;
}
.diff-path { overflow-wrap: anywhere; }
.diff-counts { margin-left: auto; font-variant-numeric: tabular-nums; white-space: nowrap; }
.diff-plus  { color: var(--success); }
.diff-minus { color: var(--danger); }
.diff-status {
  font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.04em;
  padding: 0.1rem 0.45rem; border-radius: 999px;
  border: 1px solid var(--border-light);
  color: var(--muted);
}
.diff-status-a { color: var(--success); border-color: var(--success); }
.diff-status-d { color: var(--danger);  border-color: var(--danger); }

.diff-body {
  margin: 0; padding: 0.4rem 0;
  font-size: 0.78rem; line-height: 1.45;
  overflow-x: auto;
  background: transparent;
}
.diff-line { display: block; padding: 0 0.75rem; white-space: pre; }
.diff-hunk {
  color: var(--muted);
  background: rgba(128, 128, 128, 0.08);
  padding-top: 0.15rem; padding-bottom: 0.15rem;
}
.diff-add { background: rgba(52, 211, 153, 0.12); color: var(--success); }
.diff-del { background: rgba(248, 113, 113, 0.12); color: var(--danger); }
.diff-masked {
  color: var(--warn);
  font-style: italic;
  padding-top: 0.15rem; padding-bottom: 0.15rem;
  white-space: normal;
}
.diff-masked .icon-glyph { vertical-align: -2px; margin-right: 0.25rem; }

/* --- TEMPLATE-META-1 MR3: catalogue cards + category facets ------- */
.cat-facets {
  display: flex; flex-wrap: wrap; gap: 0.4rem;
  margin: 0.25rem 0 0.75rem;
}
.cat-facet {
  border: 1px solid var(--border);
  background: var(--surface-2, transparent);
  color: var(--text);
  border-radius: 999px;
  padding: 0.15rem 0.65rem;
  font-size: 0.8rem;
  cursor: pointer;
}
.cat-facet:hover { border-color: var(--accent); }
.cat-facet.active {
  border-color: var(--accent);
  color: var(--accent);
  font-weight: 600;
}
.cat-facet-n { opacity: 0.6; margin-left: 0.25rem; }
.catalogue-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(19rem, 1fr));
  gap: 0.6rem;
}
/* The facet chips filter by setting the `hidden` ATTRIBUTE — but an
   author `display: flex` overrides the UA's `[hidden] {display:none}`
   (author styles always beat UA styles), which left every card visible
   no matter the filter. Same trap .mk-loading[hidden] already guards. */
.cat-card[hidden],
.stackmap-cat-item[hidden] { display: none !important; }
.cat-card {
  border: 1px solid var(--border);
  border-radius: var(--radius, 6px);
  padding: 0.55rem 0.7rem;
  display: flex; flex-direction: column; gap: 0.3rem;
  background: var(--surface-1, transparent);
}
/* In-stack cards read at a glance: a solid accent border, a bright
   left rail, and a faint tint — so "what's already in my stack" pops
   out of the grid instead of a barely-there border tweak. */
.cat-card.cat-in-stack {
  border-color: var(--ok, var(--accent));
  border-left: 3px solid var(--ok, var(--accent));
  padding-left: calc(0.7rem - 2px);
  background: color-mix(in srgb, var(--ok, var(--accent)) 8%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ok, var(--accent)) 35%, transparent);
}
.cat-card-head {
  display: flex; align-items: center; gap: 0.45rem;
  flex-wrap: wrap;
}
.cat-card-head .icon-glyph { flex: 0 0 auto; opacity: 0.85; }
.cat-card-name { font-size: 0.95rem; }
.cat-family {
  font-size: 0.72rem; color: var(--muted);
  border: 1px solid var(--border); border-radius: 4px;
  padding: 0 0.3rem;
}
/* A shared-library CATEGORY (data, ops, …) vs a PROJECT namespace
   (medusa, trademate). Both are a subdirectory in micro-glue; giving
   them one treatment read as "there is a project called data".
   Category = a soft filled tag; project = the bordered chip, which
   is what the namespaces already looked like. */
.cat-family-type {
  border-color: transparent;
  background: var(--surface-2, rgba(127, 127, 127, 0.13));
  text-transform: lowercase;
}
.cat-family-project { font-weight: 600; }
.cat-desc {
  margin: 0; font-size: 0.82rem; line-height: 1.35;
  color: var(--muted);
  display: -webkit-box; -webkit-line-clamp: 3;
  -webkit-box-orient: vertical; overflow: hidden;
}
.cat-rules { margin: 0; font-size: 0.75rem; font-style: italic; }
.cat-card-foot {
  margin-top: auto;
  display: flex; align-items: center; gap: 0.6rem;
  flex-wrap: wrap; font-size: 0.78rem;
}
.cat-chip {
  font-size: 0.72rem; text-transform: uppercase;
  letter-spacing: 0.03em;
  border: 1px solid var(--accent); color: var(--accent);
  border-radius: 4px; padding: 0 0.35rem;
}
.cat-actions { margin-left: auto; }

/* --- TEMPLATE-META-1 MR5: catalogue rules lint ------------------- */
.rules-lint {
  border: 1px solid var(--warn, #d97706);
  border-radius: var(--radius, 6px);
  padding: 0.5rem 0.75rem;
  margin: 0.5rem 0 0.75rem;
  font-size: 0.85rem;
}
.rules-lint .icon-glyph { vertical-align: -2px; margin-right: 0.3rem; }
.rules-lint-items { margin: 0.25rem 0 0 1.2rem; padding: 0; }

/* FW-DRIFT-1 — per-section diff lists in the Firewalls tab panels */
.fw-drift-list {
  margin: 0 0 0.3rem;
  padding-left: 1.1rem;
  font-size: 0.85em;
}
.fw-drift-add { color: var(--success); }
.fw-drift-del { color: var(--danger); }
.fw-drift-chg { color: var(--warn); }
.fw-drift-list code { color: var(--text); }

/* CONFIG-STUDIO-3 MR2 — service cards + context sub-tabs */
.cs3-subtabs { display: flex; gap: .4rem; margin: .4rem 0 .8rem; flex-wrap: wrap; }
.cs3-subtab--active { background: var(--accent, #2563eb); color: #fff; }
.cs3-panel { margin-bottom: 1rem; }
.cs3-controls { display: flex; align-items: center; gap: .5rem;
                flex-wrap: wrap; margin-bottom: .5rem; }
.cs3-endpoint { display: flex; align-items: center; gap: .5rem;
                flex-wrap: wrap; padding: .3rem 0;
                border-top: 1px solid var(--border, #e5e7eb); }
.cs3-endpoint form { margin: 0; }
.cs3-port { width: 6rem; }
.cs3-caveat { display: block; max-width: 46rem; margin-top: .3rem; }
.cs3-envsec { margin-top: .6rem; padding-top: .4rem;
              border-top: 1px dashed var(--border, #e5e7eb); }
/* One aligned line per env key: key | (missing pill) | input | Set | Unset */
.cs3-envrows { list-style: none; margin: .3rem 0 0; padding: 0; }
.cs3-envrow { display: flex; align-items: center; gap: .5rem;
              flex-wrap: wrap; padding: .2rem 0; }
.cs3-envkey { min-width: 15rem; flex-shrink: 0; }
.cs3-envform { display: inline-flex; align-items: center;
               gap: .35rem; margin: 0; }
.cs3-envval { min-width: 18rem; flex: 1 1 auto;
              padding: .25rem .5rem; height: auto; }

/* CATALOG-HARMONISE-2 — the Configs "Service cards" section borrows the
   Stack-Map catalogue's tile + chip + button language so the two
   service surfaces read as one system. Scoped to .cs3-cards so the
   shared kit-panel / btn-mini components are untouched elsewhere. */
/* Context sub-tabs → the catalogue facet-chip look (pill outline, the
   active one filled with the accent, same as .cat-facet). */
.cs3-cards .cs3-subtabs { gap: 0.4rem; }
.cs3-cards .cs3-subtab {
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 0.2rem 0.75rem;
  background: transparent;
  font-weight: 500;
}
.cs3-cards .cs3-subtab:hover { border-color: var(--accent); }
.cs3-cards .cs3-subtab--active {
  border-color: var(--accent, #2563eb);
  background: var(--accent, #2563eb);
  color: #fff;
}
/* Per-service editor panels read as catalogue tiles: same border,
   radius, surface tint and hover-lift as .cat-card (the panel is a
   <details> so we style it in place rather than restructure it). */
.cs3-cards details.kit-panel {
  border: 1px solid var(--border);
  border-radius: var(--radius, 6px);
  background: var(--surface-1, transparent);
  transition: border-color 0.12s ease;
}
.cs3-cards details.kit-panel:hover { border-color: var(--accent); }
.cs3-cards details.kit-panel--warn {
  border-left: 3px solid var(--warn, #fbbf24);
}
.cs3-cards details.kit-panel--bad {
  border-left: 3px solid var(--bad, #f87171);
}
/* The panel header echoes the .cat-card-head row (icon-sized gap,
   monospace service name reading like .cat-card-name). */
.cs3-cards .kit-panel-title code { font-size: 0.95rem; font-weight: 600; }
/* Action buttons match the catalogue's btn-sm weight instead of the
   denser btn-mini, so "Enable"/"Add"/"Set" feel like the catalogue's
   Add/Enable/Disable. */
.cs3-cards .cs3-controls .btn-mini,
.cs3-cards .cs3-endpoint .btn-mini {
  padding: 0.3rem 0.7rem;
  font-size: 0.82rem;
}

/* STACKMAP-INGRESS-1 — flyout routes + control toggles */
.sm-routes { list-style: none; margin: .3rem 0 0; padding: 0; }
.sm-routes li { padding: .15rem 0; }
.sm-routes-off { opacity: .45; }
.mk-toggle { display: inline-flex; align-items: center; gap: .25rem;
             font-size: .78rem; color: var(--muted, #94a3b8);
             margin-left: .4rem; cursor: pointer; user-select: none; }

/* WIZARD-2 MR1: contexts step rows */
.ctx-rows { border: 0; padding: 0; margin: 0 0 1rem; }
.ctx-row { display: flex; align-items: center; gap: .75rem;
           flex-wrap: wrap; padding: .3rem 0; }
.ctx-row-name { min-width: 9rem; display: inline-flex;
                align-items: center; gap: .5rem; }
.ctx-row-zone select { min-width: 22rem; }
.ctx-row-custom input[type="text"] { width: 8rem; }

/* WIZARD-2 MR2: services step cards */
.svc-cat { margin: 1rem 0 .4rem; text-transform: capitalize; }
.svc-cards { display: grid; gap: .5rem;
             grid-template-columns: repeat(auto-fill,
                                           minmax(20rem, 1fr)); }
.svc-card { display: flex; gap: .6rem; align-items: flex-start;
            padding: .55rem .7rem; border: 1px solid
            var(--border, #3a3f4b); border-radius: .4rem;
            cursor: pointer; }
.svc-card-selected { border-color: var(--accent, #7aa2f7); }
.svc-card-base { opacity: .55; cursor: default; }
.svc-card-body { display: flex; flex-direction: column; gap: .15rem; }
.svc-card-title { display: flex; gap: .4rem; align-items: center;
                  flex-wrap: wrap; }
.svc-card-desc { font-size: .85em; }

/* WIZARD-2 MR4: flavour bundle chips */
.flavour-chips { display: grid; gap: .5rem;
                 grid-template-columns: repeat(auto-fill,
                                               minmax(16rem, 1fr));
                 margin-bottom: .5rem; }
.flavour-chip { display: flex; flex-direction: column; gap: .15rem;
                align-items: flex-start; text-align: left;
                padding: .5rem .7rem; border: 1px dashed
                var(--border, #3a3f4b); border-radius: .4rem;
                background: transparent; color: inherit;
                cursor: pointer; font: inherit; }
.flavour-chip:hover:not([disabled]) {
  border-color: var(--accent, #7aa2f7); border-style: solid; }
.flavour-chip-off { opacity: .45; cursor: not-allowed; }
.flavour-chip-name { font-weight: 600; }
.flavour-chip-desc { font-size: .8em; }

/* TANG-2: encrypted-at-rest badge on fleet VM rows. A small inline lock
   next to the VM name; green (ok) when the root is LUKS + auto-unlock is
   healthy, amber when at-risk (Tang unreachable / no binding). */
.enc-badge { display: inline-flex; vertical-align: middle; margin-left: .3em; }
.enc-badge svg { width: 0.95em; height: 0.95em; }
.enc-badge.enc-ok    { color: var(--good, #2ea043); }
.enc-badge.enc-at-risk { color: var(--warn, #d29922); }

/* OS-LIFECYCLE-1 — the machines-tab OS column: running OS + declared-image
   drift + latest/behind badges + the inline bump button. */
.os-cell { white-space: nowrap; }
.os-running { font-size: 0.85rem; }
.os-badge {
  display: inline-block;
  margin-left: 0.35rem;
  padding: 0.02rem 0.35rem;
  font-size: 0.62rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  border-radius: 0.25rem;
  vertical-align: middle;
}
.os-current { color: var(--ok, #34d399); border: 1px solid var(--ok, #34d399); }
.os-behind  { color: var(--warn); border: 1px solid var(--warn); }
.os-pending { color: var(--accent); border: 1px solid var(--accent); }
.os-bump-form, .os-relup-form { display: inline; margin-left: 0.35rem; }
/* The two OS actions sit together on ONE row BELOW the OS+badge line. */
.os-actions { display: flex; align-items: center; gap: 0.35rem; margin-top: 0.3rem; }
.os-actions form { margin-left: 0; }
.os-bump-all-form { margin: 0.25rem 0 0.75rem; }
.btn-xs { padding: 0.05rem 0.4rem; font-size: 0.68rem; }


/* PEOPLE-1 verdict banner. The People tab's counts alone are
   ambiguous — "0 unexpected, 0 stale" is what a clean fleet, an
   unreachable fleet and a months-old audit all look like — so one
   banner states which, above any policy table. Colour comes from the
   semantic set, not the accent: this is a state, not branding. */
.people-verdict {
  display: flex; align-items: flex-start; gap: 0.75rem;
  padding: 0.8rem 1rem; margin: 0 0 1rem;
  border: 1px solid var(--border);
  border-left-width: 3px;
  border-radius: 4px;
  background: var(--bg-surface-hover);
}
.people-verdict .pv-state { flex: 0 0 auto; }
.people-verdict .pv-body { display: flex; flex-direction: column; gap: 0.2rem; }
.people-verdict .pv-body strong { font-size: 0.95rem; }
.people-verdict-findings   { border-left-color: var(--status-failed, #d9534f); }
.people-verdict-unverified { border-left-color: var(--text-muted); }
.people-verdict-stale      { border-left-color: var(--status-queued, #c8a020); }
.people-verdict-converging { border-left-color: var(--status-queued, #c8a020); }
.people-verdict-clean      { border-left-color: var(--status-succeeded, #3f9d58); }

/* People audit: findings lead, the rest folds. The flat table put 23
   machines x ~3 accounts x 5 missing names on screen at once — the
   least actionable content dominating the page while a real finding
   could scroll past unseen. */
.audit-fold { margin: 0.6rem 0; }
.audit-fold > summary {
  cursor: pointer; font-size: 0.86rem; color: var(--text-muted, var(--muted));
  padding: 0.35rem 0;
}
.audit-fold > summary:hover { color: var(--text-primary); }
.audit-fold[open] > summary { margin-bottom: 0.3rem; }
.people-audit-table td { vertical-align: top; }
.people-audit-table td:first-child { white-space: nowrap; }
/* an ssh error is one long line — let it wrap instead of stretching
   the table past the viewport */
.audit-err { white-space: normal; word-break: break-word; max-width: 60ch; }
.audit-allclear { margin: 0.4rem 0; }

/* Fleet VMs: the CPU/RAM cells must occupy the SAME height whether or
   not the row has telemetry. A row with a meter + sparkline is taller
   than one showing a bare "—", and since every cell aligns to the top
   of the row box, the taller row makes its neighbour's Actions group
   look dropped — the ragged column Richard spotted twice. Reserving
   the height makes row rhythm independent of whether a VM happens to
   be running and reporting. */
table.fleet-grid .col-cpu,
table.fleet-grid .col-mem { height: 2.9rem; }
table.fleet-grid .col-cpu .cell-stack {
  min-height: 2.1rem;
  justify-content: flex-start;
}
/* the "no data" dash should sit where the meter would, not on the
   cell's text baseline */
table.fleet-grid .col-cpu > .cell-stack > .muted,
table.fleet-grid .col-mem > .muted {
  display: inline-block; line-height: 1.15rem;
}

/* Actions column: split-buttons and plain buttons must share a top
   edge. A .split-action's caret carries padding 0.5rem/0.55rem while a
   plain .btn carries 0.3em/0.75em, and align-items:stretch lets the
   caret set the group's height — so the split pairs render a shade
   taller than Preview/Reprovision and vertical-align:middle offsets
   the two kinds by ~1px. Aligning to the TOP of the line box instead
   makes both kinds start at the same y regardless of which is taller,
   and matching the caret's padding-block to the button's removes the
   height difference at source. */
td.fleet-actions .split-action,
td.fleet-actions form { vertical-align: top; }
td.fleet-actions .split-menu > summary {
  padding-top: 0.3em;
  padding-bottom: 0.3em;
  font-size: 0.82em;
}
/* A status pill standing in for the action group (an unmanaged VM has
   no lifecycle actions) sits on the text baseline while its sibling
   buttons align to the cell top — a 2px step in the same column.
   Measured, not eyeballed: the pill reported top=720.6 against the
   buttons' 718.6. */
/* A plain label standing in for the action group (an unmanaged VM has
   no lifecycle actions) sat on the text baseline while its sibling
   buttons aligned to the cell top. Give it the buttons' own box —
   same height, contents centred — so the label's TEXT lines up with
   the buttons' text rather than its box lining up with their box.
   (A first pass set line-height alone and made the gap worse: the box
   grew and the text stayed at its top. Measured: 2px -> 4px.) */
td.fleet-actions > .status-pill,
td.fleet-actions > .hint,
td.fleet-actions > span {
  vertical-align: top;
  display: inline-flex;
  align-items: center;
  height: 28.4px;
}

/* HELPER-1 — the per-page CLI / MCP / API cheat sheet. A right-side
   sheet rather than a modal: the operator is comparing it AGAINST the
   page ("which command does this button run?"), so the page must stay
   visible. */
.cfg-head-helper {
  margin-left: auto; align-self: flex-start;
  background: transparent; border: 1px solid var(--border);
  border-radius: var(--radius); color: var(--text-muted);
  padding: 0.3rem 0.45rem; cursor: pointer; line-height: 1;
}
.cfg-head-helper:hover { color: var(--text-primary); border-color: var(--border-light); }
.cfg-head-helper:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Anchored to the TRIGGER, not the viewport edge. A right-edge sheet
   is a long way from the icon you clicked on a wide monitor — the
   panel reads as unrelated to the thing that opened it, and on an
   ultrawide it is off in the periphery. JS positions this under the
   button; these are the fallbacks if it cannot. */
.helper-host {
  position: absolute;
  width: min(52rem, calc(100vw - 3rem));
  max-height: min(70vh, 44rem);
  z-index: 60; overflow-y: auto;
  background: var(--bg-surface);
  border: 1px solid var(--border-light);
  border-radius: var(--radius);
  box-shadow: 0 12px 34px rgba(0, 0, 0, 0.45);
}
.helper-host::before {
  /* a small caret pointing back at the button that opened it */
  content: ""; position: absolute; top: -6px; width: 10px; height: 10px;
  left: var(--helper-caret, 2rem);
  background: var(--bg-surface);
  border-left: 1px solid var(--border-light);
  border-top: 1px solid var(--border-light);
  transform: rotate(45deg);
}
.helper-panel { padding: 1rem 1.2rem 2rem; }
.helper-hdr {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 1rem; padding-bottom: 0.6rem; margin-bottom: 0.8rem;
  border-bottom: 1px solid var(--border);
}
.helper-hdr .hint { display: block; margin-top: 0.15rem; }
.helper-sect { margin-bottom: 1rem; }
.helper-sect h4 { margin: 0 0 0.3rem; font-size: 0.9rem; }
.helper-cfg { list-style: none; margin: 0.4rem 0 0; padding: 0; }
.helper-cfg li {
  display: flex; flex-direction: column; gap: 0.1rem;
  padding: 0.4rem 0; border-bottom: 1px solid var(--border-soft, var(--border));
}
.helper-note { font-style: italic; }
.helper-group { margin: 0.5rem 0; }
.helper-group > summary {
  cursor: pointer; padding: 0.35rem 0; font-size: 0.88rem;
}
.helper-table { width: 100%; margin-top: 0.4rem; }
.helper-table td { vertical-align: top; font-size: 0.82rem; }
.helper-what { max-width: 22rem; }
/* A runnable command is the thing people came for: give it the
   affordance of a control, not of body text. */
.helper-cmd {
  cursor: pointer; display: inline-block;
  padding: 0.12rem 0.4rem; margin: 0.1rem 0;
  border: 1px solid transparent; border-radius: 3px;
  background: var(--bg-surface-hover);
  font-size: 0.82rem; white-space: nowrap;
}
.helper-cmd:hover {
  border-color: var(--accent); color: var(--text-bright);
}
.helper-cmd:active { transform: translateY(1px); }
.helper-table th {
  position: sticky; top: 0; z-index: 1;
}
.helper-group[open] > summary { border-bottom: 1px solid var(--border); }
.helper-group > summary:hover { color: var(--text-primary); }
.helper-hdr strong { font-size: 0.95rem; }
.helper-cfg li:last-child { border-bottom: none; }
.helper-cfg code {
  align-self: flex-start; margin-bottom: 0.1rem;
}
.helper-empty, .helper-foot { margin-top: 1rem; }
@media (max-width: 700px) {
  /* on a narrow screen anchoring buys nothing — go full width under
     the header rather than a popover that overflows. */
  .helper-host { position: fixed; inset: auto 0 0 0; width: 100vw;
                 max-height: 80vh; border-radius: 0; }
  .helper-host::before { display: none; }
}
/* HELPER-1: a CLI cell that is PROSE, not a runnable command. It must
   not look copy-pasteable — pasting "micro people offboard (no --now)"
   into zsh gives "unknown sort specifier" (reported live). Italic, no
   code styling, no copy affordance. */
.helper-prose { font-style: italic; opacity: 0.75; }

/* ── CONTAINERS-TAB-1 ────────────────────────────────────────────────
   The Fleet ▸ Containers tab and its Explore panel.

   These class names were used by the templates before any of them
   existed in CSS, so the tab rendered as a stack of unstyled blocks:
   every project heading and every machine header sat at default
   paragraph spacing, and a machine with nothing running cost three
   lines to say so. Naming a class does not style it — check the
   rendered page, not the markup. */

/* A project heading: the group label, not a document heading. */
.stack-h3 {
  margin: 1.4em 0 0.5em;
  font-size: 1.05rem;
  display: flex;
  align-items: baseline;
  gap: 0.6em;
  flex-wrap: wrap;
  border-bottom: 1px solid var(--border, #333);
  padding-bottom: 0.35em;
}

/* One machine within a project. Indented so the project → machine →
   container nesting is legible without drawing three boxes. */
.stack-machine { margin: 0 0 0.9em 0; padding-left: 0.6em;
                 border-left: 2px solid var(--border, #333); }

/* The machine's own line: name, context, state pill, probe age. It is
   a ROW — a dark machine is exactly this one line and nothing else,
   which is what keeps 15 un-probed boxes from burying the data. */
.stack-machine-head {
  display: flex;
  align-items: center;
  gap: 0.5em;
  flex-wrap: wrap;
  padding: 0.3em 0;
}
.stack-machine-head strong { font-size: 0.95rem; }

/* The per-row Explore/Logs panel target. It carries no styling of its
   own — an EMPTY row must be invisible, or every container row would
   render a blank gap under it. */
.container-panel-row > td { padding: 0; border: 0; }
.container-panel-row:empty { display: none; }

/* The Explore panel itself. */
.container-inspect { margin: 0.6em 0 1em; }
.container-inspect h4 {
  margin: 1.1em 0 0.4em;
  font-size: 0.9rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  opacity: 0.8;
}
/* `details` for the raw document: closed by default, and the summary
   should read as a control. */
.container-inspect details > summary { cursor: pointer; padding: 0.4em 0; }
.container-inspect .log-pre { max-height: 22em; overflow: auto; }

/* Key/value readout — a two-column table where the key column is a
   fixed, quiet label rather than half the width. This is what made the
   inspect panel's left column run behind the following heading. */
.kv-table { width: 100%; }
.kv-table th {
  width: 12em;
  text-align: left;
  font-weight: 500;
  opacity: 0.75;
  white-space: nowrap;
  vertical-align: top;
}
.kv-table td { vertical-align: top; }

/* `.btn-sm` — the compact in-row button. Used by ~15 templates
   (catalogue, builds, admin config, container actions) and, like
   `.kpi-row`, never defined: every one rendered at full `.btn` size,
   which is what made action-bearing table rows ~65px tall when their
   text needs ~34px. The mobile rule at the bottom of this file already
   referenced `.btn-sm` expecting it to exist. */
.btn-sm {
  padding: 0.25rem 0.55rem;
  font-size: 0.82rem;
  gap: 0.3rem;
  border-radius: calc(var(--radius) * 0.75);
}

/* ── Containers tab density ──────────────────────────────────────────
   A fleet-wide container list is a SCANNING surface: the operator is
   looking for one row among hundreds, so vertical rhythm matters more
   than breathing room. These override the default `table.grid` padding
   for this table only. */
table.grid.cargo-containers th,
table.grid.cargo-containers td {
  padding: 0.3rem 0.7rem;
  vertical-align: middle;
}
/* Actions sit on one line and never wrap a button onto a second row —
   a wrapped action cell doubles the height of the whole row. */
.cargo-container-actions {
  display: flex;
  gap: 0.3rem;
  align-items: center;
  flex-wrap: nowrap;
  white-space: nowrap;
}
/* Ports: a machine-readable list that must not push the row wide.
   Each mapping is its own chip so `80->8080` reads as one unit. */
.port-chip {
  display: inline-block;
  font-family: var(--font-mono, monospace);
  font-size: 0.75rem;
  padding: 0.05rem 0.35rem;
  margin: 0 0.2rem 0.1rem 0;
  border-radius: 3px;
  background: var(--bg-surface-hover);
  color: var(--text-muted);
  white-space: nowrap;
}
/* A published port is reachable from OUTSIDE the host — worth seeing
   at a glance among the internal-only ones. */
.port-chip.is-published { color: var(--text-primary); }

/* The Containers tab's filter bar. */
.cf-bar {
  display: flex;
  gap: 0.5rem;
  align-items: center;
  flex-wrap: wrap;
  margin: 0 0 0.8em 0;
}
.cf-bar input[type="search"] { flex: 1 1 22em; min-width: 12em; }
.cf-bar select { flex: 0 0 auto; }

/* Filtered-out rows. A CLASS, not the `hidden` attribute: a <tr>
   carries an author `display: table-row`, and author styles beat the
   UA's `[hidden] { display: none }` — the trap that left the
   catalogue's facet chips filtering nothing. `!important` for the same
   reason the .cat-card guard needs it. */
.cf-hidden { display: none !important; }

/* Inspect panel: the environment fold. */
.env-fold > summary { cursor: pointer; padding: 0.35em 0; }
.env-fold .table-scroll { max-height: 24em; overflow: auto; margin-top: 0.4em; }
.env-table th { width: 18em; }
.env-table td, .env-table th { padding: 0.2rem 0.6rem; }
/* A redacted value is a STATE, not a value — it should read as
   deliberately withheld rather than as the literal text of a variable. */
.env-redacted { opacity: 0.65; font-style: italic; }

/* Numeric table cells: right-aligned with lining figures so columns of
   counts line up. Used by the Swarm tab's manager/worker/tolerates
   columns. */
.num { text-align: right; font-variant-numeric: tabular-nums; }

/* A form that sits INSIDE a table cell alongside others — it must not
   claim a block of its own or each one stacks and the row triples in
   height. */
.inline-form {
  display: inline-flex;
  gap: 0.25rem;
  align-items: center;
  margin: 0 0.3rem 0.15rem 0;
}
.inline-form input[type="text"],
.inline-form input[type="number"],
.inline-form select { padding: 0.2rem 0.4rem; font-size: 0.82rem; }
