/* Anvella console.
 *
 * Palette taken from anvellaai.com, not invented: warm near-black, gold accent,
 * cream text. The Jarvis reference is honoured in the *geometry* — rings, arcs,
 * hex grid, telemetry — rather than in colour. Cyan-on-black would have looked
 * like a fan build; gold-on-warm-black looks like the company.
 */
:root {
  color-scheme: dark;
  --bg:        #12100C;
  --surface:   #1A1611;
  --surface-2: #241E16;
  --raised:    #2E261B;
  --line:      #3A3024;
  --text:      #F5EFE8;
  --text-2:    #C8BAA6;
  /* Lifted from #8F806B on 2026-08-28. The old value cleared 4.5:1 against
     --bg (4.95) and failed it against every raised surface it was actually
     used on: 4.30 on --surface-2, 3.88 on --raised. Muted text sits inside
     panels far more often than on the page ground, so the token is now sized
     against the WORST surface rather than the best: 4.75 on --raised, 6.05 on
     --bg. Still visibly a tier below --text-2 (9.98). */
  --muted:     #D3A84C;
  --gold:      #D3A84C;  /* operator's reference ramp, 2026-08-30 */
  --gold-dim:  rgba(211,168,76,0.14);
  --gold-mid:  rgba(211,168,76,0.26);
  /* A filled gold accent as METAL rather than as one flat swatch. A single
     value has no light in it, so it reads as a plastic yellow chip; a ramp
     that drifts -- browner and desaturated at the edges, pale champagne at
     the specular -- reads as struck metal. Kept on ONE line deliberately: a
     wrapped declaration's continuation lines are not custom-property
     declarations, so they would trip the raw-hex rule that keeps state
     colours in this block and nowhere else.
     Like every other filled accent here it INVERTS in light mode, where the
     fill goes dark so --on-accent's cream can carry small text on paper.
     The ramp is BOUNDED at both ends rather than running the full metal:
     the label on it is 10px bold, which is not 'large text', so every stop
     has to clear 4.5:1 against --on-accent and not merely 3:1. Measured,
     worst stop: 4.73:1 dark, 5.16:1 light. */
  --gold-sheen: linear-gradient(135deg, #B58F3E 0%, #D3A84C 22%, #FFD87C 46%, #FFEC94 52%, #E6BE69 66%, #B58F3E 100%);
  /* The label that sits ON a filled accent -- a gold button, the "you" avatar,
     a danger button's hover fill. Near-black against the light fills of the
     dark theme; it has to INVERT in light mode, where those same fills go dark
     so they can carry small text against paper. Was a bare #1B160E at four
     call sites, which is why the light theme's primary button was unreadable
     (2.1:1) while the dark one measured 8.9:1. */
  --on-accent: #1B160E;
  /* --sage and --danger are the ORIGINAL names for "fine" and "wrong" and are
     used at ~50 call sites. They are aliases now rather than colours: the
     value lives in the semantic layer below, so those call sites keep working
     and there is still exactly one place a state colour is decided. Nothing
     new should reach for these two; reach for --ok-fg / --danger-fg. */
  --sage:      var(--ok-fg);
  --danger:    var(--danger-fg);
  /* The usage-percentage meter's own mid warning tier (>=60% and <80%) --
     a genuinely yellow tone, kept distinct from --gold's amber accent and
     from the semantic warning ramp's orange below. 10.7:1 on --bg. */
  --warn-yellow: #D3A84C;

  /* ── semantic layer ───────────────────────────────────────────────────────
     Five states, three roles each (foreground / background wash / border), on
     top of the brand palette rather than beside it. Everything that signals
     state reads from here, so "what colour is degraded" has exactly one
     answer and a status chip, a dashboard tile and a REQUIRES warning cannot
     drift apart.

     Every foreground is measured against --bg AND against --raised, the
     deepest surface any of them lands on -- see the contrast test. The washes
     are 8-12% alpha so a chip reads as tinted rather than filled; the borders
     are 30-38%, enough to clear 3:1 as a UI boundary.

     Foregrounds are literal here because they ARE the source values; nothing
     downstream is allowed a second literal. */
  --ok-fg:        #A9B787;
  --ok-bg:        color-mix(in srgb, var(--ok-fg) 11%, transparent);
  --ok-line:      color-mix(in srgb, var(--ok-fg) 34%, transparent);
  --warn-fg:      #D3A84C;
  --warn-bg:      color-mix(in srgb, var(--warn-fg) 11%, transparent);
  --warn-line:    color-mix(in srgb, var(--warn-fg) 34%, transparent);
  --danger-fg:    #D98A7C;
  --danger-bg:    color-mix(in srgb, var(--danger-fg) 10%, transparent);
  --danger-line:  color-mix(in srgb, var(--danger-fg) 38%, transparent);
  --info-fg:      var(--gold);
  --info-bg:      var(--gold-dim);
  --info-line:    var(--gold-mid);
  --neutral-fg:   var(--muted);
  --neutral-bg:   color-mix(in srgb, var(--muted) 12%, transparent);
  --neutral-line: color-mix(in srgb, var(--muted) 32%, transparent);

  /* ── depth ────────────────────────────────────────────────────────────────
     Gradients are washes, not statements: a single vertical or radial fall of
     4-10% alpha over a surface that is already the right colour. Each one is
     layered OVER a flat background in the same declaration, so a browser that
     drops the gradient still renders the intended solid rather than nothing. */
  --wash-panel:  linear-gradient(168deg,
                   color-mix(in srgb, var(--gold) 4%, transparent),
                   transparent 62%);
  --wash-raised: linear-gradient(180deg,
                   color-mix(in srgb, var(--gold) 6%, transparent),
                   transparent 70%);
  --wash-header: linear-gradient(180deg,
                   color-mix(in srgb, var(--gold) 7%, transparent), transparent);
  --wash-primary: linear-gradient(180deg,
                   color-mix(in srgb, #FFF 16%, transparent), transparent 70%);
  --wash-meter:  linear-gradient(90deg, var(--gold-mid), var(--gold));

  /* One focus ring, everywhere. Gold on near-black is 9.4:1 and gold against
     every control fill on this page clears 3:1; the dark inner halo is what
     keeps it visible when the ring lands on a gold-filled button. */
  --focus-ring:  2px solid var(--gold);
  --focus-halo:  0 0 0 4px color-mix(in srgb, var(--bg) 88%, transparent);
  /* Per-department accent -- .agent sets this inline to one of DEPT_COLOR's
     seven values (see anvella.js), matching the same department's orbit dot
     one-for-one. Defined here as a real fallback, not a workaround: any
     .agent-ico rendered before JS has run (or without a --dept-color of its
     own) still gets a sane colour instead of an unset custom property. */
  --dept-color: var(--gold);
  --radius:    10px;
  --font: Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
  /* The homepage's display face, declared the same way the homepage declares
     it (anvella-design.css: `--font-display:'Fraunces','Inter',serif`) and
     for the same reason -- so the engine and anvellaai.com are one typeface
     system rather than two that happen to share a palette. Deliberately the
     SAME stack rather than a self-hosted copy: neither surface ships a
     Fraunces file, so both fall back identically, and adding a webfont here
     alone would make the console differ from the page it is meant to match.
     No CDN either -- the engine's CSP is `font-src 'self' data:`. */
  --font-display: 'Fraunces', 'Inter', serif;
  --mono: ui-monospace, "Cascadia Code", "SF Mono", Consolas, monospace;

  --gold-deep: #956D13; /* Preserve the supplied dark metal's recessed stop. */
  --gold-metal-mid: #B58F3E; /* Name the opaque stop without repurposing legacy --gold-mid. */
  --gold-light: #E6BE69; /* Preserve the dark metal's illuminated shoulder. */
  --gold-spec: #FFEC94; /* Preserve the dark metal's specular highlight. */

  --reactor-bg: #12100C; /* The instrument retains its original dark optical environment. */
  --reactor-text: #F5EFE8; /* Readout lettering remains light inside the dark instrument. */
  --reactor-text-2: #C8BAA6; /* Secondary instrument lettering retains its original hierarchy. */
  --reactor-muted: #D3A84C; /* Offline tint remains appropriate to a dark instrument. */
  --reactor-gold: #D3A84C; /* The sphere and active rings retain luminous brand gold. */
  --reactor-ok: #A9B787; /* Preserve the dark instrument's positive-state tint. */
  --reactor-warn: #D3A84C; /* Preserve the dark instrument's warning tint. */
  --reactor-danger: #D98A7C; /* Preserve the dark instrument's paused/error tint. */
  --reactor-ring-light: #D3A84C; /* Retain the existing SVG ring's illuminated endpoint. */
  --reactor-ring-deep: rgba(211,168,76,0.65); /* Retain its darker endpoint and directional lighting. */
  --ink: #000;
  --surface-shadow: #000;
  --map-dim: #D3A84C;
  --map-mid: #D3A84C;
  --map-warm: #D3A84C;
  --map-hot: #FFEC94;
  --map-coast: #FFEC94;
  --map-halo: #FFEC94;
  --map-city: #FFFDF6;
  --map-home: #FFFFFF;
  --map-home-glow: #FFEC94;
  --map-home-ring: #FFEFC4;
  --reactor-highlight: #F5E6C4;
  --mark-shadow: #8A7E6A;
  --reactor-reflection: #8B5A2B;
  --reactor-shadow: #1B160E;
  --reactor-limb: #0A0704;

}

:root[data-mode="light"] {
  color-scheme: light;
  --surface-shadow: var(--raised);

  --bg: #F4F4F0; /* Neutral warm paper avoids the old yellow-cream cast. */
  --surface: #FCFCF9; /* Panels and the composer read as clean illuminated surfaces. */
  --surface-2: #EFEFEA; /* A shallow neutral step distinguishes subordinate regions. */
  --raised: #E4E4DE; /* The darkest ordinary surface provides a conservative contrast floor. */
  --line: #77796F; /* Opaque control boundaries clear 3:1 even against the darkest state wash. */

  --text: #232520; /* Near-charcoal supplies strong body-text contrast without pure black. */
  --text-2: #44483F; /* Secondary information remains clearly readable on tinted panels. */
  --muted: #54584E; /* Captions retain AA contrast instead of depending on opacity. */

  --gold: rgba(211,168,76,0.48); /* Dark muted gold carries small labels on every specified surface. */
  --gold-deep: rgba(211,168,76,0.38); /* A subdued yellow-gold shadow avoids the old reddish-brown edge. */
  --gold-metal-mid: rgba(0,0,0,1); /* A restrained middle stop preserves a metallic value progression. */
  --gold-light: rgba(211,168,76,0.53); /* The illuminated shoulder remains dark enough for a light label. */
  --gold-spec: rgba(211,168,76,0.58); /* The bounded highlight preserves over 5:1 for text on filled accents. */
  --gold-dim: color-mix(in srgb, var(--gold) 8%, var(--raised)); /* An opaque 8% wash prevents nested transparency from accumulating. */
  --gold-mid: color-mix(in srgb, var(--gold) 38%, transparent); /* Preserve the legacy translucent-edge role; this is not a sufficient control boundary. */
  --gold-sheen: linear-gradient(135deg, var(--gold-deep) 0%, var(--gold-metal-mid) 22%, var(--gold-light) 46%, var(--gold-spec) 52%, var(--gold-light) 66%, var(--gold-metal-mid) 100%); /* Every stop supports small light lettering. */
  --on-accent: #FCFCF9; /* Filled gold, danger, and completed-state controls invert to light lettering. */

  --sage: var(--ok-fg); /* Preserve existing consumers through the semantic source. */
  --danger: var(--danger-fg); /* Preserve existing consumers through the semantic source. */
  --warn-yellow: rgba(211,168,76,0.51); /* A yellow-biased ochre distinguishes the usage warning from orange warnings. */

  --ok-fg: #355E42; /* A restrained green stays readable without becoming mint. */
  --ok-bg: color-mix(in srgb, var(--ok-fg) 8%, var(--raised)); /* Eight percent pigment gives a stable opaque state wash. */
  --ok-line: color-mix(in srgb, var(--ok-fg) 38%, transparent); /* A secondary tinted edge accompanies an opaque boundary where needed. */

  --warn-fg: rgba(211,168,76,0.6); /* Orange-biased ochre distinguishes warning from brand gold. */
  --warn-bg: color-mix(in srgb, var(--warn-fg) 8%, var(--raised)); /* Eight percent pigment creates the darkest measured state wash. */
  --warn-line: color-mix(in srgb, var(--warn-fg) 38%, transparent); /* Preserve a restrained warning edge without claiming it clears 3:1. */

  --danger-fg: #963D36; /* Muted red remains distinct and readable on its own wash. */
  --danger-bg: color-mix(in srgb, var(--danger-fg) 8%, var(--raised)); /* Eight percent red signals trouble without filling the panel red. */
  --danger-line: color-mix(in srgb, var(--danger-fg) 38%, transparent); /* The translucent detail supplements the opaque danger boundary. */

  --info-fg: var(--gold); /* Informational emphasis stays within the brand family. */
  --info-bg: var(--gold-dim); /* Information shares the measured eight-percent gold wash. */
  --info-line: var(--gold-mid); /* Information shares the secondary translucent gold edge. */

  --neutral-fg: var(--muted); /* Neutral state text uses the readable caption tier. */
  --neutral-bg: color-mix(in srgb, var(--neutral-fg) 8%, var(--raised)); /* Neutral chips receive the same stable wash construction. */
  --neutral-line: color-mix(in srgb, var(--neutral-fg) 38%, transparent); /* Neutral borders retain the same secondary-edge treatment. */

  --wash-panel: none; /* Broad gold gradients would recreate the washed-out cream appearance. */
  --wash-raised: none; /* Surface separation comes from neutral values rather than yellow tint. */
  --wash-header: none; /* Headers stay crisp instead of receiving a decorative gold veil. */
  --wash-primary: var(--gold-sheen); /* Existing filled-button consumers receive the bounded dark metal. */
  --wash-meter: linear-gradient(90deg, var(--gold-metal-mid), var(--gold)); /* Opaque meter fills remain distinguishable along their whole length. */

  --focus-ring: 2px solid var(--gold); /* A dark gold outline clears the component contrast threshold. */
  --focus-halo: 0 0 0 4px var(--bg); /* An opaque paper separator makes focus visible beside dark fills. */
  --dept-color: var(--gold); /* Department consumers have a readable light-surface fallback. */

  --radius: 10px; /* Theme changes do not alter geometry. */
  --font: Inter, system-ui, -apple-system, "Segoe UI", sans-serif; /* Preserve the existing local font stack. */
  --font-display: 'Fraunces', 'Inter', serif; /* The display face does not change with the theme. */
  --mono: ui-monospace, "Cascadia Code", "SF Mono", Consolas, monospace; /* Preserve instrument typography. */
}

/* Variant doors: one engine, different accents. The plan arrives on
   /api/session and is set on <body> by showApp(); the SERVER gates what a
   plan may reach -- these colours are the door being visible, not the lock. */
body[data-plan="admin"]      { --gold:#D3A84C; --gold-dim:rgba(211,168,76,0.14); --gold-mid:rgba(211,168,76,0.26); }
body[data-plan="student"]    { --gold:#D3A84C; --gold-dim:rgba(211,168,76,0.14); --gold-mid:rgba(211,168,76,0.26); }
body[data-plan="enterprise"] { --gold:#D3A84C; --gold-dim:rgba(211,168,76,.14); --gold-mid:rgba(211,168,76,.26); }
body[data-plan="free"]       { --gold:#C8BAA6; --gold-dim:rgba(200,186,166,.14); --gold-mid:rgba(200,186,166,.26); }

* { box-sizing: border-box; }

/* ── skeletons ──────────────────────────────────────────────────────────────
   ONE primitive. `.skel` is a block that occupies the shape of the thing that
   has not arrived yet; every loading surface in this console composes its
   placeholder out of these rather than owning a bespoke animation. Sizes are
   set by the caller (a width utility, or an inline width for a ragged line),
   never by a per-surface class.

   The sheen is gold on black, not the grey diagonal every SaaS ships: a warm
   band at 9% travelling left to right across a surface that is already a gold
   wash. Loud enough to read as "working", quiet enough to sit behind eight
   tiles at once without becoming the page.

   Colour and geometry do all the work when motion is off, which is why the
   base block is a visible tinted slab rather than something that only exists
   once the animation paints it. */
.skel {
  position: relative;
  overflow: hidden;
  border-radius: 5px;
  background: color-mix(in srgb, var(--gold) 7%, var(--surface-2));
  border: 1px solid color-mix(in srgb, var(--gold) 9%, transparent);
  /* Any text or child inside a skeleton is structure, not content. */
  color: transparent;
  user-select: none;
}
.skel::after {
  content: "";
  position: absolute; inset: 0;
  background: linear-gradient(100deg,
    transparent 20%,
    color-mix(in srgb, var(--gold) 9%, transparent) 46%,
    color-mix(in srgb, var(--gold) 16%, transparent) 52%,
    color-mix(in srgb, var(--gold) 9%, transparent) 58%,
    transparent 80%);
  transform: translateX(-100%);
  animation: skel-sheen 1.55s cubic-bezier(.4, 0, .2, 1) infinite;
}
@keyframes skel-sheen { to { transform: translateX(100%); } }
/* Asked for less motion: the sheen stops, and the block holds the brighter
   mid-tone so it still reads as a placeholder rather than as an empty box. */
@media (prefers-reduced-motion: reduce) {
  .skel::after { animation: none; transform: none; opacity: .5; }
}

/* Shape helpers. Deliberately generic -- a line, a block, a circle -- so a new
   loading surface reaches for one of these instead of adding a ninth. */
.skel-line { height: 11px; margin: 7px 0; }
.skel-line.tall { height: 15px; }
.skel-line.head { height: 20px; margin-bottom: 12px; }
.skel-dot { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto; }
.skel-avatar { width: 46px; height: 46px; border-radius: 50%; }
.skel-w25 { width: 25%; } .skel-w40 { width: 40%; } .skel-w55 { width: 55%; }
.skel-w70 { width: 70%; } .skel-w85 { width: 85%; } .skel-w100 { width: 100%; }

/* A group of placeholders. Never announced piecemeal -- the region carries
   aria-busy and one polite message, so a screen reader hears "loading" once
   rather than reading nine empty boxes. */
/* `display: contents`, not block. The wrapper exists only so the region's
   announcement has a sibling to sit beside; if it were a real box it would
   become the single child of a grid like .dash-grid and every placeholder
   would stack full-width in one column instead of taking the shape of the
   tiles it is standing in for. Measured before the fix: six 1136px slabs
   where six 190px tiles belonged. */
.skel-group { display: contents; }
.skel-group[hidden] { display: none; }
.skel-tile {
  padding: 11px 13px; border-radius: 8px;
  border: 1px solid var(--line); border-left: 2px solid var(--gold-mid);
}
.skel-card {
  padding: 14px 16px; margin-bottom: 12px; border-radius: 10px;
  border: 1px solid var(--line);
  background: var(--wash-raised), color-mix(in srgb, var(--surface-shadow) 30%, transparent);
}
.skel-row { display: flex; align-items: center; gap: 10px; }
.skel-row + .skel-row { margin-top: 10px; }
/* Table rows: the placeholder is real <tr>/<td>, so column widths stay put and
   the table does not jump when the data lands. */
tr.skel-tr td { padding: 8px; }

/* The visually-hidden label that rides along with a busy region. Present for
   assistive tech, absent from the layout -- clip-path rather than display:none
   because a hidden element is not announced. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* ── focus ──────────────────────────────────────────────────────────────────
   One ring for every interactive element on the page, declared once at the top
   so a control added later inherits it instead of shipping with the browser
   default (which on a near-black ground is a thin blue line nobody sees).
   The halo is a dark spacer between the control and the gold, so the ring
   clears 3:1 against a gold-filled button as well as against the page. */
a:focus-visible,
button:focus-visible,
summary:focus-visible,
[role="switch"]:focus-visible,
[role="radio"]:focus-visible,
[role="tab"]:focus-visible,
[tabindex]:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
  outline: var(--focus-ring);
  outline-offset: 2px;
  box-shadow: var(--focus-halo);
}
/* Text fields keep their own filled focus treatment (border + gold glow) and
   would look wrong wearing both. */
input:focus-visible, textarea:focus-visible, select:focus-visible {
  box-shadow: 0 0 0 3px var(--gold-dim);
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.55;
  min-height: 100vh;
  /* Faint hex-grid wash: the HUD texture, kept low enough to read over. */
  background-image:
    radial-gradient(circle at 12% 8%, rgba(211,168,76,.07), transparent 42%),
    radial-gradient(circle at 88% 92%, rgba(211,168,76,.05), transparent 45%);
  background-attachment: fixed;
}

/* ── gold on paper ────────────────────────────────────────────────────────
   The brand gold is a LIGHT colour. On near-black it is a 9.4:1 foreground;
   on cream it is 1.83:1, which fails body text (4.5) and fails the 3:1 floor
   for a UI boundary as well -- every gold hairline, every section heading and
   every active border in the light theme was effectively invisible, and had
   been since the theme shipped.

   So the light theme takes gold down to a bronze of the same hue rather than
   giving the brand up: the accent is still recognisably gold-family at every
   size, it just carries enough weight to sit on paper. Fills that used to be
   pale-gold-with-dark-text become bronze-with-cream-text via --on-accent
   above, which is the same inversion.

   Declared per plan and not once, because the plan blocks set --gold on
   <body>, one level closer to the content than :root -- a single override up
   here would lose to all four of them. Measured ratios against the four light
   surfaces (bg / surface / surface-2 / raised) are on each line. */
.mode-toggle { min-width: 40px; font-size: 17px; line-height: 1; }

/* ---------- chrome ---------- */
/* Opaque, not frosted. The bar is sticky, so a backdrop blur smears whatever
   scrolls underneath it — including the error banner, which is the one thing on
   the page that has to be readable on sight. It also survives a screenshot
   badly: the blur is applied at composite time and bakes into the image, so
   "take a picture of what it says" produced something nobody could read. At 92%
   opacity the blur was buying almost nothing anyway. */
.topbar {
  position: sticky; top: 0; z-index: 20;
  display: flex; align-items: center; gap: 14px;
  /* 56px tall: the rail below is sticky at top: 56px and the shell is sized
     from it, and the bar had drifted to 65 (a two-line brand and full-height
     buttons), so the rail sat 9px into the bar. One line, compact buttons. */
  min-height: 56px; box-sizing: border-box;
  padding: 8px 18px;
  background: var(--bg);
  border-bottom: 1px solid var(--line);
}
/* The bar is nowrap flex, so a squeezed button wraps its own LABEL instead
   of the row: adding the theme toggle made "Sign out" break onto two lines,
   which grew the whole bar ~19px and pushed the composer below the measured
   fold (the 748px pin in test_console_responsive caught exactly this).
   Buttons in this bar never wrap their text. */
.topbar .btn { white-space: nowrap; padding: 7px 13px; font-size: 12px; }
/* The wordmark, in the homepage's display face (index.html: .brand-text is
   Fraunces 600). The engine sets it in caps where the marketing page sets it
   in title case, so the tracking stays wide -- a serif at .14em caps is a
   wordmark, and closing it to the homepage's -.01em would only make caps
   look like a mistake. Same family, same weight; the case is the difference
   that was already there. */
.brand {
  display: flex; align-items: center; gap: 10px;
  font-family: var(--font-display); font-weight: 600; letter-spacing: .14em;
}
.brand .mark { width: 26px; height: 26px; }
/* The strapline under the wordmark is body copy, not display: back to the
   text face explicitly, since it now inherits a serif from .brand. */
.brand small {
  color: var(--muted); font-family: var(--font); font-weight: 400;
  letter-spacing: .04em; font-size: 11px;
}
/* The project, beside the brand: where you are, in the same breath as
   whose console this is. Plain case, no tracking -- it is a name, not a
   label. */
.topbar-context {
  display: inline-flex; align-items: baseline; gap: 10px; min-width: 0;
  font-size: 13px; color: var(--text-2);
}
.topbar-context #hdr-project {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 28ch;
}
.topbar-sep { color: var(--muted); opacity: .6; }
/* The two right-hand groups: state with its one control, then account. A
   hairline between them says they are different kinds of thing. */
.topbar-state, .topbar-account { display: inline-flex; align-items: center; gap: 8px; }
.topbar-account {
  padding-left: 14px; border-left: 1px solid var(--line);
}
/* The operator's console names no account; an empty chip is not drawn. */
.status-chip:empty { display: none; }
/* The header mark's SVG strokes two gradients (markGoldB, markCreamB) baked
   for the dark --bg -- the same "gold is close in lightness to a light
   background" problem already fixed on the dial rim and the radar sweep,
   here in a third spot CSS variables can't reach because the gradient
   colors are hardcoded hex stops, not currentColor. markCreamB in
   particular (#C8BAA6 -> #8A7E6A) is close enough to light mode's cream
   --bg (#f7f3eb) to read as barely-there -- confirmed by rendering the
   header locally, cropped to the mark, before touching this (2026-09-02).
   A CSS rule on stroke beats the SVG's own url(#gradient) presentation
   attribute (same precedence the dial-rim fix already relies on), so this
   needs no markup change: every stroked shape in the mark switches to
   --muted, the same real-contrast token the dial rim uses. */
/* Every scrolling panel wears the same thin gold scrollbar the gate page
   established. Without this, Windows drew its default chunky white bars
   inside the HUD stat columns and every overflowing list -- the single
   brightest thing on an otherwise warm-black console (seen live in the
   2026-08-31 guest audit). The universal selector is the lowest possible
   specificity, so the two deliberate hide-the-bar rules (nav, settings
   tabs) still win. */
* { scrollbar-width: thin; scrollbar-color: rgba(211,168,76,.28) transparent; }
*::-webkit-scrollbar { width: 10px; height: 10px; }
*::-webkit-scrollbar-thumb { background: rgba(211,168,76,.28); border-radius: 10px; }
*::-webkit-scrollbar-track { background: transparent; }

nav { display: flex; gap: 2px; overflow-x: auto; scrollbar-width: none; }
nav::-webkit-scrollbar { display: none; }
nav button {
  background: none; border: 0; color: var(--muted);
  padding: 7px 14px; border-radius: 8px; cursor: pointer;
  font: inherit; font-size: 13px; white-space: nowrap;
  letter-spacing: .06em; text-transform: uppercase;
}
nav button:hover { color: var(--text-2); }
nav button[aria-selected="true"] {
  color: var(--bg); background: var(--gold); font-weight: 600;
}

.spacer { flex: 1; }

.status-chip {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 12px; color: var(--text-2);
  padding: 5px 11px; border: 1px solid var(--line);
  border-radius: 999px; background: var(--surface);
}
.dot { width: 7px; height: 7px; border-radius: 50%; background: var(--neutral-fg); }
.dot.live { background: var(--ok-fg); box-shadow: 0 0 8px var(--ok-fg); }
.dot.warn { background: var(--warn-fg); box-shadow: 0 0 8px var(--warn-fg); }
.dot.down { background: var(--danger-fg); }
/* Never colour alone. The chip's own text already names the state; the dot
   also changes SHAPE per state, so the four are distinguishable in greyscale
   and to anyone who cannot separate the hues. */
.dot.warn { border-radius: 1px; transform: rotate(45deg); }
.dot.down { border-radius: 1px; }

main { padding: 20px; max-width: 1400px; margin: 0 auto; }

/* ---------- panels ---------- */
.grid { display: grid; gap: 14px; }
.cols-4 { grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); }
.cols-2 { grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); }

.panel {
  /* Two layers, both optional: the gold wash on top, the surface fall
     under it, and a flat --surface last so a browser that drops gradients
     still paints a panel rather than a hole. */
  background:
    var(--wash-panel),
    linear-gradient(160deg, var(--surface) 0%, var(--surface-2) 100%),
    var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 16px;
  position: relative;
  overflow: hidden;
}
/* Corner ticks — the HUD detail that makes a rectangle read as an
   instrument. Defined ONCE, further down (search ".panel::before" for the
   edge-flush 1.5px gold version). An earlier 7px-inset variant lived here
   and was silently part-overridden by the later rule — same specificity,
   later in the sheet — leaving a half-merged cascade where the position and
   colour came from one rule and stray border-widths from the other. One
   definition, one place. */

h2 {
  margin: 0 0 12px; font-size: 11px; font-weight: 600;
  letter-spacing: .16em; text-transform: uppercase; color: var(--gold);
}
/* The header sits on its own faint band rather than floating on the panel.
   Padding, not margin, and the negative insets pull the band out to the
   panel edge -- the header occupies exactly the height it did before, which
   matters because every pixel here is measured against the composer fold. */
.panel > h2 {
  /* 5 + 6 + the 1px rule is the 12px bottom margin this replaced, to the
     pixel. The composer's distance from the fold is a measured, pinned number
     (test_console_responsive) and a header treatment is not allowed to spend
     any of it. */
  padding: 0 16px 5px; margin: 0 -16px 6px;
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--gold) 8%, transparent), transparent 58%);
  border-bottom: 1px solid color-mix(in srgb, var(--gold) 12%, transparent);
}
.stat { font-size: 30px; font-weight: 300; font-family: var(--mono); letter-spacing: -.01em; }
.sub { color: var(--muted); font-size: 12px; }
.sub a { color: var(--gold); text-decoration: none; border-bottom: 1px dotted var(--line); }
.sub a:hover { border-bottom-color: var(--gold); }

/* The escalation ladder: local -> cheap -> premium -> premium-high, with the
   rung that answered last lit up. Built from .pill rather than a new visual
   language — muted until it is the active rung, gold when it is. */
.ladder { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-top: 6px; }
.ladder i { font-style: normal; color: var(--muted); font-size: 12px; }

/* The composer sits docked to the HUD above it — a small, deliberate gap,
   not the wide margin every other panel gets — because it reads as part of
   the same command-center module, not a separate section further down the
   page. */
.composer-panel { margin-top: 4px; }
/* The command bar's visible label. Sentence case at the module-title size,
   not the engraved uppercase tag every other panel heading wears: the spec's
   section 2 asks for sentence case throughout and no decorative
   letter-spacing, and this is the one heading on Home that names an action
   rather than filing a region. */
.composer-panel > h2 {
  font-size: 16px; line-height: 24px; letter-spacing: 0;
  text-transform: none; color: var(--text);
}

/* Then a real break before the dashboard: this is the second layer, read
   after the command center rather than as part of the same surface. A
   labelled rule rather than bare whitespace, so the separation reads as
   intentional structure instead of an accidental gap. */
.dash-divider {
  height: 1px; margin: 64px 0 32px; position: relative;
  background: linear-gradient(to right,
    transparent, var(--line) 12%, var(--line) 88%, transparent);
}
.dash-divider::after {
  content: "Dashboard"; position: absolute; top: -7px; left: 50%;
  transform: translateX(-50%); background: var(--bg); padding: 0 14px;
  font-size: 10px; letter-spacing: .16em; text-transform: uppercase;
  color: var(--muted); white-space: nowrap;
}

/* ---------- data ---------- */
table { width: 100%; border-collapse: collapse; font-size: 13px; }
th {
  text-align: left; padding: 6px 8px; color: var(--muted);
  font-size: 10px; letter-spacing: .12em; text-transform: uppercase;
  border-bottom: 1px solid var(--line); font-weight: 600;
}
td { padding: 8px; border-bottom: 1px solid rgba(58,48,36,.5); vertical-align: top; }
tr:last-child td { border-bottom: 0; }
.scroll { overflow-x: auto; }

/* Every status pill reads from the semantic layer. The variant classes are
   named for the brand colour they used to hard-code (sage/danger/muted) so no
   markup or client code had to move; what changed is that they now resolve
   through the --ok-, --danger- and --neutral- families like every other
   state signal.

   Spelled out in words rather than as a glob. Joining those token names with
   a star and a slash puts the two characters that END a block comment in the
   middle of a sentence, which is how this comment used to close four words
   early: the orphaned prose then swallowed the .pill rule below it, and
   pills rendered with no padding, radius, background or border. */
.pill {
  display: inline-block; padding: 2px 9px; border-radius: 999px;
  font-size: 11px; background: var(--info-bg);
  border: 1px solid var(--info-line); color: var(--info-fg);
}
.pill.sage { background: var(--ok-bg); border-color: var(--ok-line); color: var(--ok-fg); }
.pill.danger { background: var(--danger-bg); border-color: var(--danger-line); color: var(--danger-fg); }
.pill.warn { background: var(--warn-bg); border-color: var(--warn-line); color: var(--warn-fg); }
/* Neutral: "we do not actually know yet" — not_configured and untested both
   read this way rather than borrowing gold, which this page already uses for
   attention/consequence (see .pill.gold on an approval's consequence tag). A
   connector nobody has pressed Test on yet is not a warning, it is a blank. */
.pill.muted { background: var(--neutral-bg); border-color: var(--neutral-line); color: var(--neutral-fg); }

/* Spend-by-day: a plain CSS bar list rather than a charting dependency, same
   "no new dependency for a small visual" reasoning as everywhere else on this
   page. Each bar's height is relative to the busiest day in the 14, so a
   quiet stretch still reads as quiet rather than being crushed to a sliver by
   whatever the single most expensive day happened to be. */
.spend-bars {
  display: flex; align-items: flex-end; gap: 4px; height: 84px;
  padding: 4px 0;
}
.spend-bar {
  flex: 1 1 0; display: flex; flex-direction: column; align-items: center;
  justify-content: flex-end; height: 100%; min-width: 0;
}
.spend-bar > i {
  display: block; width: 100%; max-width: 22px; border-radius: 3px 3px 0 0;
  background: linear-gradient(180deg, var(--gold), var(--gold-mid)), var(--gold-mid);
  min-height: 2px;
}
.spend-bar.zero > i { background: color-mix(in srgb, var(--neutral-fg) 25%, transparent); }
.spend-bar > b {
  font-size: 9px; color: var(--muted); margin-top: 4px; white-space: nowrap;
}

/* Connected services. One card per outside system: what it is for, whether it
   is linked, and where the key comes from. */
.conn {
  border: 1px solid var(--line); border-left: 2px solid var(--line);
  border-radius: 10px; padding: 14px 16px; margin-bottom: 12px;
  background: var(--wash-raised), color-mix(in srgb, var(--surface-shadow) 30%, transparent);
}
.conn.linked { border-left-color: var(--ok-fg); }
.conn-head { display: flex; align-items: baseline; gap: 12px; }
.conn-name { font-size: 15px; font-weight: 600; letter-spacing: .02em; }
.conn-state {
  margin-left: auto; font-family: var(--mono); font-size: 11px; color: var(--muted);
}
/* drawConnectors already writes "● linked" / "○ not linked" into this span,
   so the state is a word and a glyph before it is ever a colour. */
.conn.linked .conn-state { color: var(--ok-fg); }
.conn-why { font-size: 13px; color: var(--text-2); margin-top: 3px; }
.conn-dept {
  font-family: var(--mono); font-size: 10px; color: var(--muted);
  text-transform: uppercase; letter-spacing: .1em; margin-left: 6px;
}
.conn-docs {
  font-size: 11px; color: var(--muted); margin-top: 8px;
  font-family: var(--mono);
}
.conn-field { display: block; margin-top: 12px; }
.conn-label {
  display: flex; align-items: center; gap: 8px; font-size: 12px;
  color: var(--text-2); margin-bottom: 5px;
}
/* "set" rather than a row of dots: dots look editable and invite you to
   overwrite a key you cannot read back. */
.conn-set {
  font-family: var(--mono); font-size: 9px; font-weight: 400; color: var(--bg);
  background: var(--sage); border-radius: 3px; padding: 1px 5px;
  letter-spacing: .08em; text-transform: uppercase;
}
.conn-label i { font-size: 11px; font-style: normal; }
.conn-help { display: block; font-size: 11px; color: var(--muted); margin-top: 5px; }
.conn-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; margin-top: 14px; }
.conn-note { font-size: 12px; color: var(--sage); }
/* The health chip sits beside the name, not shoved to the state column —
   .conn-state already floats right with margin-left:auto, and the chip reads
   as part of the name group rather than colliding with it on a narrow card. */
.conn-head { flex-wrap: wrap; row-gap: 6px; }
.conn-health { white-space: nowrap; }

/* The engine pick-list. Replaces a <select>: three models is not enough to hide
   behind a control you have to open, and the reliability figure is the whole
   basis for choosing, so it belongs on screen while you choose. */
.picklist { display: flex; flex-direction: column; gap: 4px; margin: 4px 0; width: 100%; }
/* One compact row per model, on a single line.
   This column is a third of the panel with the reactor taking the middle, so
   anything wordier wrapped into a vertical column of single syllables. The tag
   and the measured score are what you choose on; the sentence explaining the
   score is in the title attribute. */
.picklist { display: flex; flex-direction: column; gap: 4px; }
.pickrow {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  width: 100%; text-align: left; cursor: pointer; white-space: nowrap;
  background: color-mix(in srgb, var(--ink) 55%, transparent);
  border: 1px solid var(--line); border-left: 2px solid transparent;
  border-radius: 6px; padding: 6px 10px; color: var(--text-2);
  font: inherit; font-family: var(--mono); font-size: 12px;
  transition: border-color .15s, background .15s, color .15s;
}
.pickrow:hover { border-color: var(--gold-mid); background: color-mix(in srgb, var(--gold) 7%, var(--ink)); }
.pickrow.on {
  border-left-color: var(--gold); background: color-mix(in srgb, var(--gold) 12%, var(--ink));
  color: var(--gold);
  box-shadow: inset 0 0 12px color-mix(in srgb, var(--gold) 12%, transparent);
}
/* Two lines now: the role people choose by, and the tag they type to pull it.
   Wrapping is allowed on this row only, because a role is a word rather than a
   filename and truncating "Balanced" to "Bala…" would defeat the point. */
.pickrow { flex-wrap: wrap; }
.pickrole {
  flex: 1 1 auto; letter-spacing: .02em; font-weight: 600; color: var(--text);
  overflow: hidden; text-overflow: ellipsis;
}
.picktag {
  flex: 1 0 100%; font-family: var(--mono); font-size: 10.5px; opacity: .7;
  letter-spacing: .02em; overflow: hidden; text-overflow: ellipsis;
}
.pickrow.on .pickrole { color: var(--gold); }
.pickscore {
  font-size: 10px; color: var(--muted); flex: 0 0 auto;
  padding: 1px 5px; border-radius: 4px;
  border: 1px solid color-mix(in srgb, var(--gold) 22%, transparent);
}
.pickrow.on .pickscore { color: var(--gold); border-color: var(--gold-mid); }

/* The specialists. On screen so the routing is visible, and the one that just
   answered lights up rather than being described in text somewhere else. */
.agents {
  display: flex; flex-wrap: wrap; gap: 12px; justify-content: center;
  margin: 22px 0 4px; padding-top: 18px; border-top: 1px solid var(--line);
}
.agent {
  display: flex; flex-direction: column; align-items: center; gap: 7px;
  cursor: default; width: 88px; padding: 12px 4px 9px;
  font-size: 10px; letter-spacing: .06em; text-align: center;
  color: var(--text-2); border-radius: 12px; position: relative;
  background: var(--surface-2); border: 1px solid var(--line);
  transition: color .15s, border-color .15s, transform .15s;
}
.agent:hover { transform: translateY(-1px); border-color: var(--gold-mid); color: var(--text); }

/* The portrait. Round and clipped so a generated emblem with an imperfect
   edge still reads as a crest rather than a stray square. */
.agent-face {
  position: relative; width: 46px; height: 46px; border-radius: 50%;
  display: grid; place-items: center; overflow: hidden;
  border: 1px solid var(--line); background: var(--surface);
}
.agent-face img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Sits under the image and shows through only if the image is missing or
   removed by onerror, so a fresh install has icons rather than holes. */
.agent-ico {
  position: absolute; font-family: var(--mono); font-size: 17px;
  color: var(--dept-color, var(--gold)); opacity: .85; z-index: -1;
}
.agent-name { line-height: 1.25; }

.agent.live { color: var(--gold); border-color: var(--gold); }
.agent.live .agent-face { border-color: var(--gold); }
/* A specialist that cannot actually work says so, rather than looking ready.
   A badge in the corner rather than a bare glyph under the name: a lone red
   "!" sitting in the text flow read as a typo more than a status the first
   time this was seen next to a real card row. */
.agent-warn {
  position: absolute; top: 7px; right: 7px; z-index: 2;
  width: 15px; height: 15px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--mono); font-weight: 700; font-size: 10px; line-height: 1;
  color: var(--bg); background: var(--danger); cursor: help;
}
.agent.live .agent-warn { color: var(--bg); }
.agent.off { opacity: .35; }

/* Which specialist answered. Quiet by default — it is provenance, not the
   point — but the unsure state is loud, because that is the one you act on. */
.dept {
  display: inline-block; margin-top: 8px; font-family: var(--mono);
  font-size: 10px; letter-spacing: .12em; text-transform: uppercase;
  color: var(--gold); border: 1px solid color-mix(in srgb, var(--gold) 30%, transparent);
  border-radius: 999px; padding: 2px 9px;
}
.dept.unsure {
  color: var(--danger); border-color: color-mix(in srgb, var(--danger) 45%, transparent);
  text-transform: none; letter-spacing: .04em; cursor: help;
}

/* ---------- command deck ----------
   Plain buttons, one press each -- these are the shortcuts you actually
   touch, so they stay simple rather than dressed up as "physical keys". */
.deck {
  display: flex; flex-wrap: wrap; gap: 10px; justify-content: center;
  margin-top: 18px; padding-top: 16px; border-top: 1px solid var(--line);
}
.key {
  position: relative;
  display: flex; flex-direction: column; align-items: center; gap: 5px;
  min-width: 86px; padding: 11px 14px 9px; cursor: pointer;
  font: inherit; font-size: 10px; letter-spacing: .14em; text-transform: uppercase;
  color: var(--text-2); border-radius: 10px;
  border: 1px solid var(--line); background: var(--surface-2);
  transition: color .15s, border-color .15s, transform .1s;
}
.key b {
  font-family: var(--mono); font-size: 19px; font-weight: 400; line-height: 1;
  color: var(--gold);
}
.key:hover { color: var(--text); border-color: var(--gold-mid); }
.key:active { transform: translateY(1px); }
.key:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }

/* The stop key. Red before it is pressed, and marked while the engine is held. */
.key.danger b { color: var(--danger); }
.key.danger:hover { border-color: var(--danger); }
.key.danger.on { border-color: var(--danger); color: var(--danger); }

/* Voice controls. The listening state has to be obvious from across the room:
   an open microphone you have forgotten about is the failure mode here. */
.btn.mic { font-size: 12px; padding: 8px 12px; letter-spacing: .04em; }
/* .btn.ghost.tiny[aria-pressed="true"] (four selectors' worth of specificity)
   sets gold TEXT for every other toggle in this family, and used to beat
   this rule's plain .btn.mic.active (three) -- gold text on the gold
   background this rule ALSO sets, i.e. invisible. The wake-toggle button
   carries both class sets at once (btn ghost tiny mic + active), so this
   has to out-specify the other rule explicitly rather than rely on source
   order, which is what actually broke here. */
.btn.ghost.tiny.mic.active {
  color: var(--bg); background: var(--gold); border-color: var(--gold);
  animation: micpulse 1.4s ease-in-out infinite;
}
@keyframes micpulse {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--gold) 55%, transparent); }
  50%      { box-shadow: 0 0 0 7px transparent; }
}
.btn.mic:disabled { opacity: .4; cursor: not-allowed; }
@media (prefers-reduced-motion: reduce) { .btn.mic.active { animation: none; } }

/* ---------- controls ---------- */
input, textarea, select, button.btn {
  font: inherit; font-size: 14px; color: var(--text);
  background: var(--bg); border: 1px solid var(--line);
  border-radius: 8px; padding: 10px 12px; width: 100%;
}

/* An open <select> is drawn by the OS, not by this stylesheet, and Windows
   defaults it to a white list with near-white text — unreadable until a row
   happened to be hovered. `color-scheme` is what actually tells the platform to
   render its own widgets dark; the explicit option colours cover the browsers
   that ignore it. Both are needed: neither alone fixed it.

   SCOPED, because both halves were written when dark was the only theme and
   neither followed it into light mode:

   * `color-scheme: dark` was unconditional, so it overrode the light theme's
     own `color-scheme: light` and the OS drew a dark dropdown on a cream page
     — the single most visible thing wrong with light mode.
   * The option colours were a hardcoded #000 against `--gold`. The light theme
     deliberately takes gold DOWN to a bronze so it can sit on paper (see the
     "gold on paper" block above), and that bronze on black measures 3.24–3.57:1
     across the four plans — below the 4.5 floor on every one of them, in the
     one place the theme's own inversion met a background it did not own. The
     dark theme's ramp measures 10.4–13.7:1 on the same rule, which is why this
     was never noticed.

   Both now follow the mode, and the surface is a token rather than a constant
   so a future palette change cannot re-open the same gap. */
select { color-scheme: dark; }
select option {
  background: var(--surface); color: var(--text);
}
/* The highlighted row inverts so the selection is unmistakable rather than a
   slightly different shade of the surface. */
select option:checked,
select option:hover { background: var(--gold); color: var(--on-accent); }

input:focus, textarea:focus, select:focus {
  outline: none; border-color: var(--gold-mid);
  box-shadow: 0 0 0 3px var(--gold-dim);
}
button.btn {
  /* The wash is a light fall over the flat gold, declared in the same
     shorthand so the solid is the fallback rather than a second rule. */
  cursor: pointer; background: var(--wash-primary), var(--gold); color: var(--on-accent);
  border-color: transparent; font-weight: 650; letter-spacing: .02em;
}
button.btn:hover { filter: brightness(1.08); }
button.btn.ghost { background: transparent; color: var(--text-2); border-color: var(--line); font-weight: 500; }
button.btn.ghost:hover { border-color: var(--gold-mid); color: var(--gold); }
button.btn:disabled { opacity: .45; cursor: not-allowed; filter: none; }
/* A link that does a button's job (Export CSV on Contacts and Finance). Only
   `button.btn` was styled, so these two rendered as browser-default blue
   underlined links (about 2:1 on the dark panel) beside real buttons. */
a.btn {
  box-sizing: border-box; display: inline-block; font: inherit; font-size: 14px;
  padding: 10px 12px; border: 1px solid transparent; border-radius: 8px;
  text-decoration: none; cursor: pointer;
  background: var(--wash-primary), var(--gold); color: var(--on-accent);
  font-weight: 650; letter-spacing: .02em;
}
a.btn:hover { filter: brightness(1.08); }
a.btn.ghost { background: transparent; color: var(--text-2); border-color: var(--line); font-weight: 500; }
a.btn.ghost:hover { border-color: var(--gold-mid); color: var(--gold); }
/* Irreversible actions only (account deletion) -- everything else uses .ghost
   or the plain gold .btn, so danger stays rare enough to mean something. */
button.btn.danger {
  background: transparent; color: var(--danger); border-color: var(--danger);
}
button.btn.danger:hover { background: var(--danger-fg); color: var(--on-accent); filter: none; }
/* Normal case. Uppercase tracking is reserved for section headers (h2, th,
   .rail-group) -- a page where every field label also shouts is a page where
   nothing does. */
label { display: block; font-size: 12px; color: var(--muted); margin-bottom: 5px;
        letter-spacing: .02em; }
.row { display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-end; }
.row > * { flex: 1 1 150px; }
.row .narrow { flex: 0 0 auto; }

/* ---------- conversation ---------- */
/* Collapsed by default: applyThreadCollapse() hides all but the last
   exchange, so this needs no height cap of its own -- there is nothing to
   scroll. The cap comes back when it expands, or a long history would push
   the composer off the page again, which is the thing the collapse exists to
   stop. */
.thread { display: flex; flex-direction: column; gap: 12px; padding-right: 6px; }
.thread.expanded { max-height: 52vh; overflow-y: auto; }
/* Quiet until wanted: this sits between the conversation and the composer,
   and it is a way back to history, not a call to action. */
.thread-toggle {
  align-self: flex-start;
  margin-top: 10px;
  background: none; border: 1px solid var(--line); border-radius: 999px;
  padding: 5px 14px; font-size: 12px; color: var(--muted); cursor: pointer;
  transition: border-color .15s, color .15s;
}
.thread-toggle:hover { border-color: var(--gold-mid); color: var(--text); }
.msg { display: flex; gap: 11px; align-items: flex-start; }
.msg .who {
  flex: 0 0 auto; width: 30px; height: 30px; border-radius: 8px;
  display: grid; place-items: center; font-size: 10px; font-weight: 700;
  letter-spacing: .04em;
  background: var(--raised); color: var(--gold); border: 1px solid var(--line);
}
/* The assistant's avatar is the Anvella mark, not the letter "A". The mark is
   an arc reactor that is also an A, so it still reads as the initial while
   actually being the product's own logo -- currentColor above is what lets it
   pick up --gold rather than shipping its own fill. */
.msg .who .who-mark { width: 21px; height: 21px; display: block; }
/* The operator asked for their own name in the conversation to READ as gold
   (2026-09-09). It previously did the opposite: near-black --on-accent
   lettering sitting ON a gold-sheen fill, so the chip was gold but the name
   itself was not, and at 10px it read as a dark smudge rather than a label.
   Inverted to gold lettering on the same raised surface the assistant's own
   mark already uses two rules above -- that exact --gold-on---raised pairing
   is already proven in both themes here, so this needs no new colour and no
   new contrast argument. The gold border keeps the two speakers distinct now
   that they share a surface, and the larger, heavier type is the "more
   visible" half of the request. */
.msg.you .who {
  background: var(--raised);
  color: var(--gold);
  border-color: var(--gold-mid);
  font-size: 11px;
  font-weight: 800;
  text-shadow: none;
}
.msg .body { flex: 1; white-space: pre-wrap; word-break: break-word; }
.msg .tools { font-family: var(--mono); font-size: 11px; color: var(--muted); margin-top: 5px; }
.finance-prompt {
  margin-top: 8px; padding: 8px 10px; border: 1px solid var(--line);
  border-radius: 8px; background: var(--raised); white-space: normal;
}

/* ---------- todos ---------- */
.todo { display: flex; align-items: flex-start; gap: 11px; padding: 9px 4px;
        border-bottom: 1px solid rgba(58,48,36,.5); }
.todo:last-child { border-bottom: 0; }
.check {
  flex: 0 0 auto; width: 19px; height: 19px; margin-top: 2px;
  border: 1.5px solid var(--gold-mid); border-radius: 5px;
  background: transparent; cursor: pointer; display: grid; place-items: center;
  color: transparent; font-size: 12px; line-height: 1; transition: all .12s;
}
.check:hover { border-color: var(--gold); color: var(--gold-mid); }
.todo.done .check { background: var(--ok-fg); border-color: var(--ok-fg); color: var(--on-accent); }
.todo.done .text { color: var(--muted); text-decoration: line-through; }
.todo .when { font-size: 11px; color: var(--muted); font-family: var(--mono); }
.todo.overdue .when { color: var(--danger); }

/* ---------- breadcrumb (WEBBED_IA_AND_KNOWLEDGE_SYSTEM.md 2.1) ---------- */
.breadcrumb { font-size: 13px; color: var(--muted); margin-bottom: 10px; }
.breadcrumb:empty { display: none; }
.crumb-link {
  background: none; border: none; padding: 0; margin: 0; cursor: pointer;
  font: inherit; color: var(--gold); text-decoration: underline;
}
.crumb-link:hover, .crumb-link:focus-visible { text-decoration: none; }
.crumb-current { color: inherit; }

/* ---------- misc ---------- */
.muted { color: var(--muted); }
.hidden { display: none !important; }
/* Banners are this console's toast: a save result, a key that would not
   validate, a stale engine. Four semantic variants, one shape. Each carries a
   glyph as well as a colour, so the state survives greyscale and colour
   blindness -- the ::before is decorative to a screen reader because the
   banner's own text always names the outcome. */
.banner { padding: 11px 14px; border-radius: 8px; margin-bottom: 14px; font-size: 13px;
          border: 1px solid var(--info-line);
          background: var(--wash-header), var(--info-bg); }
.banner::before { font-family: var(--mono); margin-right: 8px; opacity: .85; }
.banner.err { border-color: var(--danger-line); background: var(--danger-bg); color: var(--danger-fg); }
.banner.err::before { content: "!"; }
.banner.ok { border-color: var(--ok-line); background: var(--ok-bg); color: var(--ok-fg); }
.banner.ok::before { content: "\2713"; }
.banner.warn { border-color: var(--warn-line); background: var(--warn-bg); color: var(--warn-fg); }
.banner.warn::before { content: "\26A0"; }
.login { max-width: 400px; margin: 14vh auto; }
.pin-input {
  font-size: 28px; letter-spacing: .5em; text-align: center; font-variant-numeric: tabular-nums;
}

/* ── The PIN field ────────────────────────────────────────────────────────
   Four lit cells over ONE real input. The input keeps its full size and
   sits transparently on top so every native behaviour (caret, paste,
   autofill, mobile numeric keypad, select-all) still works and the click
   target is the whole row; the cells underneath are pure presentation
   driven by a class the JS sets from the input's length. Nothing here
   depends on four separate fields, which is what makes it robust. */
.pin-field { position: relative; height: 62px; margin: 4px 0 14px; }
.pin-field .pin-real {
  position: absolute; inset: 0; width: 100%; height: 100%;
  opacity: 0; z-index: 2; cursor: pointer;
  /* Transparent, not display:none -- a hidden input cannot hold focus, and
     without focus there is no typing, no caret and no mobile keyboard. */
  border: 0; padding: 0; margin: 0; background: none; color: transparent;
}
.pin-cells {
  position: absolute; inset: 0; display: grid; gap: 10px;
  grid-template-columns: repeat(4, 1fr); pointer-events: none;
}
.pin-cells span {
  border-radius: 10px; position: relative;
  border: 1px solid color-mix(in srgb, var(--gold) 26%, transparent);
  background: linear-gradient(180deg,
              color-mix(in srgb, var(--gold) 7%, transparent),
              color-mix(in srgb, var(--gold) 2%, transparent));
  transition: border-color .16s ease-out, box-shadow .16s ease-out,
              background .16s ease-out;
}
/* A filled cell shows a dot rather than the digit: this is a password field,
   and echoing the number back defeats the point of masking it. */
.pin-cells span.filled::after {
  content: ""; position: absolute; inset: 0; margin: auto;
  width: 11px; height: 11px; border-radius: 50%; background: var(--gold);
  box-shadow: 0 0 10px color-mix(in srgb, var(--gold) 70%, transparent);
}
.pin-cells span.filled {
  border-color: color-mix(in srgb, var(--gold) 62%, transparent);
  background: color-mix(in srgb, var(--gold) 10%, transparent);
}
/* The cell the next digit will land in, so the caret's job is done by the
   cells -- the real caret is invisible on a transparent input. */
.pin-real:focus + .pin-cells span.active {
  border-color: var(--gold);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--gold) 45%, transparent),
              0 0 18px color-mix(in srgb, var(--gold) 30%, transparent);
}
.pin-real:focus + .pin-cells span.active::before {
  content: ""; position: absolute; inset: auto 0 10px; margin: auto;
  width: 16px; height: 2px; border-radius: 2px; background: var(--gold);
  animation: pin-caret 1.1s steps(1) infinite;
}
@keyframes pin-caret { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }

/* Verifying: the row dims and the cells pulse, so a slow round trip reads as
   "checking" rather than as a frozen form. */
.pin-field.checking .pin-cells { animation: pin-pulse .9s ease-in-out infinite; }
@keyframes pin-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .55; } }

/* Accepted. Brief, because the app is already loading behind it. */
.pin-field.ok .pin-cells span {
  border-color: var(--sage);
  box-shadow: 0 0 16px color-mix(in srgb, var(--sage) 45%, transparent);
}

/* Rejected. A shake is the right feedback for a wrong PIN: it is felt
   rather than read, which is what lets a quiet auto-retry say "not that one"
   without putting an error banner on screen. */
.pin-field.wrong .pin-cells { animation: pin-shake .42s ease-in-out; }
.pin-field.wrong .pin-cells span { border-color: color-mix(in srgb, var(--danger) 70%, transparent); }
@keyframes pin-shake {
  0%, 100% { transform: translateX(0); }
  15% { transform: translateX(-7px); }
  30% { transform: translateX(6px); }
  45% { transform: translateX(-4px); }
  60% { transform: translateX(3px); }
  80% { transform: translateX(-2px); }
}
@media (prefers-reduced-motion: reduce) {
  .pin-field.wrong .pin-cells,
  .pin-field.checking .pin-cells { animation: none; }
  .pin-real:focus + .pin-cells span.active::before { animation: none; }
}
.spin { display: inline-block; width: 11px; height: 11px; border: 2px solid var(--gold-mid);
        border-top-color: var(--gold); border-radius: 50%; animation: s .7s linear infinite; }
@keyframes s { to { transform: rotate(360deg); } }
.ring { animation: rot 26s linear infinite; transform-origin: 50% 50%; }
.ring.rev { animation-duration: 38s; animation-direction: reverse; }
@keyframes rot { to { transform: rotate(360deg); } }
section { margin-bottom: 16px; }

@media (max-width: 720px) {
  main { padding: 12px; }
  .topbar { padding: 8px 12px; gap: 10px; }
  .brand small { display: none; }
  .stat { font-size: 24px; }
}

/* --- settings ------------------------------------------------------------ */
/* A label wrapping its own input, so the caption and the control travel
   together when the row wraps — otherwise a narrow window pairs each label
   with the wrong field, which on a settings page is genuinely dangerous. */
.field { display: flex; flex-direction: column; gap: 6px; }
/* Normal case, like the bare-label rule above: uppercase tracking belongs to
   section headers only. */
.field > span { color: var(--text-2); font-size: 12px; letter-spacing: .02em; }
.field input[type="color"] { height: 38px; padding: 3px; cursor: pointer; }
code { font-family: var(--mono); font-size: 12px; background: var(--raised);
  padding: 1px 5px; border-radius: 4px; border: 1px solid var(--line); }

/* --- HUD ----------------------------------------------------------------- */
/* Everything below is drawn from the same CSS variables the settings page
   writes, so a custom accent recolours the chrome rather than clashing with it.
   Restraint is the point: a console you read numbers off all day cannot also be
   a light show, so the effects are static geometry plus one slow sweep. */

.panel { position: relative; }

/* Corner ticks. Two gradients per corner, drawn on a pseudo-element so no
   markup changes and no extra DOM nodes per panel. */
.panel::before, .panel::after {
  content: ""; position: absolute; width: 12px; height: 12px;
  border-color: var(--gold); opacity: .45; pointer-events: none;
}
.panel::before { top: -1px; left: -1px; border-top: 1.5px solid; border-left: 1.5px solid;
  border-top-left-radius: 3px; }
.panel::after { bottom: -1px; right: -1px; border-bottom: 1.5px solid; border-right: 1.5px solid;
  border-bottom-right-radius: 3px; }

/* A panel whose numbers belong to the project you are in, so "why did that
   change when I switched" is answerable by looking at it. */
.panel.scoped { border-left: 2px solid color-mix(in srgb, var(--gold) 55%, transparent); }
.panel.scoped h2::after {
  content: "· this project"; margin-left: 8px; font-size: 9px; letter-spacing: .1em;
  color: var(--muted);
}

.stat { text-shadow: 0 0 22px color-mix(in srgb, var(--gold) 22%, transparent); }

/* One slow sweep across the top edge of the header — enough to read as "live"
   without anything moving in your peripheral vision while you work. */
.topbar { position: relative; overflow: hidden; }
.topbar::after {
  content: ""; position: absolute; inset: 0 auto 0 0; width: 22%; pointer-events: none;
  background: linear-gradient(90deg, transparent,
    color-mix(in srgb, var(--gold) 8%, transparent), transparent);
  animation: sweep 9s linear infinite;
}
@keyframes sweep { from { transform: translateX(-120%); } to { transform: translateX(560%); } }

/* A live dot should look live. */
.dot.live { box-shadow: 0 0 0 0 color-mix(in srgb, var(--sage) 60%, transparent);
  animation: pulse 2.6s ease-out infinite; }
@keyframes pulse {
  to { box-shadow: 0 0 0 7px color-mix(in srgb, var(--sage) 0%, transparent); }
}

/* Honour the system setting. Motion is decoration here and decoration is the
   first thing that should go when someone has asked for less of it. */
@media (prefers-reduced-motion: reduce) {
  .topbar::after, .dot.live, .ring { animation: none; }
}

/* --- the talk button ------------------------------------------------------- */
/* The one ring still drawn (the spend progress ring) reports a real number;
   nothing here is decoration that looks like a gauge but reports nothing. */

/* The readings-and-work section, under the command bar. It was "the status
   panel above the composer" for most of this file's life, and it did hold
   the instrument -- an animated panel with scanlines, corner brackets and a
   glowing reactor ringed with avatars. The reactor left twice: first into
   the middle track of the work grid (2026-09-06), then out of this section
   entirely and above the command bar (2026-09-07, #eye-stage). What is left
   here is the metric band and the two work columns. */
.hud {
  display: grid; grid-template-columns: 1fr auto 1fr; gap: 20px;
  align-items: center; margin-bottom: 18px; padding: 16px 18px;
  border: 1px solid var(--line); border-radius: 12px;
  background: var(--surface);
  position: relative;
}

/* Sizing lives on the later .reactor rule below (the real, button-shaped
   one, with the viewport-scaling clamp() and its own explanation) -- this
   selector predates it and is kept only for the properties that rule does
   not redeclare. */
.reactor { position: relative; color: var(--gold); }
/* NO filter on the SVG as a whole. There used to be two drop-shadows here,
   as an "anti-Minecraft" contact glow, and they cost this instrument twice:
   a blur over all three thousand elements softens exactly the tick marks,
   graticule and hairline rings that make it read as an instrument rather
   than a marble, and it is re-rasterised on every animation frame over the
   whole 300x300 box. Measured 2026-09-06 in headless Chromium: the restored
   reactor ran at 14fps with it and 21fps without, and the main thread being
   that starved is visible elsewhere -- a 150ms opacity transition on an
   orbit node's label was still at 0.74 after 300ms. Glow belongs on the few
   elements that mean something by glowing (the activity arc, the spend
   ring, the home marker), not on every line. */
.reactor svg { width: 100%; height: 100%; display: block; }
/* .core is gone from the markup entirely -- it was a second, smaller sphere
   sitting inside the globe, which is exactly what "there should not be 2
   spheres in the center" was about. The state rules further down that used
   to tint and breathe it now target .globe-tint, an overlay at the globe's
   own radius: same feedback on hover/listening, no second object.

   Losing that disc also lost the status text's backdrop, so the text carries
   its own shadow now (.reactor-text below) rather than a shape behind it. */
.globe-tint { fill: transparent; transform-origin: 150px 150px; }

/* The sphere's surface, rendered by static/core.js on a WebGL canvas that
   sits BEHIND the SVG: z-index -1 inside .reactor's own stacking context
   (.reactor carries z-index 3, so this can never fall behind the page). It
   covers only the r=92 core -- 92/150 of the box, plus 2% so the shader's
   own anti-aliased edge lands under the SVG's rim stroke rather than
   inside it -- because every fragment outside the sphere is a fragment
   thrown away, and this runs on integrated graphics. Transparent outside
   the sphere, so it composes over whatever background the operator's
   theme has chosen. */
.core-gl {
  position: absolute; z-index: -1;
  left: 18.72%; top: 18.72%; width: 62.56%; height: 62.56%;
  display: block; pointer-events: none;
}
/* Once the shader is drawing the volume, the SVG's own shading circles
   (see .globe-shade in console.html) drop to a hint: the limb and the far
   hemisphere still add a little depth under the continent, but the cream
   hot spot would only bleach a surface that now has its own highlight.
   Never display:none -- opacity, so that losing the GL context and dropping
   .gl-live (core.js does this) brings the old sphere straight back. */
.reactor.gl-live .globe-shade { opacity: .58; }
.reactor.gl-live .globe-shade-lit { opacity: .10; }
/* Reduced motion is honoured INSIDE core.js (it draws one frame per state
   change and runs no loop), not by hiding the canvas: the still frame is a
   lit metal sphere, which is the whole point of keeping it. */

/* The ball gives way to the instrument.
   ====================================
   "A subtler, SMALLER globe... surrounded by an elaborate ring system" is
   what was asked for, and the ring system is drawn at .15-.4 opacity in
   hairlines while the sphere is a lit metal render: at equal geometry the
   ball wins every time, and the panel reads as a marble with some faint
   scratches around it. Rather than move the rings out (their radii are
   fixed by 36 hand-placed tick marks and two ellipses) or shrink the sphere
   (its clip path, its shader canvas and 2,800 dot positions are all tied to
   r=92), this changes what each one WEIGHS.

   Two knobs, both measured against a render rather than reasoned about:

   1. The shader canvas is no longer at full strength. It is the brightest
      object on the page by a wide margin and it needs to stop being that;
      at .82 it still reads as lit metal and no longer flares past the rim.
   2. The SVG's limb and far-hemisphere passes come back up (.34 -> .58,
      above). Those darken the sphere's OUTER half, which is the half that
      touches the rings -- so the ball recedes from its own edge inward and
      the rim ring, the ticks and the dashed orbit are what the eye meets
      first. The cream hot-spot pass stays down at .10: it lights the
      middle, which is where the words are. */
.core-gl { opacity: .82; }
/* The United States as city lights from orbit.
   ============================================

   Density does the work, not opacity alone: engine/tools/gen_usa_dots.py
   decides whether each dot EXISTS from a population-weighted falloff, so the
   Great Basin and the northern Plains are genuinely empty while the northeast
   corridor fills in solid. Positions are jittered off the sampling grid, which
   is what removes the halftone-screen look a regular lattice always produces.

   Four tiers plus a coastline pass, all sharing one warm amber ramp -- dim
   land is a dull ember, cities are near-white. No per-dot glow anywhere:
   ~2800 overlapping drop-shadows is a real performance cost and produces the
   smeared mush this element has failed with before. Glow is applied ONCE, to
   the group, where it costs one filter pass instead of thousands.

   Every tier dropped about a quarter on 2026-09-06, together with the coast
   and the metro cores below, for the operator's reason: "a subtler, SMALLER
   globe". City lights from orbit are points in the dark; these were reading
   as daylight, and at full strength the continent was the loudest thing on
   a panel whose subject is the ring system around it.

   NOT load-bearing for the words' legibility, and it is worth being exact
   about that because the first version of this comment claimed it was.
   Mutation-checked: putting every tier back to full brightness leaves
   test_reactor_readout's contrast measurement passing, because the scrim
   under the words (.reactor-text::before) is what clears WCAG on its own.
   These numbers are a design choice; the scrim is the fix. */
.us-dots { fill: var(--map-dim); }
.us-dim  { opacity: .28; }
.us-mid  { fill: var(--map-mid); opacity: .46; }
.us-warm { fill: var(--map-warm); opacity: .62; }
.us-hot  { fill: var(--map-hot); opacity: .74; }

/* The lit shoreline -- the single biggest reason the country is
   recognisable at this size. No filter at all: a glow on a sub-pixel dot is
   almost entirely glow, and 321 of them overlapping is what turned the coast
   into a fuzzy rope instead of a line of lights. */
.us-coast { fill: var(--map-coast); opacity: .8; }
/* Scatter around each metro. Dropped well back so it reads as the faint
   sprawl around a city rather than as a second, competing blob. */
.us-halo { fill: var(--map-halo); opacity: .3; }
/* The metro cores. No filter here either -- 37 glowing cores at 4px merged
   the northeast into one smear, and even 1.5px was enough to soften every
   one of them. A city is now a small, hard, near-white point, which is
   exactly what a city looks like from orbit. */
.us-cities { fill: var(--map-city); opacity: .82; }

/* Columbia, South Carolina -- the home office, and the one point on this map
   that is about the company rather than about the country. Deliberately the
   brightest thing on the globe and the only pulsing one, so the eye lands
   there without needing a label: a white core, a warm bloom, and a ring that
   expands and fades like a sonar return. */
.us-home .home-core {
  fill: var(--map-home);
  filter: drop-shadow(0 0 6px rgba(255, 248, 224, 1));
}
.us-home .home-glow {
  fill: var(--map-home-glow); opacity: .38;
  transform-origin: 180.5px 157.7px;
}
.us-home .home-ring {
  fill: none; stroke: var(--map-home-ring); stroke-width: 1.1; opacity: .6;
  transform-origin: 180.5px 157.7px;
}
@media (prefers-reduced-motion: no-preference) {
  .us-home .home-ring { animation: home-ping 3.4s ease-out infinite; }
  .us-home .home-glow { animation: home-breathe 3.4s ease-in-out infinite; }
}
@keyframes home-ping {
  0%   { transform: scale(.30); opacity: .85; }
  75%  { transform: scale(1.5); opacity: 0; }
  100% { transform: scale(1.5); opacity: 0; }
}
@keyframes home-breathe {
  0%, 100% { transform: scale(.85); opacity: .30; }
  50%      { transform: scale(1.1); opacity: .5; }
}
#load-ring {
  opacity: .95; transition: stroke-dashoffset .8s ease;
  filter: drop-shadow(0 0 4px color-mix(in srgb, var(--gold) 28%, transparent));
}
/* 150,150 -- the actual centre of every circle in the SVG (viewBox is
   0 0 300 300). This was 130,130, off by 20 units in both axes, which
   rotated every ring around a point below-and-left of true centre instead
   of spinning in place -- a visible wobble rather than a clean spin, and
   exactly the kind of thing that reads as "not quite right" without being
   obviously broken.

   .spin-slow went with the strip and did not come back: no element in the
   markup ever carried it, so it was a rule looking for an element. */
.spin-rev { transform-origin: 150px 150px; }
@media (prefers-reduced-motion: no-preference) {
  .spin-rev { animation: rot 30s linear infinite reverse; }
}
@keyframes rot { to { transform: rotate(360deg); } }

/* The "gold earth" plasma bands: three different speeds and directions
   around the true centre (150,150) -- not their own ellipse centres --
   so each one sweeps through the whole clipped circle rather than
   spinning in place, drifting past one another the way the old nebula's
   rotation always did (that part was never the problem; the blur and
   the near-identical overlapping shapes were). */
.wisp-a, .wisp-b, .wisp-c { transform-origin: 150px 150px; }
/* Declared inside no-preference rather than switched off again in a reduce
   block below, which is how every other turning part of this instrument is
   written -- one shape for all of them, so test_console_ui_overhaul can
   assert it of the whole reactor instead of listing the exceptions. */
@media (prefers-reduced-motion: no-preference) {
  .wisp-a { animation: rot 54s linear infinite; }
  .wisp-b { animation: rot 68s linear infinite reverse; }
  .wisp-c { animation: rot 39s linear infinite; }
}

/* Radar sweep. A cone of light going round behind the rings, which is what
   makes the panel read as *live* rather than as a drawing of a panel. It is
   masked to a ring so it never washes over the text in the middle. */
.reactor::after {
  content: ""; position: absolute; inset: 0; border-radius: 50%;
  pointer-events: none; z-index: 0;
  background: conic-gradient(
    from 0deg,
    transparent 0deg 300deg,
    color-mix(in srgb, currentColor 22%, transparent) 348deg,
    color-mix(in srgb, currentColor 42%, transparent) 360deg);
  mask-image: radial-gradient(circle, transparent 34%, var(--ink) 42%, var(--ink) 74%, transparent 80%);
  opacity: .85;
}
/* Declared here rather than on the rule above, like every other turning part
   of this instrument. The universal reduced-motion net at the foot of this
   file would have stopped the sweep anyway, but stopping it is not enough:
   a cone that is not turning is a flat gold wedge lying across the panel,
   which is the exact failure this rule's own comment already records from
   light mode. So under reduced motion there is no animation AND no cone.
   .reactor.paused / .reactor.down below only retune a sweep that runs. */
@media (prefers-reduced-motion: no-preference) {
  .reactor::after { animation: radar-sweep 6.5s linear infinite; }
}
@media (prefers-reduced-motion: reduce) {
  .reactor::after { opacity: 0; }
}
/* Same cone, but at 42% gold-mix it reads as a soft light beam only against
   the near-black dark --bg -- against light mode's cream --bg the identical
   color-mix is a visibly solid gold wedge, not a glow (confirmed by
   rendering both modes locally with Playwright, 2026-09-02: it froze
   mid-sweep as a flat pie-slice sitting on top of the panel, the same
   "gold and a light background are close in lightness so opacity alone
   can't fix it" problem the dial-rim fix above already solved for the
   ring strokes). Halving the mix and dropping the element opacity keeps
   the sweep readable as motion without it reading as a shape. */
/* Its own name, not `sweep`: the skeleton sheen at the top of this
   file already owns that one, and two @keyframes of the same name
   means the last wins for both. */
@keyframes radar-sweep { to { transform: rotate(360deg); } }

/* The one crisp status arc (see the SVG comment on .flow-arc for why this
   replaced the CSS conic-gradient ring: a stroked vector arc has no soft
   mask edge to blur at any render size, where the gradient-plus-mask
   version did once the reactor grew past a few hundred pixels). Glow comes
   from drop-shadow, which blurs the light AROUND a still-sharp stroke,
   not the stroke's own edge -- the shape stays a shape. */
.flow-arc {
  filter: drop-shadow(0 0 7px color-mix(in srgb, var(--gold) 55%, transparent));
  transition: filter .2s ease-out, opacity .2s ease-out;
}
/* The dimmer, smaller second arc gets its own lighter glow and a different
   speed/direction so it reads as an independent layer, not a shadow of the
   primary one -- two things turning at different rates is what makes this
   look engineered rather than merely decorated. The third, thinnest arc is
   dimmer again, for a third independent rate. */
/* No glow on the second and third arcs. They had one (4px and 3px) and it
   bought nothing: a halo that faint is invisible next to the primary arc's
   own, while each of these is a full circle, so its filter costs a
   whole-box off-screen pass on every animation frame. Fine etched lines at
   different rates is what makes this read as engineered; a torch on each of
   them is not. Weight now comes from stroke opacity alone. */
.flow-arc-2, .flow-arc-3 { filter: none; }
/* The trailing echoes carry .flow-arc for its animation only -- three of
   them stacked with the primary's own 7px glow would stack into a single
   blurred smear exactly like the mistakes documented above, so this drops
   the filter back to none and lets fading opacity and shrinking dash length
   alone do the "thinning tail" work. Same-specificity override, so this has
   to stay after .flow-arc in source order to win. */
.flow-echo { filter: none; }
@media (prefers-reduced-motion: no-preference) {
  /* Unconditional, not gated to .reactor.live: these used to only turn once
     the engine was actually up, which meant the ring was completely static
     for as long as a fresh install shows OFFLINE -- exactly the state a
     "make the ring look cool" screenshot was taken in, and a still ring is
     not what "insanely cool... radioactive" is asking for. The engine's
     actual up/down state is already communicated in words ("ONLINE" /
     "OFFLINE") and by .reactor.down dimming these below -- the ring's job
     is to look like a real instrument in every state, not just a connected
     one. Large and slow on purpose: this is the biggest single moving
     element on the page, and 46s means it is always visibly turning
     without racing the faster 6.5s radar sweep on ::after underneath it. */
  .flow-arc { animation: rot 46s linear infinite; }
  .flow-arc-2 { animation: rot 33s linear infinite reverse; }
  .flow-arc-3 { animation: rot 61s linear infinite; }
}
/* Genuinely down (a real connection failure, not merely not-yet-connected)
   still reads as quieter than live -- the same principle .reactor.down::after
   already applies to the radar sweep -- without going fully static, which
   is what made the ring look flat in the first place. */
.reactor.down .flow-arc, .reactor.down .flow-arc-2, .reactor.down .flow-arc-3 {
  opacity: .4;
}

/* The holographic ring band -- a different shape (tilted ellipse) from
   every other ring on this panel (all plain circles), which is the actual
   ask behind "a different design," not just another concentric line.
   transform-origin stays the true centre (150,150) even though these sit
   inside an already-rotated parent <g>, because that parent's own pivot is
   the same point -- rotating a point around itself leaves it unmoved, so
   the two transforms compose cleanly regardless of order. */
.holo-ring {
  /* The stroke comes from the SVG's own gradient defs (light gold into dark
     bronze -- see #goldStroke in console.html) rather than a flat CSS colour
     override: a flat bright stroke on a 4px band is what read as cartoonish.
     No drop-shadow either, for the same two reasons as the minor arcs above:
     a faint halo on an etched line is not visible, and these two ellipses
     are the widest shapes in the box, so filtering them is the most
     expensive pass on the panel. */
  transform-origin: 150px 150px;
  filter: none;
}

/* The constellation layers. Sub-pixel strokes at low opacity do the work;
   the band turns almost imperceptibly so it reads as live telemetry rather
   than a static texture. The inner mesh (on the sphere, under the USA dots)
   stays still -- a mesh sliding beneath a fixed continent would break the
   "one ball" illusion. */
.mesh { pointer-events: none; }
.mesh-band { transform-origin: 150px 150px; }
@media (prefers-reduced-motion: no-preference) {
  .mesh-band { animation: rot 240s linear infinite; }
}
.reactor.down .mesh { opacity: .4; }
@media (prefers-reduced-motion: no-preference) {
  .holo-ring-outer { animation: rot 74s linear infinite; }
  .holo-ring-inner { animation: rot 55s linear infinite reverse; }
}
.reactor.down .holo-ring { opacity: .3; }

/* What is left of the graticule after "less of vertical and horizontal
   lines": five faint curves rather than a full cage, at a quarter of the old
   opacity. They sit under the dot map and are there only so the ball still
   has a surface when your eye is away from the continent -- the sphere's
   volume is carried by the shading and by the dot falloff now, not by
   linework. */
.globe-grid {
  fill: none;
  stroke: var(--reactor-highlight);
  stroke-width: .9;
  opacity: .11;
}
.reactor.down .globe-grid { opacity: .06; }
.reactor.down .us-dots { opacity: .4; }
.reactor.down .us-cities { opacity: .5; }

/* Three real states, not decoration: the arc reads differently depending on
   what the engine is actually doing right now, each reusing a class that
   either already existed (.listening, set by the wake-word code) or gets
   set at the one or two real call sites for that state (.responding around
   every await that produces a reply; .talking for as long as
   speechSynthesis actually has something queued). */
.reactor.responding .flow-arc {
  opacity: 1; filter: drop-shadow(0 0 11px color-mix(in srgb, var(--gold) 75%, transparent));
  animation: ring-pulse 1.1s ease-in-out infinite;
}
.reactor.listening .flow-arc {
  opacity: 1; filter: drop-shadow(0 0 12px color-mix(in srgb, var(--gold) 80%, transparent));
}
.reactor.talking .flow-arc {
  opacity: 1; filter: drop-shadow(0 0 13px color-mix(in srgb, var(--gold) 85%, transparent));
  animation: ring-pulse .55s ease-in-out infinite;
}
@keyframes ring-pulse {
  0%, 100% { filter: drop-shadow(0 0 8px color-mix(in srgb, var(--gold) 60%, transparent)); }
  50%      { filter: drop-shadow(0 0 16px color-mix(in srgb, var(--gold) 90%, transparent)); }
}
/* One flash per spoken word, layered on top of the steady talking pulse
   above -- see speakReply()'s own comment on why this is tied to
   SpeechSynthesisUtterance's real "boundary" events (Chrome/Edge) rather
   than a made-up fixed interval, and why it degrades gracefully (the steady
   pulse alone) where that event isn't fired. Restarted by anvella.js
   removing then re-adding this class, which needs a reflow in between for
   the animation to actually restart -- see the JS comment for that too. */
.reactor.beat .flow-arc { animation: ring-beat .22s ease-out; }
@keyframes ring-beat {
  0%   { filter: drop-shadow(0 0 20px color-mix(in srgb, var(--gold) 95%, transparent)); }
  100% { filter: drop-shadow(0 0 13px color-mix(in srgb, var(--gold) 85%, transparent)); }
}
@media (prefers-reduced-motion: reduce) {
  .reactor.responding .flow-arc, .reactor.talking .flow-arc, .reactor.beat .flow-arc {
    animation: none;
  }
}

/* The core breathes only while the engine is actually up. A panel that pulses
   identically when the model is offline is decoration; this one stops. */
.reactor.live .globe-tint { animation: breathe 3.4s ease-in-out infinite; }
@keyframes breathe {
  0%, 100% { filter: drop-shadow(0 0 14px color-mix(in srgb, var(--gold) 40%, transparent)); }
  50%      { filter: drop-shadow(0 0 30px color-mix(in srgb, var(--gold) 78%, transparent)); }
}
/* Paused and offline are states you should be able to read across the room. */
.reactor.paused::after { animation-duration: 22s; opacity: .4; }
.reactor.down::after { animation: none; opacity: .18; }

.reactor-text {
  /* Above the near half of the orbits (.orbit-front, z 1), which is
     kept from the department-node work and now draws inside this
     button: a specialist crossing the face of the core passes
     BEHIND the words. .reactor-agent already carries z-index 2. */
  z-index: 3;
  /* Centred and SHRUNK TO THE WORDS, not inset:0 of the button. The scrim
     below has to be sized to the text -- a scrim sized to the sphere is
     just a darker sphere -- and a box that is the words is the only thing
     that stays sized to them when the model name changes length or the
     type scales with the dial. max-width keeps a long model name from
     reaching the rings. */
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  max-width: 56%;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center; text-align: center;
  pointer-events: none;
  /* Legibility now comes from the text itself rather than from a disc drawn
     behind it -- that disc was the second sphere and it is gone. A dark halo
     tight around each glyph does the same job over a busy dot map without
     putting another object on the globe. */
  text-shadow: 0 0 10px rgba(12, 9, 5, .92), 0 0 22px rgba(12, 9, 5, .75);
  /* The dial's own side, so the supporting lines below keep their
     proportion to the sphere instead of being 12px on a 266px dial and 12px
     on a 576px one. Mirrors the clamp() on .reactor, and the short-viewport
     rule further down re-states it for the same reason. */
  --reactor-side: clamp(200px, min(64vh, 86vw), 860px);
}
/* The scrim: a soft, feathered pool of the page's own dark, between the map
   and the words.
   ==========================================================================
   This is the defect that got the whole reactor stripped once already, and
   removing the map is not the fix -- the operator asked for the map. So the
   map stays everywhere except directly under the letters.

   MEASURED, not styled. tests/test_reactor_readout.py photographs each text
   run's own box with the glyphs made transparent, takes the 95th-percentile
   background luminance (the bright tail is what a white letter has to beat:
   the near-white city dots) and computes the WCAG ratio against that run's
   own colour. Before this rule, at 1440x900: ONLINE 1.60:1, the model line
   1.44:1, "local" 1.33:1 and "click to speak" 1.18:1 -- every one of them
   under the 3:1 floor for large text, let alone 4.5:1.

   Nearly opaque in the middle and gone by 88%, which is the same shape and
   the same near-opaque centre the marketing hero's .hero-copy::before uses
   over the same shader (index.html), for the same reason: a half-strength
   scrim over near-white dots reads as a grey smudge AND still fails, so the
   honest choice is a scrim dark enough to work and feathered far enough
   that its edge is never a line. Sized off .reactor-text, which is the
   words rather than the sphere, so the pool is a caption-sized ellipse on a
   globe rather than a second, darker globe. */
.reactor-text::before {
  content: ""; position: absolute; inset: -80% -40%;
  z-index: -1; pointer-events: none;
  /* Eight stops, and the last four are the point. A three-stop version of
     this measured fine and LOOKED like a dark oval pasted on the continent:
     the alpha held near .9 across a wide flat core and then fell away over
     a short span, which is exactly how you draw an edge. The core is tight
     to the words now and the tail runs to nothing over more than half the
     radius, so there is nowhere for the eye to find a boundary. */
  background: radial-gradient(ellipse 50% 42% at 50% 50%,
    rgba(9, 7, 4, .87) 0%, rgba(9, 7, 4, .83) 32%, rgba(9, 7, 4, .72) 48%,
    rgba(9, 7, 4, .52) 60%, rgba(9, 7, 4, .31) 70%, rgba(9, 7, 4, .15) 80%,
    rgba(9, 7, 4, .05) 90%, transparent 100%);
}
.reactor-text b {
  /* Sans, not the monospace the rest of the panel's labels use -- the
     status word is the one piece of text meant to read at a glance from
     across the room, and a reference screenshot of the actual target look
     used a clean sans-serif here specifically, with the monospace/uppercase
     treatment reserved for the smaller supporting labels around it. */
  font: 600 20px var(--font); letter-spacing: .04em;
  color: var(--text); position: relative; z-index: 1;
  /* The block's dark halo FIRST, then the gold lift. Written the other way
     round this replaced the halo with a 6px shadow of --bg (see the
     supporting lines below for the same bug and the same fix): the gold
     pass is a glow, not a backing, so the state word was reading straight
     onto the continent with nothing dark behind it. */
  text-shadow: 0 0 10px rgba(12, 9, 5, .95), 0 0 22px rgba(12, 9, 5, .8),
               0 0 18px color-mix(in srgb, var(--gold) 60%, transparent),
               0 1px 6px var(--bg);
}
/* The supporting lines. They used to REPLACE the block's halo with
   `0 1px 6px var(--bg)` -- a 6px drop shadow, where the halo above is 10px
   and 22px of near-black at .92 and .75. text-shadow inherits, so
   overriding it here traded the one thing making these readable over a lit
   dot map for a much weaker one, and "local" under the model name was
   illegible on the restored instrument at every size checked. They keep the
   block's halo now and add their own tight shadow on top of it. */
.reactor-text span {
  font-size: clamp(11px, calc(var(--reactor-side) * .023), 15px);
  letter-spacing: .12em; color: var(--text-2); margin-top: 6px;
  text-shadow: 0 0 8px rgba(12, 9, 5, .95), 0 0 18px rgba(12, 9, 5, .8),
               0 1px 4px var(--bg);
}

.hud-side { display: flex; flex-direction: column; gap: 12px; }
.hud-side.right { align-items: flex-end; text-align: right; }
.hud-read { display: flex; flex-direction: column; gap: 3px; position: relative; z-index: 1; }
.hud-read span {
  font-size: 9px; letter-spacing: .16em; color: var(--muted);
}
.hud-read b {
  font-family: var(--mono); font-size: 15px; font-weight: 400; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 30ch;
}
.hud-read.paid b { color: var(--gold); }

@media (max-width: 820px) {
  .hud { grid-template-columns: 1fr 1fr; }
  /* The reactor's hoist out of this grid is gone with the reactor: it is not
     a child of .hud any more (see #eye-stage), so a grid-column and an order
     for it here would be a rule about an element that is not in this box. */
  .hud-side.right { align-items: flex-start; text-align: left; }
}
@media (prefers-reduced-motion: reduce) {
  .reactor.live .globe-tint { animation: none; }
  #load-ring { transition: none; }
}

/* --- sidebar ------------------------------------------------------------- */
/* Navigation moved off the top strip because the list stopped fitting: eleven
   destinations wrap on a laptop, and a wrapping tab strip is how a tool starts
   feeling improvised. A vertical rail also has room for the grouping that makes
   eleven items readable — workspace, knowledge, engine. */

.shell { display: flex; align-items: stretch; min-height: calc(100vh - 56px); }

.rail {
  flex: 0 0 232px; border-right: 1px solid var(--line); background: var(--surface);
  display: flex; flex-direction: column; padding: 14px 10px 10px;
  position: sticky; top: 56px; height: calc(100vh - 56px); overflow-y: auto;
  /* Without this, scrolling past the rail's own bottom hands the scroll
     gesture to the page behind it -- the default browser behaviour for any
     nested scroll container once it hits its own limit. `contain` stops the
     scroll right there instead of dragging the whole page down with it. */
  overscroll-behavior: contain;
}
.rail.collapsed { display: none; }
.shell > main { flex: 1 1 auto; min-width: 0; }

.rail-search { position: relative; margin-bottom: 14px; }
.rail-search input { font-size: 13px; }
.omni-results {
  position: absolute; top: 100%; left: 0; right: 0; z-index: 40; margin-top: 6px;
  background: var(--raised); border: 1px solid var(--line); border-radius: 8px;
  max-height: 340px; overflow-y: auto;
  box-shadow: 0 14px 34px rgba(0,0,0,.5);
}
.omni-results button {
  display: flex; gap: 8px; align-items: baseline; width: 100%; text-align: left;
  background: none; border: 0; border-bottom: 1px solid var(--line);
  padding: 9px 11px; color: var(--text); font-size: 12.5px; cursor: pointer;
}
.omni-results button:last-child { border-bottom: 0; }
.omni-results button:hover { background: var(--surface-2); }
.omni-results i {
  font-style: normal; font-size: 9px; letter-spacing: .08em; color: var(--muted);
  text-transform: uppercase; flex: 0 0 52px;
}
.omni-results span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Five destinations, the panels disclosed under them (see the markup's own
   note). The destination row is the primary reading unit: 14px, weight 600,
   an 18px icon; the items under it are the secondary tier at 13px, indented
   under the icon column so the eye reads two levels rather than one long
   list. <details>/<summary> is the disclosure -- native keyboard operation,
   native expanded state -- and the only script involvement is opening the
   group holding the active tab. */
/* The icon sprite is markup, not layout: absolutely positioned so its
   zero-sized box does not leave an empty line above the top bar (it did --
   23px, measured). */
.sprite { position: absolute; width: 0; height: 0; overflow: hidden; }
.rail-nav {
  display: flex; flex-direction: column; gap: 2px; flex: 1 1 auto;
  overflow: visible;
}
.rail-nav .ico {
  width: 18px; height: 18px; flex: 0 0 18px;
  fill: none; stroke: currentColor; stroke-width: 1.6;
  stroke-linecap: round; stroke-linejoin: round;
  color: var(--muted);
}
.rail-dest { border-radius: 8px; }
.rail-dest-head {
  display: flex; align-items: center; gap: 10px;
  list-style: none; cursor: pointer; user-select: none;
  padding: 8px 10px; border-radius: 8px;
  color: var(--text-2); font-size: 14px; font-weight: 600; letter-spacing: .01em;
}
.rail-dest-head::-webkit-details-marker { display: none; }
.rail-dest-head:hover { background: var(--surface-2); color: var(--text); }
.rail-dest-name { flex: 1 1 auto; }
/* The chevron is the one part of the row that turns: the open group points
   down, a closed one points right. Transform only, and it stands still
   under prefers-reduced-motion (the universal net below covers it). */
.rail-chev {
  width: 14px; height: 14px; flex: 0 0 14px; color: var(--muted);
  transition: transform .15s ease;
}
.rail-dest[open] > .rail-dest-head .rail-chev { transform: rotate(90deg); }
/* The group holding the current panel is lit the way the panel's own row
   is, so a collapsed rail still says which destination you are in. */
.rail-dest:has(button[aria-selected="true"]) > .rail-dest-head {
  color: var(--text);
}
.rail-dest:has(button[aria-selected="true"]) > .rail-dest-head .ico { color: var(--gold); }
/* A count somewhere inside a CLOSED group: one gold dot on the summary,
   set by the console script's badge() so a waiting approval is not hidden
   by the disclosure that hides its row. */
.rail-dest-dot {
  width: 7px; height: 7px; border-radius: 50%; background: var(--gold);
  box-shadow: 0 0 6px var(--gold); flex: 0 0 7px;
}
.rail-dest[open] > .rail-dest-head .rail-dest-dot { display: none; }
.rail-dest-items {
  display: flex; flex-direction: column; gap: 1px; padding: 2px 0 6px;
}
.rail-nav button {
  display: flex; align-items: center; gap: 10px; width: 100%; text-align: left;
  background: none; border: 0; border-radius: 7px; padding: 7px 10px 7px 12px;
  color: var(--text-2); font-size: 13px; cursor: pointer;
  /* The generic `nav button` rule above tracks and uppercases; that is the
     old tab-strip voice, and the critique's "internal diagnostics" note.
     Rail rows read as words, the same case as their destination. */
  text-transform: none; letter-spacing: 0;
}
/* Secondary items sit under the destination's icon column: a 16px icon
   inset so the item's text starts where the destination's text starts. */
.rail-dest-items button { padding-left: 20px; }
.rail-dest-items .ico { width: 16px; height: 16px; flex-basis: 16px; }
/* Home is a single row that is also a destination, so it wears the
   destination's own weight and icon size. */
.rail-dest-home button { font-size: 14px; font-weight: 600; padding: 8px 10px; }
.rail-nav button b {
  margin-left: auto; font-family: var(--mono); font-size: 11px; font-weight: 400;
  color: var(--muted);
}
.rail-nav button:hover { background: var(--surface-2); color: var(--text); }
.rail-nav button:hover .ico { color: var(--text-2); }
.rail-nav button[aria-selected="true"] {
  background: color-mix(in srgb, var(--gold) 12%, transparent);
  color: var(--text); box-shadow: inset 2px 0 0 var(--gold);
}
.rail-nav button[aria-selected="true"] .ico { color: var(--gold); }

/* The system drawer at the foot: closed by default, the same summary shape
   as a destination but quieter, because it is a status readout rather than
   somewhere to go. */
.rail-foot { border-top: 1px solid var(--line); padding: 8px 0 4px; margin-top: 10px; }
.rail-system > summary {
  display: flex; align-items: center; gap: 10px; list-style: none; cursor: pointer;
  padding: 8px 10px; border-radius: 8px;
  color: var(--muted); font-size: 12px; font-weight: 600; letter-spacing: .02em;
}
.rail-system > summary::-webkit-details-marker { display: none; }
.rail-system > summary:hover { background: var(--surface-2); color: var(--text); }
.rail-system > summary span { flex: 1 1 auto; }
.rail-system > summary .ico {
  width: 16px; height: 16px; flex: 0 0 16px;
  fill: none; stroke: currentColor; stroke-width: 1.6;
  stroke-linecap: round; stroke-linejoin: round;
}
.rail-system[open] > summary .rail-chev { transform: rotate(90deg); }
.rail-system > :not(summary) { margin-left: 10px; margin-right: 10px; }
.rail-project { font-size: 13px; color: var(--text); margin-top: 6px; }
.rail-meta { font-family: var(--mono); font-size: 11px; color: var(--muted); margin-top: 3px; }
.rail-guide {
  margin-top: 10px; padding-top: 10px;
  border-top: 1px solid color-mix(in srgb, var(--gold) 14%, transparent);
}
.rail-guide summary {
  cursor: pointer; font-size: 11px; letter-spacing: .1em; text-transform: uppercase;
  color: var(--muted); list-style: none;
}
.rail-guide summary::-webkit-details-marker { display: none; }
.rail-guide summary::before { content: "▸ "; }
.rail-guide[open] summary::before { content: "▾ "; }
.rail-guide p { font-size: 11.5px; color: var(--text-2); line-height: 1.5; margin: 8px 0 0; }
.rail-guide p b { color: var(--text); font-weight: 600; }

.rail-toggle {
  background: none; border: 1px solid var(--line); color: var(--text-2);
  border-radius: 7px; padding: 6px 8px; cursor: pointer; font-size: 14px;
  display: inline-flex; align-items: center;
}
.rail-toggle .ico {
  width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.8;
  stroke-linecap: round;
}
.rail-toggle:hover { color: var(--text); border-color: var(--gold); }

/* On a phone the rail would eat the screen, so it starts hidden and the toggle
   brings it over the content rather than beside it. */
@media (max-width: 760px) {
  .rail {
    position: fixed; left: 0; top: 56px; z-index: 60; width: 232px;
    box-shadow: 0 0 40px rgba(0,0,0,.6);
  }
}

/* --- HUD controls --------------------------------------------------------- */
.hud-read span { text-transform: none; letter-spacing: .1em; font-size: 10px; }
.hud-read a { color: inherit; text-decoration: none; border-bottom: 1px dotted var(--line); }
.hud-read a:hover { color: var(--gold); border-bottom-color: var(--gold); }

.hud-pick {
  background: color-mix(in srgb, var(--gold) 7%, transparent);
  border: 1px solid color-mix(in srgb, var(--gold) 26%, transparent);
  color: var(--text); font-family: var(--mono); font-size: 13px;
  padding: 5px 8px; border-radius: 7px; max-width: 190px;
}
.hud-side.right .hud-pick { margin-left: auto; }

.seg { display: inline-flex; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
.seg button {
  background: none; border: 0; color: var(--text-2); cursor: pointer;
  font-size: 11px; padding: 5px 9px; font-family: var(--mono);
}
.seg button[aria-pressed="true"] {
  background: color-mix(in srgb, var(--gold) 18%, transparent); color: var(--text);
}
.seg button:hover { color: var(--text); }

/* --- composer ------------------------------------------------------------- */
/* A blank textarea gives no signal about what it is connected to. This shows
   the model that will answer and whether the brain is up, so the thing you are
   about to talk to is visible before you talk to it. */
/* The box has to read as a box you type in, from across the room and before
   reading a word of it. It did not: a 1px hairline the same colour as every
   other divider, sitting on --surface-2 which is barely off the panel it sits
   on, wrapped around a 23px textarea. Measured, not guessed -- the input was
   23px tall and its whole frame was indistinguishable from a bordered div.
   Three things fix that and they are all here: a real resting elevation, a
   background that separates from the panel behind it, and enough internal
   height that the empty state is obviously somewhere to write. */
.composer {
  border: 1px solid color-mix(in srgb, var(--gold) 26%, var(--line));
  border-radius: 16px;
  background: var(--surface);
  padding: 14px 16px 12px;
  box-shadow: 0 2px 14px -6px rgba(0, 0, 0, .5),
              inset 0 1px 0 color-mix(in srgb, var(--gold) 7%, transparent);
  transition: border-color .15s, box-shadow .15s;
}
.composer:focus-within {
  border-color: color-mix(in srgb, var(--gold) 70%, transparent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--gold) 16%, transparent),
              0 4px 18px -6px rgba(0, 0, 0, .55);
}
.composer-rail { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
.composer-dot {
  width: 7px; height: 7px; border-radius: 50%; background: var(--sage);
  box-shadow: 0 0 8px var(--sage);
}
.composer-dot.down { background: var(--danger); box-shadow: 0 0 8px var(--danger); }
.composer-model {
  font-family: var(--mono); font-size: 11px; color: var(--muted); letter-spacing: .04em;
}
/* min-height, not rows="1": a one-row textarea is 23px, which is the height
   of a line of body text and reads as one. Resting height is what makes an
   empty composer look like somewhere to write rather than a caption that
   happens to have a cursor. It still auto-grows from here (the JS sets height
   from scrollHeight) and still caps at max-height.
   Three rows (76px) until 2026-09-06, when the composer became the command
   bar at the TOP of Home and every pixel of it started pushing the
   instrument down the page. 56px is two rows, still unmistakably a box, and
   it is the same 56px the design spec sets as the command bar's minimum
   height. */
.composer textarea {
  border: 0; background: none; padding: 0; resize: none; width: 100%;
  font-size: 16px; line-height: 1.55;
  min-height: 56px; max-height: 40vh;
}
.composer textarea:focus { outline: none; box-shadow: none; }
/* The placeholder is the only thing in an empty box, so it carries the whole
   affordance. It was --muted, the same grey as every hint and caption on the
   page, which is what made it read as another line of small print. */
.composer textarea::placeholder {
  color: color-mix(in srgb, var(--text) 52%, transparent);
  opacity: 1;
}
.composer-actions {
  display: flex; align-items: center; gap: 8px;
  margin-top: 8px;
}
.composer-hint { font-size: 11px; color: var(--muted); margin-right: auto; }
.composer-actions .btn { width: auto; padding: 7px 16px; }
.composer-actions .btn.send { padding: 7px 20px; }

/* Share/wake-word/speak-aloud: real controls, kept out of the primary row so
   the composer reads as "type, attach, talk, send" rather than a toolbar. */
.composer-secondary {
  display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px;
  /* Quieter than the primary row, and visibly a second tier rather than
     eight equal buttons in a heap -- what made the box read as a jumble was
     that every control in it had the same weight. */
  padding-top: 8px;
  border-top: 1px solid color-mix(in srgb, var(--line) 60%, transparent);
  opacity: .8;
}
.composer:focus-within .composer-secondary { opacity: 1; }
.btn.ghost.tiny {
  width: auto; padding: 4px 10px; font-size: 11px; color: var(--muted);
}
.btn.ghost.tiny:hover { color: var(--text); }
.btn.ghost.tiny[aria-pressed="true"] { color: var(--gold); border-color: var(--gold-mid); }

/* Openers, so an empty console is not a blank stare. */
.chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.chips button {
  background: none; border: 1px solid var(--line); border-radius: 999px;
  color: var(--text-2); font-size: 12px; padding: 6px 13px; cursor: pointer;
}
.chips button:hover { border-color: var(--gold); color: var(--text); }

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

/* --- earlier conversations ------------------------------------------------ */
.recent-title {
  font-size: 10px; letter-spacing: .16em; text-transform: uppercase;
  color: var(--muted); margin: 22px 0 8px; font-weight: 400;
}
.recent { display: flex; flex-direction: column; gap: 2px; }
.recent-item { border-bottom: 1px solid var(--line); }
.recent-item:last-child { border-bottom: 0; }
.recent-head {
  display: flex; align-items: center; gap: 10px; width: 100%; text-align: left;
  background: none; border: 0; color: var(--text-2); cursor: pointer;
  padding: 9px 8px; border-radius: 7px; font-size: 13px;
}
.recent-head:hover { background: var(--surface-2); color: var(--text); }
.recent-caret { color: var(--gold); font-size: 10px; width: 10px; }
.recent-day { flex: 1 1 auto; }
.recent-date { font-family: var(--mono); font-size: 11px; color: var(--muted); }
.recent-body {
  font-family: var(--mono); font-size: 12px; white-space: pre-wrap;
  margin: 0 0 12px; padding: 12px 14px; max-height: 46vh; overflow: auto;
  background: var(--bg); border: 1px solid var(--line); border-radius: 8px;
  color: var(--text-2);
}

/* --- status panel ----------------------------------------------------------
   A plain status bar: stat readouts flank a small talk button, a telemetry
   strip runs beneath. Was an animated "instrument panel" -- scanlines, corner
   brackets, a 340px glowing reactor with an orbit ring of avatars around it --
   simplified to match a normal AI chat product rather than a HUD. */

/* The section is itself a three-column grid, so every direct child becomes a
   column. The inner grid, the roster and the telemetry strip are full-width
   bands and must say so — without this the roster was dealt into the middle
   column and wrapped into a 2-wide block beside the reactor. */
/* Top padding stays at 18px, and that is a measured decision rather than an
   oversight. Pushing the reactor down the panel was asked for, and a first
   pass raised this to 34px -- which put the composer's bottom edge at
   1008.9px on the 1000px-tall fixture and failed
   test_the_composer_is_above_the_fold_on_a_laptop. That test's own docstring
   is the warning: "every control added to the HUD pushes this down and
   nobody notices until it is off-screen."
   The week strip above already moves the reactor down the page, which is the
   effect that was wanted; spending the remaining headroom on padding as well
   costs the chat box its place on screen, and the chat box wins. */
.hud { padding: 18px 20px; border-radius: 12px; }
.hud > .hud-grid,
.hud > .hud-metrics-band,
.hud > .agents,
.hud > .deck,
.hud > .today,
.hud > .hud-strip { grid-column: 1 / -1; }
/* THE EYE'S HOST. One box, above the command bar, holding the whole
   instrument: the far-orbit canvas, both node hosts and the reactor button.
   It is `position: relative` because `.orbit`, `.orbit-labels` and
   `.orbit-near` are all `inset: 0` and have to resolve against the same box
   the reactor sits in -- hud.js reads that box (Constellation.centre() and
   resize(), via canvas.parentElement) and maps both canvases and every node
   onto it, so one shared positioned ancestor is what keeps a specialist's
   dot on its ring.

   Nothing here sizes the instrument. The reactor keeps its own clamp() (see
   the .reactor rule further down) and this host is only wide enough to
   centre it; the height comes from the button. That is deliberate -- the
   Eye is a drop-in, and #eye-stage is the stable mount point for replacing
   it without touching the page around it. */
.eye-stage {
  position: relative;
  display: flex; justify-content: center; align-items: flex-start;
  margin: 0 0 14px;
}
/* The orbit ring sits on top of the Eye's host, centred on the reactor --
   the host is a centring flex box sized by the button, so the centre of
   this box is the centre of the reactor without measuring anything. */
.orbit {
  position: absolute; inset: 0; z-index: 2;
  pointer-events: none;
}
.orbit canvas { position: absolute; inset: 0; display: block; }
/* The near half of the orbits: a canvas inside the reactor button, above
   the sphere (the SVG and the shader canvas) and below the words
   (.reactor-text, z-index 2), so a specialist passes in front of the core
   and behind the state text. Overhangs the button by a third each way so a
   dot at a ring's far edge is not cut off; the overhang takes no clicks. */
.orbit-front {
  /* Explicit width/height, not inset alone: a canvas is a replaced element
     and keeps its intrinsic 300x150 when only its edges are set. */
  position: absolute; left: -34%; top: -34%; width: 168%; height: 168%;
  z-index: 1; display: block; pointer-events: none;
}
/* The two node hosts. Same box as .orbit (inset 0 of #eye-stage) so a node
   positioned at the constellation's host-relative (x, y) lands on the same
   screen point whichever host it is in; the far host sits under the
   reactor (z 2 < 3) and the near host over it (z 4 > 3). Neither takes
   clicks itself -- only the nodes do -- so the reactor stays the largest
   button on the page with nothing in front of it that eats a press. */
.orbit-labels { position: absolute; inset: 0; pointer-events: none; z-index: 2; }
.orbit-near { position: absolute; inset: 0; pointer-events: none; z-index: 4; }
.orbit-labels[hidden], .orbit-near[hidden] { display: none; }

/* A department node: the specialist on its orbit, as a control.
   28-32px (hud.js sets --node from the core radius, clamped), a dark plate
   with a 1.5px rim and a glyph in the department's own colour -- the same
   DEPT_COLOR the Specialists card uses, so the ring and the row are the
   same seven things. The old 16px canvas disc with an emblem inset read as
   a dead pixel; a glyph at 14px on a plate this size reads as an
   instrument's indicator. Positioned by transform from hud.js every frame
   (the transition is on state, not on position, so it never lags the
   orbit). */
.orbit-node {
  --node: 30px;
  /* The department's colour, written per node by hud.js (mountNode sets
     --node-colour from DEPT_COLOR); gold is only the fallback for a node
     that arrived without one. Declared here so the stylesheet defines
     every variable it uses (test_frontend.py). */
  --node-colour: var(--gold);
  position: absolute; left: 0; top: 0; width: var(--node); height: var(--node);
  padding: 0; margin: 0; border-radius: 50%; cursor: pointer;
  pointer-events: auto; -webkit-tap-highlight-color: transparent;
  color: var(--node-colour, var(--gold));
  /* The BUTTON is transparent and stays 28-32px, which is the hit target.
     The visible indicator is the ::before below, drawn at 66% of it. Against
     the restored instrument -- hairlines at .15-.4 opacity and a graticule
     of sub-pixel curves -- a 30px opaque plate with a 1.5px rim read as a
     fat dot dropped on top of the panel rather than as part of it, which is
     the "styled to belong to the instrument" the operator asked for. This
     way the mark gets smaller and lighter without the control getting
     harder to hit, and 28px stays clear of the 24px floor for a touch
     target where a 20px plate would not. */
  background: none; border: 0;
  display: grid; place-items: center;
  font: 600 calc(var(--node) * .34) / 1 "Segoe UI Symbol", var(--mono), system-ui, sans-serif;
  will-change: transform;
  transition: opacity .35s ease, filter .35s ease;
}
/* The indicator itself: a dark disc under the glyph with a hairline rim in
   the department's colour, the same weight as the rings it sits among. */
.orbit-node::before {
  content: ""; position: absolute; left: 17%; top: 17%; width: 66%; height: 66%;
  border-radius: 50%; pointer-events: none;
  background: rgba(11, 9, 6, .82);
  border: 1px solid color-mix(in srgb, currentColor 62%, transparent);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .5);
  transition: border-color .35s ease, box-shadow .35s ease;
}
.orbit-node-glyph {
  display: block; line-height: 1; position: relative;
  transform: translateY(.02em);
}
/* The name, on demand: hover, keyboard focus, or while this one is
   working. Not permanently -- seven floating names were tried and
   reverted as clutter (see hud.js setAgents). */
.orbit-node-name {
  /* Measured from the disc's bottom edge (83% of the box), not the box's,
     now that the button is a transparent hit target larger than its mark. */
  position: absolute; top: calc(83% + 6px); left: 50%; transform: translateX(-50%);
  font: 600 9px / 1 var(--mono); letter-spacing: .16em; text-transform: uppercase;
  white-space: nowrap; color: var(--text);
  background: rgba(11, 9, 6, .94); padding: 4px 7px; border-radius: 4px;
  border: 1px solid color-mix(in srgb, currentColor 45%, transparent);
  opacity: 0; pointer-events: none; transition: opacity .15s ease;
}
.orbit-node:hover .orbit-node-name,
.orbit-node:focus .orbit-node-name,
.orbit-node.busy .orbit-node-name { opacity: 1; }
/* Negative offset, so the ring lands just outside the DISC rather than
   3px outside a hit target that is half again its size. */
.orbit-node:focus-visible {
  outline: 2px solid currentColor; outline-offset: -3px;
}
/* The one at work glows in its own colour and the rest step back, so
   "who is busy" is a ratio the eye reads from across the room. */
.orbit-node.busy::before {
  border-color: currentColor;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .5),
              0 0 12px 1px color-mix(in srgb, currentColor 60%, transparent),
              0 0 28px 5px color-mix(in srgb, currentColor 24%, transparent);
}
.orbit-node.busy { animation: node-pulse 1.6s ease-in-out infinite; }
.orbit-node.recede { opacity: .5; }
/* On the node itself rather than the disc, so the glyph and its rim pulse
   together and the keyframes stay one property. */
@keyframes node-pulse {
  0%, 100% { filter: drop-shadow(0 0 3px color-mix(in srgb, currentColor 45%, transparent)); }
  50%      { filter: drop-shadow(0 0 8px color-mix(in srgb, currentColor 80%, transparent)); }
}
@media (prefers-reduced-motion: reduce) {
  .orbit-node.busy { animation: none; }
  .orbit-node { transition: none; }
}

/* THE WEEK STRIP. It was above the reactor when it was a strip across the
   top; it is a column inside the work grid's right-hand band now, and the
   reactor is above both.
   Seven equal columns so the shape of the week is readable at a glance
   without counting -- an auto-sized grid would make a busy Thursday wider
   than an empty Friday and lose exactly the comparison this is for.
   Deliberately short: it is a glance, not the Tasks panel. Anything past
   WEEKBAR_MAX items collapses to "+n more" rather than growing the strip. */
/* No container width of its own any more: the week lives inside the HUD's
   right-hand column now (see .hud-col in the Enterprise Home rules below),
   and the column decides how wide it is. */
.weekbar { margin: 0; padding: 0; }
.weekbar-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 0 2px 6px;
}
.weekbar-head em {
  font-style: normal; font-size: 11px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--muted);
}
.weekbar-refresh {
  background: none; border: 0; color: var(--muted); cursor: pointer;
  font-size: 13px; line-height: 1; padding: 2px 6px; border-radius: 4px;
}
.weekbar-refresh:hover { color: var(--gold); }
/* Seven rows, not seven columns.
   The week was a strip across the top of Home until 2026-09-06 and is a
   column beside the instrument now, which settles a question the strip could
   never answer well: seven columns is ~44px a day on a 375px phone, which
   truncates every item to a couple of characters, and the fix for that was a
   sideways scroller -- a week you have to drag to read the end of. Down the
   page there is no such trade: every day is full width, every item is legible,
   and "what is on Thursday" is answered by looking rather than by scrolling.
   It also costs nothing at the fold, because the column it lives in is capped
   at the instrument's own height (see .hud-col). */
.weekbar-days {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: minmax(0, 1fr); gap: 4px;
}
.weekday {
  border: 1px solid var(--line); border-radius: 8px;
  /* Every pixel here comes straight off the composer's headroom -- the strip
     sits above the reactor, so its height is subtracted from what is left
     before the chat box crosses the fold. One compact strip (the critique:
     "seven narrow boxes consume attention even when the user has no
     calendar-driven work"): a day is its name, its number and one line per
     item, 44px when clear. Anything longer belongs in Tasks. */
  padding: 4px 8px 5px; min-height: 44px;
  display: flex; flex-direction: column; gap: 2px;
  background: color-mix(in srgb, var(--gold) 2%, transparent);
}
/* Today is the one column you look for first, so it gets the gold edge --
   the same signal the live reactor ring uses, rather than a new colour. */
.weekday.is-today {
  border-color: var(--gold);
  background: color-mix(in srgb, var(--gold) 7%, transparent);
}
.weekday-name {
  font-size: 10px; letter-spacing: .1em; text-transform: uppercase;
  color: var(--muted); display: flex; justify-content: space-between;
  align-items: baseline; gap: 4px;
}
.weekday.is-today .weekday-name { color: var(--gold); }
.weekday-num { font-size: 11px; opacity: .75; }
.weekday-item {
  font-size: 11px; line-height: 1.3; color: var(--text);
  /* One line then ellipsis: a long task title must not be able to make one
     column taller than the other six and break the week's alignment, and
     the strip is a glance -- the full title is in the tooltip and in Tasks. */
  display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical;
  overflow: hidden;
}
.weekday-item.is-overdue { color: var(--danger-fg); }
/* Calendar events, distinguishable from to-dos at a glance: an appointment
   is a commitment to a time, a to-do is a commitment to a task, and mixing
   them unlabelled makes the strip read as one undifferentiated pile. The
   gold ties it to the accent already used for live things; no new color. */
.weekday-item.is-cal::before { content: "◷ "; color: var(--gold); }
.weekday-more { font-size: 10px; color: var(--muted); }
.weekday-clear { font-size: 11px; color: var(--muted); opacity: .45; }


/* What needs attention, first on Home. Same container width/head styling
   as .weekbar (reusing .weekbar-head/.weekbar-refresh rather than
   duplicating that CSS), but the cards themselves carry a gold edge and a
   raised ground: this is the one block on Home that asks for a decision,
   and it must not read as one more small-card strip among the others. */
/* ---------- the Enterprise Home context header ----------
   Who is here, one line of what is happening, and the rotating line. Two
   cells on one row: the greeting owns the left, the quote the right, and the
   quote gives way first because it is the only decorative thing on the page.
   Deliberately short -- every pixel of height here comes off the composer's
   headroom, the same budget .weekday's comment is spending from. */
.ehome-head {
  display: flex; flex-wrap: wrap; align-items: baseline;
  justify-content: space-between; gap: 6px 24px;
  margin: 0 auto 12px; padding: 0 2px;
}
.ehome-hello { min-width: 0; }
/* An h2 because it is the heading of the page's first region, but not the
   engraved uppercase tag h2 wears inside a panel: the spec's section 2 asks
   for sentence case and no decorative letter-spacing, at 24/32px and weight
   600. The base rule's tracking and case are undone here rather than a <div>
   being used, because the outline a screen reader builds is worth more than
   the CSS saved. */
/* The one true display heading on Home, and since 2026-09-07 it is set in
   the homepage's display face: same family, same weight 500, same -.02em
   tracking and tight leading as index.html's h1-h4 block. The size stays 24
   rather than climbing to the homepage's clamp -- this is a console page
   title, not a marketing hero, and the composer's distance from the fold is
   a measured number that a heading is not allowed to spend. */
.ehome-title {
  margin: 0; font-family: var(--font-display);
  font-size: 24px; line-height: 30px; font-weight: 500;
  letter-spacing: -.02em; text-transform: none; color: var(--text);
}
/* One sentence, and it stays one sentence: the situation line is written from
   module STATES, never from item counts (see homeSituation in anvella.js for
   why a count would be a claim rather than a reading). */
.ehome-situation {
  margin: 8px 0 0; font-size: 14px; line-height: 20px; color: var(--text-2);
}
.ehome-situation:empty { display: none; }

/* The rotating line in Home's top-right (anvella/quotes.py holds the words). */
.home-quote {
  margin: 0; padding: 0; max-width: 46ch;
  display: flex; flex-wrap: wrap; justify-content: flex-end;
  align-items: baseline; gap: 6px;
  text-align: right;
}
/* Italic display serif, which is exactly what the homepage does with a pull
   quote (index.html's .quote and .final-cta em: Fraunces, italic, weight
   500). One line of rotating prose is the one place on this page where the
   marketing surface's voice belongs verbatim. */
.home-quote blockquote {
  margin: 0; font-family: var(--font-display); font-weight: 500;
  font-size: 13px; line-height: 1.45; color: var(--text-2);
  font-style: italic;
}
.home-quote figcaption {
  margin: 0; font-size: 11px; line-height: 1.4; color: var(--muted);
  letter-spacing: .04em; white-space: nowrap;
}
/* The transition, and only for someone who has not asked for less motion.
   Declared as no-preference rather than as a transition that a reduce block
   later cancels: this way the still version is the default and a future edit
   cannot reintroduce motion by adding a property outside the guard. */
.home-quote { opacity: 1; }
@media (prefers-reduced-motion: no-preference) {
  .home-quote {
    opacity: 0; transform: translateY(-2px);
    transition: opacity .45s ease, transform .45s ease;
  }
  .home-quote.settled { opacity: 1; transform: none; }
}

/* Same reasoning as .weekbar: the feed lives inside the HUD's left-hand
   column now, so the column owns the width. */
.action-cards { margin: 0; padding: 0; }
.action-cards-list {
  display: flex; flex-direction: column; gap: 8px;
}
/* A --text-2 edge rather than a gold one. The gold outline was this block's
   way of saying "this is the one thing on Home that asks for a decision", and
   the design spec's section 8 takes it away for a better reason than tidiness:
   "Gold card borders... compete with the Eye and make ordinary operations feel
   more urgent than they are." The emphasis stays -- a 3px edge nothing else on
   Home has -- in a colour that is not the instrument's. */
.action-card {
  border: 1px solid var(--line);
  border-left: 3px solid var(--text-2);
  border-radius: 8px;
  padding: 12px 16px;
  background: var(--surface);
}

/* Home's composed modules, as compact cards rather than a column of headings
   with a list under each (measured 414px tall for five modules, three of them
   "nothing right now"). The LOWER band wraps its cards at 240px apiece; the
   two column bands stack theirs one per row, because a column here is
   250-380px wide and two cards abreast in that is two truncated cards. */
.home-modules {
  margin: 20px auto 14px;
  padding: 0 2px;
}
.home-modules-list {
  display: grid; gap: 8px;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
}
.ehome-zone { min-width: 0; }
.ehome-zone .home-modules-list { grid-template-columns: minmax(0, 1fr); }
.hud-col > * + * { margin-top: 12px; }

/* --surface for the body, --line for the edge, and no gold outline: the spec's
   section 3 reserves gold for the instrument and section 8 rules out "gold
   card borders... they compete with the Eye and make ordinary operations feel
   more urgent than they are". The 3px left edge is the state gutter; READY
   keeps it transparent so all three states share one text inset. */
.home-module {
  border: 1px solid var(--line); border-left: 3px solid transparent;
  border-radius: 8px;
  padding: 12px 16px; min-width: 0;
  background: var(--surface);
}
.home-module-top {
  display: flex; align-items: center; flex-wrap: wrap; gap: 4px 12px;
  margin-bottom: 8px;
}
/* 16/24 at 600, sentence case, no tracking: the spec's type scale for a
   module title. It was 12px engraved caps, which is the console's instrument
   idiom and the wrong one for a heading over readable work. */
/* ...and since 2026-09-07 in the homepage's display face, which is what a
   card heading wears there too (index.html: .platform-card h3, .audience-card
   h3, .addon h4 -- Fraunces, weight 500, negative tracking). Weight 500 and
   -.015em rather than 600/0 for the same reason the homepage uses them: a
   serif at this size does not need the extra weight to hold a card, and the
   tracking is what stops it reading as a body paragraph in bold. */
.home-module-top b {
  font-family: var(--font-display);
  font-size: 17px; line-height: 24px; font-weight: 500; letter-spacing: -.015em;
}
.home-module-top .pill { font-size: 10px; }
.home-module-items {
  margin: 0; padding-left: 18px;
  font-size: 15px; line-height: 22px; font-weight: 500; color: var(--text);
}
.home-module-items li { margin-bottom: 12px; overflow-wrap: anywhere; }
.home-module-items li:last-child { margin-bottom: 0; }
/* Evidence and supporting content drop to 14/20 at 400 -- one clear step
   below the item it belongs to, so a row reads as a title with a source under
   it rather than as two competing sentences. */
.home-module-items .muted { font-size: 14px; line-height: 20px; font-weight: 400; }
.home-module-items a { color: var(--gold); text-decoration: none; }
.home-module-items a:hover { text-decoration: underline; }
.home-module-reason {
  margin: 0; font-size: 14px; line-height: 20px; color: var(--text-2);
}
/* A reason and a list can both be present -- a collapsed READY module has no
   reason, but an EMPTY one that is open has a reason and no list. The gap
   only exists when both do. */
.home-module-reason + .home-module-body:not([hidden]) { margin-top: 8px; }

/* ---------- collapse, and the pin ----------
   Both are real controls, so both are <button>: Enter and Space work without
   a line of script, the focus ring is the page's one focus ring, and there is
   no drag gesture anywhere here that a keyboard would have to be given an
   alternative to (brief section 31 -- the alternative is that the gesture
   does not exist). The title is the fold control itself rather than a label
   beside one, so the whole heading is the hit target. */
/* min(100%, max-content), so the TITLE is never what shrinks: in a 240px
   card "Your views" was breaking across two lines to make room for the state
   chip beside it. The chip wraps to a second line instead, and the title
   still gives way if it alone is wider than the card. */
/* `0 1 auto`, not `1 1 auto`: a growing title fills the row and pushes the pin
   onto a line of its own, which is what it did until this was measured -- the
   state chip is what gives way, and only when it has to. */
.home-module-fold {
  flex: 0 1 auto; min-width: min(100%, max-content);
  display: flex; align-items: center; gap: 8px;
  background: none; border: 0; padding: 0; margin: 0;
  font: inherit; color: var(--text); text-align: left; cursor: pointer;
}
/* Now and Opportunities have no fold, so their title is not a control and
   must not look like one -- see homeModuleCard for why the element differs
   rather than the button being disabled. */
.home-module-fold.is-fixed { cursor: default; }
.home-module-chev {
  font-style: normal; font-size: 10px; line-height: 1; color: var(--muted);
  transition: transform .12s ease;
}
.home-module[data-collapsed] .home-module-chev { transform: rotate(-90deg); }
@media (prefers-reduced-motion: reduce) {
  .home-module-chev { transition: none; }
}
.home-module-fold:hover b { color: var(--gold); }
/* A 44x44 target, per the spec's section 2 -- the glyph is 14px but the thing
   a finger or a shaky hand has to hit is not. */
.home-module-pin {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 44px; min-height: 44px; margin: -12px -8px -12px 0;
  background: none; border: 0; padding: 0;
  font: inherit; font-size: 14px; line-height: 1; cursor: pointer;
  color: var(--muted); border-radius: 8px;
  transition: background-color .1s ease, color .1s ease;
}
/* --text-2 rather than gold: the spec asks for "a small filled pin beside the
   title, using --text-2". Gold is the instrument's colour and a row of gold
   stars down the page would compete with it. */
.home-module-pin[aria-pressed="true"] { color: var(--text-2); }
.home-module-pin:hover { background: var(--raised); color: var(--text); }
@media (prefers-reduced-motion: reduce) {
  .home-module-pin { transition: none; }
}
.home-module-fixed { flex: 0 0 auto; }

/* ---------- the three states, drawn as three different things ----------
   home.py hands every module one of READY / EMPTY / UNAVAILABLE and refuses
   at construction to be READY with nothing in it. The distinction it protects
   is the one the brief names: "not set up" and "set up and empty" are
   different answers and the operator does something different about each --
   one is a two-minute setup task, the other is a free afternoon.

   AVAILABILITY IS NOT SEVERITY, and that is what decides these colours. The
   design spec is explicit: "READY does not mean healthy, and UNAVAILABLE does
   not automatically mean error", and "Do not infer severity from UNAVAILABLE
   alone." An earlier pass here painted every UNAVAILABLE module with
   --warn-bg across the whole body, which said "something is wrong" about a
   calendar feed nobody has linked yet. Nothing in the payload supports that
   claim, so the state now reads NEUTRAL: a muted gutter, a muted mark, and
   the server's own sentence. If a payload ever distinguishes "temporarily
   unreachable" from "not configured", the warning tokens belong to that
   distinction and not to the state.

   And the accent is a gutter, never a fill. Same section: state -bg/-line
   tokens are "small decorative accents in an 8px gutter, separated from the
   text surface... Do not tint entire module bodies." Tinting the body puts
   small text on a translucent wash whose contrast is not the one the token
   table measured, and turns three availability conditions into three
   competing alert cards.

   Colour is never the only carrier either: each state prints its own glyph
   and its own words in the heading, so the difference survives a monochrome
   screen. */
.home-module-state {
  flex: 0 1 auto; margin-right: auto;
  display: inline-flex; align-items: center; gap: 4px;
  font-size: 13px; line-height: 18px; color: var(--muted);
  white-space: nowrap;
}
.home-module-state i { font-style: normal; font-size: 12px; line-height: 1; }

/* EMPTY: checked, and the answer is none. A dashed gutter, because a dashed
   edge reads as "nothing in here" without claiming anything is wrong -- and
   never green, which the spec rules out for exactly that reason. */
.home-module[data-state="empty"] {
  border-left-style: dashed;
  border-left-color: var(--neutral-line);
}
.home-module[data-state="empty"] .home-module-reason { color: var(--muted); }

/* UNAVAILABLE: could not be checked at all. A solid muted gutter with an 8px
   neutral wash inside it -- the accent the spec allows, kept off the text
   surface where the contrast table applies. */
.home-module[data-state="unavailable"],
.home-module[data-state="locked"] {
  border-left-color: var(--muted);
  background:
    linear-gradient(90deg, var(--neutral-bg) 0 8px, transparent 8px),
    var(--surface);
}
/* A CONFIRMED failure is the one case severity is known, because something
   actually failed rather than never being asked. */
.home-module[data-state="error"] {
  border-left-color: var(--danger-fg);
  background:
    linear-gradient(90deg, var(--danger-bg) 0 8px, transparent 8px),
    var(--surface);
}
.home-module[data-state="error"] .home-module-state { color: var(--danger-fg); }
.action-card-top {
  display: flex; align-items: center; justify-content: space-between;
  gap: 8px; margin-bottom: 4px;
}
.action-card-top b { font-size: 13px; }
.action-card-problem {
  margin: 0 0 6px; font-size: 12px; color: var(--text-2); line-height: 1.4;
}
.action-card-value { font-size: 12px; margin-bottom: 6px; }
.action-card-evidence {
  margin: 0 0 6px; padding-left: 18px; font-size: 11px; color: var(--muted);
}
.action-card-evidence li { margin-bottom: 2px; }
.action-card-evidence .evidence-kind { margin-left: 6px; }
.action-card-action {
  font-size: 12px; color: var(--text-2); padding-top: 6px;
  border-top: 1px dashed var(--line);
}
.action-card-action.muted { color: var(--muted); }

/* ---------- the metrics band ----------
   The seven executive readouts, as one ruled strip above the instrument
   rather than two towers beside it. Same .gauge cells, moved rather than
   copied: there is still one element and one writer per figure.

   auto-fit rather than a fixed seven, so the band folds to two rows on a
   laptop and to a pair of columns on a phone without a breakpoint per
   count -- and so an eighth figure would not need this rule edited. */
/* 100px and a 14px gutter, and both are measured rather than chosen. auto-fit
   fits floor((width + gap) / (min + gap)) tracks, and the band has to fit all
   four primary readings on ONE row at the NARROWEST width the three-column
   layout runs at -- 1101px, where the band is ~630px wide once the rail, the
   HUD's padding and the 144px disclosure are taken off:
   floor((630 + 14) / (100 + 14)) = 5, so four fit with room. The tracks are
   1fr, so 100px is a fitting threshold rather than the width anything gets. */
/* One continuous band, not a wall of cards: four readings, then ~144px at the
   right for the disclosure that holds the rest. The readings divide the
   remaining width equally and are separated by the cell rules they already
   carry, with no individual backgrounds. */
.hud-metrics-band {
  display: grid; grid-template-columns: minmax(0, 1fr) 144px;
  column-gap: 16px; align-items: start;
  margin-bottom: 24px;
}
.metrics-more-btn {
  align-self: stretch;
  background: none; border: 0; border-top: 1px solid var(--line);
  padding: 6px 0 6px 12px; margin: 0;
  font: inherit; font-size: 13px; line-height: 18px; text-align: left;
  color: var(--text-2); cursor: pointer;
}
.metrics-more-btn:hover { background: var(--raised); color: var(--gold); }
.metrics-more-btn::after { content: " ▾"; color: var(--muted); }
.metrics-more-btn[aria-expanded="true"]::after { content: " ▴"; }
.hud-metrics-more { grid-column: 1 / -1; margin-top: 12px; }
.hud-metrics-more[hidden] { display: none; }

.hud-metrics {
  display: grid; gap: 0 14px;
  grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
}
/* Two lines, not three, and that is a height decision rather than a taste
   one: this band sits above the instrument, and every pixel of it comes off
   the chat box's distance from the fold on an 800px laptop (see
   test_console_responsive, which pins that distance as a number). The caption
   takes the first line; the figure and its note share the second, on a common
   baseline. Three stacked lines measured 95px; this measures 62. */
.hud-metrics .gauge {
  display: grid; align-items: baseline;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-areas: "label label" "value note";
  column-gap: 7px; row-gap: 1px;
  padding: 6px 0 6px 12px; min-width: 0;
}
.hud-metrics .gauge em { grid-area: label; }
.hud-metrics .gauge b { grid-area: value; }
.hud-metrics .gauge i {
  grid-area: note; min-width: 0;
  /* Two lines, then clipped -- the same clamp .weekday-item uses and for the
     same reason: an unbounded note makes one cell taller than the other six
     and breaks the band's baseline. Two rather than one because the longest
     note on the row ("of your monthly limit - click for a breakdown") is an
     affordance, and one line cut it at "monthly limi...", which reads as a
     rendering bug rather than as a hint. Two lines fit it whole at every
     width from 112px up, and cost 4px of band height. */
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.hud-metrics .gauge::before { width: 12px; }
.hud-metrics .gauge[data-jump]:hover::before { width: 24px; }
/* Smaller than the 26px the figures wore while they were the only thing in
   their column. A metric strip is read at a glance, not studied, and 26px
   across seven cells competed with the instrument the band sits over --
   section 29 allows exactly one dominant visual element. */
.hud-metrics .gauge b { font-size: 19px; line-height: 24px; }

/* ---------- the machine bar ----------
   Which model answers, how hard it thinks, the voice dial, what may be
   driven, whether the engine is running. Below the CONVERSATION, not merely
   below the instrument: section 29 allows one dominant visual element and one
   dominant interaction, and nothing may sit in the gap between the Eye and
   the composer. Section 21 agrees from the other direction -- this is the one
   row on Home that is entirely technical, so it goes last. */
.machine-bar { margin: 14px 0 0; padding: 0 2px; }
.hud-machine {
  display: flex; flex-wrap: wrap; align-items: flex-start;
  justify-content: flex-start; gap: 0 16px;
  margin-top: 0;
}
/* Sized, not stretched. `flex: 1 1 300px` put Model at one end of the panel
   and Effort at the other with 600px of nothing between them, which reads as
   two unrelated controls rather than one row about the machine. */
.hud-machine .gauge { flex: 0 1 auto; min-width: 0; padding: 8px 0 0 12px; }
.hud-machine .gauge[data-gauge="engine"] { flex: 1 1 320px; max-width: 520px; }
.hud-machine .gauge[data-gauge="effort"] { flex: 0 0 200px; }

.hud-grid {
  position: relative;
  /* The work composition: priority work on the left, the business on the
     right. It was three columns with THE EYE `auto`-sized in the middle
     until 2026-09-07; the Eye now leads the page from #eye-stage above the
     command bar, so the middle track went with it and the two work columns
     get the full width evenly.
     minmax(0, ...) rather than a bare fr on both sides: a bare `1fr` is
     `minmax(auto, 1fr)`, whose auto floor is min-content, and one long
     unbroken word in a module row would otherwise set the width of the
     track and push the other column narrow. */
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 24px;
  align-items: start;
}
/* Capped and scrolled, not left to grow. Measured: the columns -- not the
   reactor -- were what pushed the composer to 1124px on an 800px-tall laptop,
   and every tier added to the model picker made it worse. A gauge column that
   scrolls costs a glance; a composer below the fold costs the thing you opened
   the console to do. */
.hud-col {
  display: flex; flex-direction: column; gap: 0; min-width: 0;
  /* Deliberately uncapped, and this is a rule the spec states outright:
     "Modules grow when text requires it. Do not clip evidence, force matching
     heights or introduce internal scrollbars to preserve the illustrated
     fold." An earlier pass here did exactly that -- both columns capped to the
     instrument's own height with an overflow-y and a scroll shadow -- and on a
     720px-tall screen it sliced the Now module through the middle to protect a
     fold that the command bar moving to the top had already made safe. The
     columns are as tall as their work. */
}
/* The mirror is gone with the gauges it was for -- "rail and tick on the
   right, text right-aligned" reads as an instrument's plate and reads as a
   mistake on a card. */
.hud-col.right { align-items: stretch; text-align: left; }
/* A day is one row now (see .weekbar-days), so it no longer needs the 44px
   floor a column of its own needed to look like a card. */
.hud-col .weekday { min-height: 0; padding: 3px 8px 4px; }

/* A stat cell on the instrument's grid.
   The two columns used to be two orphan lists -- each cell a 2px side
   border and a number, 14px of air between them, mirrored on the right --
   and the panel read as a sphere with captions loosely parked either side.
   Now every cell sits on one horizontal rule with a short bright tick at
   the end that faces the dial, on fixed line-heights (14 / 30 / 16) so the
   caption, the figure and the note of every gauge land on the same
   baselines up and down both columns: a ruled plate, not a list. The
   right column mirrors the tick, not the type. */
.gauge {
  display: flex; flex-direction: column; gap: 3px; padding: 10px 0 10px 14px;
  border-top: 1px solid color-mix(in srgb, var(--gold) 18%, transparent);
  position: relative;
}
.gauge::before {
  content: ""; position: absolute; top: -1px; left: 0; width: 14px; height: 1px;
  background: color-mix(in srgb, var(--gold) 70%, transparent);
  transition: width .15s, background .15s;
}
.gauge[data-jump] { cursor: pointer; }
.gauge[data-jump]:hover::before { width: 28px; background: var(--gold); }
.gauge[data-jump]:hover b { color: var(--gold); }
.gauge em {
  font-style: normal; font-size: 10px; line-height: 14px; letter-spacing: .2em;
  text-transform: uppercase; color: var(--muted);
}
.gauge b {
  font-family: var(--mono); font-size: 26px; font-weight: 400; line-height: 30px;
  color: var(--text); letter-spacing: -.02em;
  transition: color .15s;
}
.gauge i { font-style: normal; font-size: 11px; line-height: 16px; color: var(--muted); }
/* The whole tier picker, collapsed to one line by default -- see this
   element's own comment in console.html for why: four permanently-expanded
   <details> tiers plus the smart-routing box was what forced .hud-col into
   an internal scrollbar no amount of trimming the tiers themselves fixed. */
.model-summary summary {
  cursor: pointer; display: flex; align-items: baseline; gap: 8px;
  list-style: none;
}
.model-summary summary::-webkit-details-marker { display: none; }
.model-summary summary::before { content: "▸ "; color: var(--muted); font-size: 11px; }
.model-summary[open] summary::before { content: "▾ "; }
/* "Model" reads as a word in the sentence now, not as a tiny engraved tag.
   It was 9px wide-tracked caps against a 13px monospace value -- a ~45%
   size difference on one baseline, which is what made the line look
   mismatched rather than deliberate. Same size and same family as the value
   it introduces; the muted colour alone is enough to mark it as the label. */
/* Matching the px size was not enough: the value is bold and the label was
   not, and a weight difference reads as a SIZE difference at a glance --
   which is why the line still looked mismatched after they were both 13px.
   Both are 14px and the same weight now, so the two words sit as one phrase;
   colour alone separates label from value, which is all the separation a
   two-word line needs. */
.model-summary summary em {
  font-style: normal; font-family: var(--mono); font-size: 14px;
  font-weight: 600; letter-spacing: 0; text-transform: none;
  color: var(--muted); flex-shrink: 0;
}
.model-summary summary b {
  font-family: var(--mono); font-size: 14px; font-weight: 600; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.model-summary[open] { display: block; }

/* The three model tiers -- each a native <details>, so only the one you
   care about right now takes up room; the other two stay a single readable
   line (name + what's currently in that slot). */
.model-tier {
  margin-top: 8px; padding-top: 8px;
  border-top: 1px solid color-mix(in srgb, var(--gold) 14%, transparent);
}
.model-tier:first-of-type { margin-top: 4px; padding-top: 0; border-top: 0; }
.model-tier summary {
  cursor: pointer; display: flex; align-items: baseline; gap: 6px;
  list-style: none; font-size: 11px; font-weight: 600; color: var(--text-2);
}
.model-tier summary::-webkit-details-marker { display: none; }
.model-tier summary::before { content: "▸ "; color: var(--muted); font-weight: 400; }
.model-tier[open] summary::before { content: "▾ "; }
.model-tier-note {
  font-style: normal; font-family: var(--mono); font-size: 10px;
  color: var(--muted); font-weight: 400;
}
.model-tier-detail {
  margin: 6px 0 0; font-size: 11px; color: var(--muted); line-height: 1.5;
}
.gauge .hud-pick { margin: 2px 0; }
.hud-col.right .gauge .hud-pick { margin-left: auto; }

/* Importing memory from another assistant. One box for paste, files and
   links, because people have it in all three shapes and asking them to
   convert first is the reason it never gets imported at all. */
.import-box {
  margin: 4px 0 14px; padding: 14px;
  border: 1px solid color-mix(in srgb, var(--gold) 26%, transparent);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--gold) 5%, var(--surface));
}
.import-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.import-box textarea {
  width: 100%; background: var(--surface-2); color: var(--text);
  border: 1px solid var(--line); border-radius: 8px;
  padding: 10px 12px; font-family: var(--font); font-size: 13px; resize: vertical;
}
.import-box input[type="file"] { font-size: 12px; color: var(--text-2); flex: 1; }
.import-facts {
  max-height: 300px; overflow-y: auto;
  border: 1px solid var(--line); border-radius: 8px; padding: 6px;
  background: var(--surface-2);
}
.import-fact {
  display: flex; gap: 9px; align-items: flex-start;
  padding: 7px 8px; border-radius: 6px; cursor: pointer; font-size: 12px; line-height: 1.5;
}
.import-fact:hover { background: color-mix(in srgb, var(--gold) 8%, transparent); }
.import-fact input { accent-color: var(--gold); margin-top: 2px; flex-shrink: 0; }

/* Smart Routing sits above the tiers because it decides whether they are a
   choice or a preference. */
.smart-route {
  /* Plain source order now (info, label, switch) instead of row-reverse.
     The reverse trick existed only to keep a checkbox first in the DOM for
     <label> semantics; with a real <button role="switch"> there is no label
     wrapper to satisfy, so the visual order and the source order can simply
     agree. */
  display: flex; align-items: center; gap: 10px;
  margin: 2px 0 8px; padding: 6px 8px 6px 10px;
  border-radius: 8px; border: 1px solid color-mix(in srgb, var(--gold) 22%, transparent);
  background: color-mix(in srgb, var(--gold) 4%, transparent);
}
.smart-route-label {
  /* text-transform: none stays deliberately. The bare `label { text-transform:
     uppercase }` rule is this file's generic default for settings labels and
     was inheriting into this text, which is what produced the all-caps look
     reported as "horrible". This element is no longer inside a <label>, but
     the override costs nothing and documents the hazard for the next person
     who wraps it in one. */
  flex: 1; font-size: 12px; font-weight: 500; color: var(--text-2);
  line-height: 1.3; white-space: nowrap; text-transform: none;
}
/* The switch itself. A <button>, so it is keyboard-operable and clickable
   with no label-forwarding in the picture at all; aria-checked carries the
   state, and the styling reads off that same attribute, so what you see and
   what the code believes cannot disagree. */
.switch {
  appearance: none; -webkit-appearance: none; margin: 0; padding: 0;
  flex-shrink: 0; width: 34px; height: 19px; border-radius: 999px;
  position: relative; cursor: pointer; display: block;
  border: 1px solid var(--gold-mid);
  background: color-mix(in srgb, var(--surface-2) 92%, transparent);
  transition: background .15s ease-out, border-color .15s ease-out;
}
.switch .switch-knob {
  position: absolute; top: 2px; left: 2px;
  width: 13px; height: 13px; border-radius: 50%; background: var(--muted);
  transition: transform .15s ease-out, background .15s ease-out;
}
.switch[aria-checked="true"] {
  background: color-mix(in srgb, var(--gold) 32%, transparent);
  border-color: var(--gold);
}
.switch[aria-checked="true"] .switch-knob {
  transform: translateX(15px); background: var(--gold);
}
/* Visible keyboard focus: a <button> gets none by default once appearance is
   stripped, and this is a real control. */
.switch:focus-visible {
  outline: 2px solid var(--gold); outline-offset: 2px;
}
/* Mid-flight, so a slow save reads as "working" rather than as a dead
   control -- the failure mode this whole element has a history of. */
.switch[data-busy="1"] { opacity: .6; cursor: progress; }
/* ── Autonomy strip ───────────────────────────────────────────────────────
   Always-on-screen answer to "is the engine working right now?". Reads as a
   piece of running equipment: a live indicator, plain-language state, the
   next scheduled pass, and one control. Green-ish (sage) rather than gold
   when running, because the gold is the interface and this is a signal. */
.engine-strip {
  /* inline-flex, no top margin: this is one item inside the existing
     .hud-controls row, not a row of its own. Sized to sit level with the
     chips beside it. */
  display: inline-flex; align-items: center; gap: 8px;
  padding: 5px 6px 5px 11px; border-radius: 999px;
  border: 1px solid color-mix(in srgb, var(--gold) 20%, transparent);
  background: linear-gradient(90deg,
              color-mix(in srgb, var(--gold) 8%, transparent),
              color-mix(in srgb, var(--gold) 2%, transparent));
  font-size: 11.5px;
}
.engine-dot {
  width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
  background: var(--muted);
}
/* A pulsing dot is the one piece of motion here, and it is load-bearing: a
   static dot cannot distinguish "running" from "a screenshot of running". */
.engine-strip.on .engine-dot {
  background: var(--sage);
  box-shadow: 0 0 0 0 color-mix(in srgb, var(--sage) 70%, transparent);
  animation: engine-pulse 2.4s ease-out infinite;
}
@keyframes engine-pulse {
  0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--sage) 55%, transparent); }
  70%  { box-shadow: 0 0 0 7px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}
.engine-strip.on { border-color: color-mix(in srgb, var(--sage) 34%, transparent); }
.engine-strip.held .engine-dot { background: var(--gold-mid); }
.engine-state { font-weight: 600; color: var(--text); letter-spacing: .01em; }
.engine-detail { font-size: 11px; }
.engine-btn {
  flex-shrink: 0; cursor: pointer;
  font: 600 10.5px var(--font); letter-spacing: .04em;
  padding: 3px 10px; border-radius: 999px; color: var(--text-2);
  border: 1px solid color-mix(in srgb, var(--gold) 34%, transparent);
  background: color-mix(in srgb, var(--surface-2) 90%, transparent);
  transition: color .15s, border-color .15s, background .15s;
}
.engine-btn:hover { color: var(--text); border-color: var(--gold); }
.engine-btn:disabled { opacity: .5; cursor: progress; }
@media (prefers-reduced-motion: reduce) {
  .engine-strip.on .engine-dot { animation: none; }
}

/* The exact prompt text, shown verbatim. Monospace and pre-wrapped because
   this is the literal string sent to the model -- reflowing it would make the
   panel a paraphrase of the thing it exists to prove. */
.mono-block {
  font-family: var(--mono); font-size: 11px; line-height: 1.5;
  white-space: pre-wrap; word-break: break-word; margin: 8px 0 0;
  padding: 10px; border-radius: 8px; color: var(--text-2);
  background: color-mix(in srgb, var(--surface-2) 88%, transparent);
  border: 1px solid color-mix(in srgb, var(--gold) 16%, transparent);
  max-height: 220px; overflow: auto;
}

.smart-note {
  margin: -4px 0 10px; padding: 8px 10px; border-radius: 8px;
  background: color-mix(in srgb, var(--gold) 7%, var(--surface-2));
  border: 1px solid color-mix(in srgb, var(--gold) 20%, transparent);
  font-size: 11px; line-height: 1.55; color: var(--text-2);
}
.smart-note em { color: var(--gold); font-style: normal; font-weight: 700; }
/* This note lives inside <div class="gauge">, and `.gauge b` is the 24px
   monospace style for the big readout numbers (Open tasks, Completed). A
   <b> in a sentence here was inheriting it, which is why "Smart Routing is
   off" rendered with a giant "off" mid-sentence. Emphasis in prose should be
   weight, never size. */
.smart-note b {
  font: inherit; font-weight: 700; color: var(--text);
  letter-spacing: normal;
}

/* Image/video providers -- same shape as the free-trial model picker, so the
   two "pick a provider" controls do not look like different mechanisms. */
.media-opt {
  display: flex; align-items: center; gap: 8px;
  padding: 6px 9px; margin: 3px 0; border-radius: 8px;
  border: 1px solid color-mix(in srgb, var(--gold) 14%, transparent);
  background: color-mix(in srgb, var(--surface) 92%, transparent);
  cursor: pointer; font-size: 12px;
}
.media-opt input[type="radio"] { accent-color: var(--gold); margin: 0; }
.media-opt span { flex: 1; font-weight: 600; }
.media-tag { font-family: var(--mono); font-size: 10px; color: var(--muted); font-style: normal; }
.media-opt:has(input:checked) {
  border-color: var(--gold);
  background: color-mix(in srgb, var(--gold) 10%, var(--surface));
}
/* The no-choice-to-make version: ComfyUI is the only image/video provider
   offered now (see tier-media's own comment), so this reads as a fact
   rather than a control -- same look as a checked .media-opt, minus the
   pointer cursor and the radio input that had nothing left to pick between. */
.media-opt.fixed {
  cursor: default;
  border-color: var(--gold);
  background: color-mix(in srgb, var(--gold) 10%, var(--surface));
}

/* The memory tab's spider-web view. Sits beside the list rather than
   replacing it: the list is better for "is this fact still there", the graph
   is better for "what does it know about this project". */
/* The view switch is a primary control here, not a footnote -- the graph is
   half of what this tab is for, and at seg-button size it read as an option
   nobody was meant to use. */
#mem-view-seg { gap: 0; margin-bottom: 12px; }
#mem-view-seg [data-memview] {
  padding: 9px 22px; font-size: 13px; font-weight: 700; letter-spacing: .02em;
}
#mem-view-seg [data-memview].on {
  background: color-mix(in srgb, var(--gold) 18%, var(--surface-2));
  color: var(--gold);
}

.graph-wrap {
  position: relative;
  height: 620px;
  border: 1px solid var(--gold-mid);
  border-radius: var(--radius);
  background:
    radial-gradient(circle at 50% 40%,
      color-mix(in srgb, var(--gold) 6%, transparent), transparent 70%),
    var(--bg);
  box-shadow: inset 0 0 60px rgba(0,0,0,.4), 0 0 24px color-mix(in srgb, var(--gold) 8%, transparent);
  overflow: hidden;
}
.graph-tools { display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin:0 0 8px; }
.graph-tools .btn { width:auto; padding:6px 10px; }
.graph-tools input { min-width:220px; flex:0 1 340px; }
.graph-tools .hint { margin-left:auto; }
#memory-graph { width: 100%; height: 100%; display: block; cursor: grab; touch-action: none; }
#memory-graph:active { cursor: grabbing; }
.memory-sigma { position:absolute; inset:0; display:none; background:rgba(0,0,0,.10); }
.graph-wrap.sigma-ready #memory-graph { opacity:0; pointer-events:none; }
.graph-wrap.sigma-ready .memory-sigma { display:block; }
.graph-empty {
  position: absolute; inset: 0; display: grid; place-items: center;
  color: var(--muted); font-size: 13px; pointer-events: none;
}
.graph-legend {
  display: flex; flex-wrap: wrap; gap: 12px;
  margin-bottom: 8px; font-size: 11px; color: var(--text-2);
}
.graph-key { display: inline-flex; align-items: center; gap: 5px; }
.graph-key i { width: 9px; height: 9px; border-radius: 50%; display: inline-block; }
.graph-key b { color: var(--text); font-family: var(--mono); }
.graph-detail {
  margin-top: 10px; padding: 10px 12px; min-height: 46px;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface); font-size: 12px; line-height: 1.6;
}
.graph-detail-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 4px; }
.graph-kind { font-family: var(--mono); font-size: 10px; text-transform: uppercase; }
.graph-detail ul.plain { margin: 4px 0 0; padding-left: 16px; color: var(--text-2); }

@media (max-width: 700px) { .graph-wrap { height: 320px; } }

/* Free-trial local models: two plain choices, not a model-name catalog --
   "smallest" and "largest" say what the tradeoff actually is (speed/quality
   vs download size and hardware) without requiring the visitor to know what
   a parameter count means. */
.webllm-pick { display: flex; flex-direction: column; gap: 6px; margin: 6px 0; }
.webllm-opt {
  display: flex; align-items: center; gap: 8px;
  padding: 7px 9px; border-radius: 8px;
  border: 1px solid color-mix(in srgb, var(--gold) 16%, transparent);
  background: color-mix(in srgb, var(--surface) 92%, transparent);
  cursor: pointer; font-size: 12px;
}
.webllm-opt.active {
  border-color: var(--gold);
  background: color-mix(in srgb, var(--gold) 10%, var(--surface));
}
.webllm-opt input[type="radio"] { margin: 0; accent-color: var(--gold); }
.webllm-opt .name { font-weight: 600; flex: 1; }
.webllm-opt .size { font-family: var(--mono); font-size: 10px; color: var(--muted); }
.tier-info-btn {
  width: 16px; height: 16px; line-height: 15px; text-align: center;
  border-radius: 50%; border: 1px solid var(--muted); color: var(--muted);
  font-size: 10px; font-weight: 700; cursor: pointer; background: none;
  flex-shrink: 0; padding: 0;
}
.tier-info-btn:hover { border-color: var(--gold); color: var(--gold); }
.hw-check {
  margin-top: 4px; padding: 8px 10px; border-radius: 8px;
  background: color-mix(in srgb, var(--surface-2) 96%, transparent);
  border: 1px solid color-mix(in srgb, var(--gold) 12%, transparent);
  font-size: 11px; line-height: 1.6; color: var(--text-2);
}
.hw-check .verdict { font-weight: 700; }
/* Reusing the theme's existing semantic tones rather than inventing new
   ones: --sage already means "fine", --warn-yellow already means "the
   yellow warning tier", --danger already means "wrong" -- everywhere else
   in this stylesheet. */
.hw-check .verdict.ok { color: var(--sage); }
.hw-check .verdict.tight { color: var(--warn-yellow); }
.hw-check .verdict.no { color: var(--danger); }
.hw-check dl { margin: 4px 0 0; display: grid; grid-template-columns: auto 1fr; gap: 2px 8px; }
.hw-check dt { color: var(--muted); }
.hw-check dd { margin: 0; font-family: var(--mono); }

/* Paid tiers, shown but not usable on a free-trial account -- darkened and
   inert rather than hidden, so a trial visitor can see what exists without
   being able to spend money they never agreed to spend. */
.model-tier.locked summary { color: var(--muted); }
.model-tier.locked { opacity: 0.55; position: relative; }
.model-tier.locked::after { content: ""; position: absolute; inset: 0; cursor: pointer; }
.tier-lock-badge {
  display: inline-flex; align-items: center; gap: 3px;
  font-size: 9px; font-weight: 700; letter-spacing: 0.03em;
  color: var(--gold); border: 1px solid var(--gold); border-radius: 999px;
  padding: 1px 6px; margin-left: auto;
}
.tier-unlock-note {
  margin: 6px 0 0; padding: 8px 10px; border-radius: 8px;
  background: color-mix(in srgb, var(--gold) 8%, var(--surface-2));
  border: 1px solid color-mix(in srgb, var(--gold) 25%, transparent);
  font-size: 11px; line-height: 1.5;
}
.tier-unlock-note a { color: var(--gold); font-weight: 700; text-decoration: underline; }

/* The talk button. It is the largest thing on the page and it did nothing,
   which is the one affordance people reach for first -- restored to its
   full size per explicit instruction (keep the reactor, simplify what's
   below it). */
.reactor {
  /* Scales with the viewport instead of a fixed size. Two prior passes (30->36->44vh)
     were each reasoned about but never actually measured against a live render, and
     each one turned out smaller on screen than intended -- a static file:// snapshot
     doesn't replicate the real app's panel-toggling, so there was no faithful way to
     check. This pass WAS measured: served the real templates/static over a local
     HTTP server (so /static/... paths resolve, unlike file://), force-revealed #app
     past the login gate, and read getBoundingClientRect() on #reactor and .composer
     directly. At 56vh on an 850px-tall window the reactor is ~476px and the composer
     top still sits at ~831px -- on screen, not below the fold. The floor protects a
     short screen no matter what; the ceiling only matters once there is vertical
     room to spare. */
  /* Sized from the smaller of height and WIDTH. 56vh alone overflows a narrow
     screen: measured 2026-08-22 on a 375x812 phone viewport at 50vh, the
     reactor came out 406px wide inside a 375px window and pushed 62px off
     the right edge -- it was the only element in the document doing so. A
     square sized purely by viewport height cannot fit a portrait phone, so
     82vw stays the phone-safe term untouched; on desktop 82vw is still the
     larger of the two at every size checked (1440x1000 -> 56vh=560 vs
     82vw=1180; 1280x800 falls under the short-viewport rule below instead).
     56vh itself is the figure the comment above already measured as safe on
     an 850px-tall window (2026-08-22): reactor ~476px, composer top still
     ~831px, on screen. 2026-09-02: bumped from 50vh to that already-measured
     56vh -- a real request that the reactor read as too small, and the
     number to use was already sitting in this file's own history. */
  width: clamp(200px, min(64vh, 86vw), 860px);
  height: clamp(200px, min(64vh, 86vw), 860px);
  flex: 0 0 auto;
  background: none; border: 0; padding: 0; font: inherit; color: var(--gold);
  cursor: pointer; -webkit-tap-highlight-color: transparent;
  position: relative; z-index: 3;
  /* Air above the instrument. "The top of the engine should be moved,
     shrinked, or re adjusted" -- measured at 1440x900 the dial's top edge
     sat 29px under the week strip, and the outermost thing it draws (the
     holographic ellipse at ry=50, plus the top cardinal tick reaching to
     y=4 of 300) runs right at that edge, so the ring system was reading as
     clipped by the panel above it. This is margin on the DIAL rather than
     padding on the HUD: .hud's own padding is 18px for a measured reason
     (see the note on that rule -- 34px there put the composer below the
     fold on the 1000px fixture), and the gauge columns beside the dial do
     not need the air, only the instrument does. */
  margin-top: 22px;
}
/* Short laptops -- 1280x800 is the canonical one. The composer sat at 870px
   against an 800px viewport: 70px below the fold, pinned as an exact number
   across two passes before anyone fixed it. On short VIEWPORTS the reactor
   cedes the vertical room rather than the composer. This is a HEIGHT query:
   the phone rules are width-based and untouched, and tall desktops are
   untouched because min(50vh, 82vw) already picks 82vw there. */
@media (min-width: 721px) and (max-height: 840px) {
  .reactor {
    width: clamp(200px, 37vh, 860px);
    height: clamp(200px, 37vh, 860px);
    /* Barely any air here: the whole point of this query is that the dial
       cedes vertical room, and every pixel it takes back lands on the
       composer. Measured at 1280x800 -- test_console_responsive pins the
       composer's top at 711 +/- 8, and 10px here put it at 721. Six is what
       the fold can afford, and at a 266px dial it is proportionally close
       to the 22px a 576px one gets. */
    margin-top: 6px;
  }
  /* The words scale with the dial (see .reactor-text), so the shrunken dial
     has to tell them. */
  .reactor-text { --reactor-side: clamp(200px, 37vh, 860px); }
  /* And the HUD cedes its own padding: measured 2026-08-24, the reactor
     shrink alone left the composer at 807 -- the frame around it was the
     rest of the distance. 18px margin + 16px padding -> 6px + 10px lifts
     the composer to ~783 against the 800px fold. */
  .hud {
    margin-bottom: 6px;
    padding: 10px 18px;
  }
  /* The columns beside the reactor used to be capped to its height with an
     internal scrollbar, because on a short laptop they were the real ceiling
     -- the right one measured 387px, so once the reactor dropped below that,
     IT set the HUD height and the composer stayed buried under it. That cap
     is gone (see .hud-col): the command bar sits ABOVE the instrument now, so
     nothing the work columns do can bury it, and the design spec forbids
     internal scrollbars kept "to preserve the illustrated fold". The reactor's
     own cede to 37vh above is what this query is still for. */
}
.reactor:focus-visible {
  outline: none;
  filter: drop-shadow(0 0 14px color-mix(in srgb, var(--gold) 70%, transparent));
}
.reactor:hover .globe-tint { fill: color-mix(in srgb, var(--gold) 12%, transparent); }
.reactor:active { transform: scale(.985); }
/* The specialist that answered, shown in the core. Swaps with the status block
   rather than crowding beside it — the middle of a reactor fits one thing. */
.reactor-agent {
  position: absolute; inset: 0; z-index: 2;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 8px; pointer-events: none;
  animation: agentIn .45s ease-out;
}
.reactor-agent[hidden] { display: none; }
.reactor-agent img {
  width: 52px; height: 52px; border-radius: 50%; object-fit: cover;
  border: 1px solid var(--gold);
  box-shadow: 0 0 22px -2px color-mix(in srgb, var(--gold) 70%, transparent);
}
#reactor-agent-name {
  font-family: var(--mono); font-size: 12px; letter-spacing: .18em;
  text-transform: uppercase; color: var(--gold);
  text-shadow: 0 0 16px color-mix(in srgb, var(--gold) 60%, transparent),
               0 1px 5px var(--bg);
}
@keyframes agentIn {
  from { opacity: 0; transform: scale(.86); }
  to   { opacity: 1; transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) { .reactor-agent { animation: none; } }

/* Only offered when the browser can actually hear; shown by JS. */
.reactor-hint {
  display: none; letter-spacing: .16em; text-transform: uppercase;
  font-size: clamp(10px, calc(var(--reactor-side) * .021), 14px);
  color: var(--text); opacity: .92; margin-top: 9px;
  /* The gold wash alone put nothing dark behind these letters, and they sit
     lower on the ball than the state word -- over the lit map rather than
     the dark centre. The block's halo plus a gold lift, not instead of it. */
  text-shadow: 0 0 9px rgba(12, 9, 5, .95), 0 0 20px rgba(12, 9, 5, .8),
               0 0 12px color-mix(in srgb, var(--gold) 30%, transparent);
}
.reactor-fix {
  display: block; max-width: 190px; margin: 8px auto 0;
  font-size: 10px; line-height: 1.5; color: var(--text-2);
  text-transform: none; letter-spacing: 0; text-align: center;
}
.reactor.can-talk .reactor-hint { display: block; }
.reactor.listening .reactor-hint { color: var(--gold); }
.reactor.listening .globe-tint { fill: color-mix(in srgb, var(--gold) 18%, transparent); }
.reactor-text u {
  text-decoration: none; font-family: var(--mono);
  font-size: clamp(9px, calc(var(--reactor-side) * .018), 12px);
  letter-spacing: .1em; color: var(--gold); margin-top: 3px;
  text-shadow: 0 0 8px rgba(12, 9, 5, .95), 0 0 18px rgba(12, 9, 5, .8);
}

/* Telemetry strip: a running row of state along the bottom of the panel. */
.hud-strip {
  display: flex; flex-wrap: wrap; justify-content: center; gap: 22px;
  margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--line);
}
/* Subheads inside the dashboard's own sections (Specialists, Quick actions). */
.dash-subhead {
  margin: 24px 0 0; padding-top: 16px; border-top: 1px solid var(--line);
  font-size: 10px; letter-spacing: .16em; text-transform: uppercase;
  color: var(--muted); font-weight: 600; text-align: center;
}
.dash-subhead:first-of-type { padding-top: 0; border-top: 0; }
.hud-strip span { display: flex; align-items: baseline; gap: 8px; }
.hud-strip em {
  font-style: normal; font-size: 9px; letter-spacing: .18em; color: var(--muted);
}
.hud-strip b {
  font-family: var(--mono); font-size: 12px; font-weight: 400; color: var(--text-2);
}
.hud-strip span.paid b { color: var(--gold); }

/* The three hoists that used to live here -- the Eye out of the middle grid
   track at 1100px, back into place at 720px, and the whole-panel flex
   reorder that put the conversation above it on a phone -- are all gone,
   because the thing they were reordering is not in the work grid any more.
   The Eye leads the page from #eye-stage in SOURCE order at every width
   (2026-09-07, operator instruction: "the HUD is also below the text box
   when it should be above it"), so there is nothing left for a media query
   to move: the markup and the screen agree, which is also what a screen
   reader gets. What follows is the rest of the phone pass. */

/* Phone: one column, and the panel is still a flex column so the bands stack
   predictably -- but with no `order` on any child, so the order on a phone is
   the order in the file. Every child carrying an explicit order was the old
   arrangement's requirement (an unset order defaults to 0 and silently hoists
   everything not listed); with the reorder gone, so is the requirement. */
@media (max-width: 720px) {
  [data-panel="console"] { display: flex; flex-direction: column; }

  .hud-grid { grid-template-columns: 1fr; }
  /* Stacked, the columns hold cards rather than gauges, so each band takes
     the whole width: two 48% cards abreast at 375px is two truncated cards.
     The flex-row-with-wrap shape stays because the metrics band below still
     depends on it, and because a single-column flex row and a flex column
     lay out identically once every item is 100% wide. */
  .hud-col {
    flex-direction: row; flex-wrap: wrap; gap: 8px;
    justify-content: center; align-items: stretch;
  }
  .hud-col.right { align-items: stretch; }
  .hud-col > * { flex: 1 1 100%; min-width: 0; }
  /* The metrics band was two tall towers of gauges beside the reactor until
     2026-09-06 and is one strip above it now. Measured 2026-08-22 on
     375x812, when it was still towers: #hud was 1177px tall, of which the
     reactor was only 308. Wrapping is what keeps that from coming back -- a
     figure stays present and fully readable, they just flow across rather
     than down -- and two per row is the floor at which the caption, the
     number and its note all still fit on one line each. */
  .hud-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 10px; }
  .hud-metrics .gauge { padding: 8px 8px 8px 12px; text-align: left; }
  .hud-machine .gauge { flex: 1 1 100%; }
  /* A gauge can be as narrow as 96px here, and the Smart Routing row's
     label carries white-space: nowrap (right for a wide laptop column,
     fatal in 97px) -- measured 2026-08-24: the row overran a 375px screen
     by 63px, the only element doing so. Wrap the row and let the label
     break; the laptop rule above is untouched. */
  .smart-route { flex-wrap: wrap; }
  .smart-route-label { white-space: normal; min-width: 0; }
}

/* ---------- autonomy panels ---------- */
/* Small variants of the existing controls. Deliberately reusing .btn and .pill
   rather than inventing a second button system: the HUD's look is settled, and
   these panels should read as part of it, not bolted on. */
button.btn.sm {
  width: auto; padding: 4px 12px; font-size: 12px; letter-spacing: .04em;
}

.pill.gold { background: var(--gold-dim); border-color: var(--gold-mid); color: var(--gold); }

/* A count on a rail button. Only rendered when non-zero — a permanent "0" is
   noise, and the point of a badge is that its presence means something. */
.rail-nav button b.badge {
  background: var(--gold-dim); border: 1px solid var(--gold-mid); color: var(--gold);
  border-radius: 999px; padding: 0 7px; min-width: 18px; text-align: center;
}

.hint {
  color: var(--text-2); font-size: 13px; line-height: 1.55; margin: -4px 0 14px;
  max-width: 62ch;
}
.hint b { color: var(--text); font-weight: 600; }
/* Under the composer's secondary row, not under a form heading. */
#voice-note { margin: 6px 0 0; }

[data-panel="work"] #work-detail {
  margin-top: 14px; font-family: var(--mono); font-size: 12px; line-height: 1.6;
}
[data-panel="work"] #work-counts { gap: 8px; flex-wrap: wrap; }

/* --- project switcher ------------------------------------------------------
   Chips rather than a dropdown: the whole point is that switching between the
   two or three live projects is one click, not click-then-scan-then-click. */
/* The switcher hangs off the bottom of the dashboard panel, which put three
   unrelated lines — the last tile, the chips, and "Working in …" — within a few
   pixels of each other with nothing to say they were different things. A rule
   and a caption separate them, and the caption is what the chips do rather than
   decoration: "Project" above a row of project names. */
/* One compact context row between the reactor and the conversation (the
   critique: "a compact project context switcher above the conversation,
   not a detached control following the HUD"): the caption inline before
   the chips, no rule above it, and the scope note on the same row when it
   fits. Was a captioned block with its own rule and 18px+16px of spacing,
   61px measured; every one of those pixels came off the composer's fold. */
.hud-projects {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
  margin-top: 8px; padding-top: 0; border-top: 0;
}
.hud-projects::before {
  content: "Project"; flex: 0 0 auto; margin-right: 4px;
  font-size: 10px; letter-spacing: .1em; text-transform: uppercase;
  color: var(--muted);
}
.hud-projects .chip { padding: 4px 10px; font-size: 11.5px; }
/* Its own spacing rather than .hint's, whose negative top margin exists to pull
   a caption up under a heading and here pulled this one into the chips. */
#hud-scope { margin: 4px 0 0; font-size: 12px; }
#hud-scope:empty { display: none; }
.chip {
  font: inherit; font-size: 12px; line-height: 1.25;
  padding: 7px 13px; border-radius: 999px; cursor: pointer;
  border: 1px solid var(--line); background: none; color: var(--text-2);
}
.chip:hover { border-color: var(--gold); color: var(--text); }
.chip.on {
  border-color: var(--gold); background: var(--gold-dim);
  color: var(--text); font-weight: 600;
}
/* Focus is drawn explicitly: these are keyboard-reachable controls and the
   default outline is hard to see against the filled state. */
.chip:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }
.btn.ghost.star { padding: 4px 9px; color: var(--muted); }
.btn.ghost.star.on, .btn.ghost.star:hover { color: var(--gold); }

/* --- pasted images ---------------------------------------------------------
   Held in the composer as thumbnails until send, each removable: attaching the
   wrong screenshot should not mean retyping the message. */
.attachments { display: none; gap: 8px; flex-wrap: wrap; padding: 8px 14px 0; }
.attachments.has { display: flex; }
.attachment { position: relative; display: inline-flex; }
.attachment img {
  height: 62px; width: auto; max-width: 140px; object-fit: cover;
  border: 1px solid var(--line); border-radius: 8px; display: block;
}
.attachment-x {
  position: absolute; top: -6px; right: -6px;
  width: 20px; height: 20px; line-height: 1; padding: 0;
  border-radius: 50%; border: 1px solid var(--line);
  background: var(--surface); color: var(--text-2); cursor: pointer;
  font-size: 14px;
}
.attachment-x:hover { border-color: var(--danger); color: var(--danger); }
.composer.dropping { border-color: var(--gold); background: var(--gold-dim); }
/* In the transcript, a sent image sits above its text. */
.msg .shot {
  display: block; max-width: min(420px, 100%); height: auto;
  border: 1px solid var(--line); border-radius: 8px; margin-bottom: 8px;
}

/* --- a paused turn ---------------------------------------------------------
   Shown while paused so nobody has to remember what the turn is holding. */
.pending { padding: 8px 14px 0; font-size: 12px; }
.pending-label { display: block; color: var(--muted); margin-bottom: 6px; }
.pending-part {
  display: block; padding: 4px 9px; margin-bottom: 4px;
  border-left: 2px solid var(--gold); color: var(--text-2);
  background: var(--gold-dim); border-radius: 0 6px 6px 0;
}

/* --- voice dial and control state ------------------------------------------ */
.hud-controls {
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px;
  margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--line);
}
.dial { display: inline-flex; align-items: center; gap: 8px; font-size: 12px; }
.dial-label { color: var(--muted); }
.dial output { color: var(--text-2); min-width: 34px; font-variant-numeric: tabular-nums; }
/* State is never carried by colour alone — each chip also spells it out. */
.chip b { font-weight: 600; margin-left: 5px; }
.ctl { display: inline-flex; gap: 3px; align-items: center; }
.chip.tiny { padding: 6px 9px; font-weight: 600; }
/* --- business dashboard ---------------------------------------------------
   Tiles read as instrument cells, like the HUD gauges above them, because they
   are the same kind of thing: a number you glance at rather than read. The
   state colours are deliberately not decorative — `off` is the muted one, so a
   page full of unconnected sources looks unfinished rather than looking calm
   and reporting nothing. */
.dash-grid {
  display: grid; gap: 12px;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
}
/* Seven pipeline stages in one row: the 190px default left "Closed" alone on a
   second row at 1440px. */
#crm-pipeline { grid-template-columns: repeat(auto-fit, minmax(min(100%, 128px), 1fr)); }
.dash-tile {
  position: relative; padding: 11px 13px; border-radius: 8px;
  border: 1px solid var(--line); border-left: 2px solid var(--gold);
}
.dash-tile em {
  display: block; font-style: normal; font-size: 10px; letter-spacing: .14em;
  text-transform: uppercase; opacity: .62;
}
.dash-tile b {
  display: block; font-family: var(--mono); font-weight: 400; font-size: 24px;
  letter-spacing: -.01em; line-height: 1.25;
}
.dash-tile i { display: block; font-style: normal; font-size: 11px; opacity: .58; }

/* Needs attention: still a real number, just one worth looking at. */
.dash-tile.warn { border-left-color: var(--warn-fg); background: var(--warn-bg); }
.dash-tile.warn b { color: var(--warn-fg); }
/* A tile that needs attention says so in a word as well as in a colour. */
.dash-tile.warn em::after { content: " · attention"; opacity: .8; }

/* Configured and broken. Louder than `off`, because somebody thought this
   worked and it does not. */
.dash-tile.error { border-left-color: var(--danger-fg); background: var(--danger-bg); }
.dash-tile.error b { color: var(--danger-fg); }
.dash-tile.error em::after { content: " · broken"; opacity: .8; }

/* Never connected. Dimmed, and it always carries the fix in .hint. */
.dash-tile.off { opacity: .72; border-left-color: var(--line); }
.dash-tile.off b { font-size: 18px; opacity: .5; }
/* Clamped to two lines with the full sentence still on the tile's own title
   tooltip: at the narrow end of the grid, "Stripe is not linked. Add your
   key in Settings -> Connectors to see revenue." ran to four or five lines
   and turned a row of instrument tiles into a wall of paragraph text. */
.dash-tile .hint {
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow: hidden; font-size: 11px; line-height: 1.45; opacity: .66; margin-top: 4px;
}

.dash-editor { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--line); }
.dash-choices { display: flex; flex-direction: column; gap: 4px; }
.dash-choice {
  display: flex; align-items: center; gap: 8px; padding: 5px 8px; border-radius: 2px;
  cursor: grab; background: color-mix(in srgb, var(--text) 4%, transparent);
}
.dash-choice.dragging { opacity: .45; }
.dash-choice span { flex: 1; font-size: 13px; }
.dash-choice .grip { opacity: .4; font-size: 12px; cursor: grab; }
/* Keyboard and touch reorder: the same move a drag makes, one row at a time. */
.dash-choice .dash-move {
  min-width: 32px; min-height: 32px; padding: 0 6px; cursor: pointer;
  background: transparent; color: inherit; border: 1px solid var(--line); border-radius: 2px;
}
.dash-choice .dash-move:focus-visible { outline: 2px solid var(--gold); outline-offset: 1px; }

/* A tile whose state this build does not recognise: never drawn as healthy. */
.dash-tile.unknown { border-left-color: var(--line); border-left-style: dashed; }
.dash-tile.unknown em::after { content: " · unknown"; opacity: .8; }

/* How old the readings are, and when a refresh failed, that they are old. */
.data-freshness { font-size: 11px; opacity: .7; margin: 4px 0; }
.data-freshness.stale { opacity: 1; color: var(--warn-fg); }
.data-freshness .freshness-retry {
  margin-left: 6px; min-height: 24px; padding: 0 8px; cursor: pointer;
  background: transparent; color: inherit; border: 1px solid currentColor; border-radius: 2px;
}
/* An approvals count that could not be read in full ("?" or "3+"). */
.rail-nav button b.badge.unknown { border-style: dashed; }

/* --- send / stop -----------------------------------------------------------
   One control. An arrow to send, a filled square while it answers — the shape
   everyone already reads as "stop" — and an arrow again once paused, so the
   next press continues the same turn. */
.btn.send {
  width: 40px; height: 40px; padding: 0; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 15px; line-height: 1; transition: background .15s, color .15s;
}
.btn.send .send-icon { display: block; transform: translateX(1px); }
.btn.send.stopping .send-icon { transform: none; font-size: 12px; }
.btn.send.stopping {
  background: var(--danger); border-color: var(--danger); color: var(--bg);
}
.btn.send.stopping::after {
  /* A ring that keeps turning, so "still working" is legible at a glance
     rather than something you infer from the button not changing. */
  content: ""; position: absolute; width: 40px; height: 40px;
  border-radius: 50%; border: 1.5px solid transparent;
  border-top-color: var(--gold); animation: send-spin 900ms linear infinite;
}
@keyframes send-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .btn.send.stopping::after { animation: none; } }

/* --- the voice dial --------------------------------------------------------
   An outlined track, not a filled bar -- the same hairline-border language
   as .pill/.gauge, so this reads as an instrument control rather than a
   mobile-app volume slider. The fill is a thin inset line, not a thick block:
   thickness is exactly what read as "cartoon" before. Firefox is styled
   explicitly on its own track/thumb pseudo-elements (::-moz-range-track):
   without that the browser's native thick track shows through, however thin
   the WebKit-only rule above it is. */
.dial input[type="range"] {
  /* Declared here as well as set from JS, so the track is filled correctly on
     first paint rather than after the first poll. */
  --fill: 80%;
  -webkit-appearance: none; -moz-appearance: none; appearance: none;
  width: 116px; height: 14px; outline: none; background: transparent;
  cursor: pointer;
}
.dial input[type="range"]::-webkit-slider-runnable-track {
  height: 3px; border-radius: 999px;
  background: linear-gradient(
    to right, var(--gold) 0 var(--fill, 80%),
    color-mix(in srgb, var(--line) 70%, transparent) var(--fill, 80%) 100%);
  border: 1px solid color-mix(in srgb, var(--gold) 20%, transparent);
}
.dial input[type="range"]::-moz-range-track {
  height: 3px; border-radius: 999px;
  background: color-mix(in srgb, var(--line) 70%, transparent);
  border: 1px solid color-mix(in srgb, var(--gold) 20%, transparent);
}
.dial input[type="range"]::-moz-range-progress {
  height: 3px; border-radius: 999px 0 0 999px; background: var(--gold);
}
.dial input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 11px; height: 11px; border-radius: 50%; margin-top: -4.5px;
  background: var(--bg); border: 1.5px solid var(--gold); cursor: pointer;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--gold) 14%, transparent);
}
.dial input[type="range"]::-moz-range-thumb {
  width: 11px; height: 11px; border-radius: 50%;
  background: var(--bg); border: 1.5px solid var(--gold); cursor: pointer;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--gold) 14%, transparent);
}
.dial input[type="range"]:focus-visible { outline: 2px solid var(--gold); outline-offset: 3px; }

/* --- the agent that is answering -------------------------------------------
   A plain highlighted border on the active specialist, plus a spinning ring
   while it is actually thinking (not decoration -- it stops the moment the
   turn does) and the word "active" so colour is never the only signal. */
.agent { position: relative; }
.agent.live.thinking::after {
  content: ""; position: absolute; inset: 0; border-radius: inherit;
  border: 1px solid transparent; border-top-color: var(--gold);
  animation: holo-ring 1.1s linear infinite; pointer-events: none;
}
@keyframes holo-ring { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .agent.live.thinking::after { animation: none; }
}
/* Never colour alone: the active card also says so. */
.agent.live .agent-name::after { content: " · active"; color: var(--gold); font-size: 10px; }

/* ---------- the allow picker ----------
   A dialog rather than a prompt box, because choosing what browser and desktop
   control may touch is the single most consequential thing in the console and
   it deserves the room to show a hostname and a warning next to each name.
   Laid out as a list of buttons, so keyboard and screen-reader users get the
   same affordance as the mouse. */
.picker[hidden] { display: none; }
.picker {
  position: fixed; inset: 0; z-index: 60;
  display: flex; align-items: center; justify-content: center; padding: 24px;
}
.picker-back {
  position: absolute; inset: 0;
  background: rgba(8, 6, 4, .66); backdrop-filter: blur(3px);
}
.picker-box {
  position: relative; width: min(680px, 100%); max-height: min(78vh, 760px);
  display: flex; flex-direction: column; gap: 10px;
  padding: 18px; border-radius: var(--radius);
  background: var(--surface); border: 1px solid var(--line);
  box-shadow: 0 40px 90px -40px var(--ink), 0 0 0 1px var(--gold-dim);
}
.picker-head { display: flex; align-items: center; gap: 12px; }
.picker-head h2 { margin: 0; font-size: 17px; flex: 1; }
.picker-x { width: auto; padding: 2px 10px; font-size: 20px; line-height: 1; }
.picker-search {
  width: 100%; padding: 10px 12px; border-radius: 8px;
  background: var(--surface-2); border: 1px solid var(--line);
  color: var(--text); font: inherit;
}
.picker-search:focus { outline: none; border-color: var(--gold); }
.picker-list { overflow-y: auto; margin: 0 -4px; padding: 0 4px; }
.pick-group {
  margin: 14px 0 6px; font-size: 11px; letter-spacing: .09em;
  text-transform: uppercase; color: var(--muted);
}
.pick-group:first-child { margin-top: 4px; }

/* One row per thing: name, what it actually matches, and — when it matters —
   why it is worth a second thought. The hostname is shown because "Gmail" and
   "mail.google.com" are the same decision only if you can see both. */
.pick {
  display: grid; width: 100%; text-align: left; gap: 2px 10px;
  grid-template-columns: 1fr auto auto;
  padding: 9px 12px; margin-bottom: 4px; border-radius: 8px;
  background: var(--surface-2); border: 1px solid var(--line);
  color: var(--text); font: inherit; cursor: pointer;
}
.pick:hover:not(:disabled) { border-color: var(--gold); background: var(--raised); }
.pick:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }
.pick-label { grid-column: 1; font-weight: 600; }
.pick-host {
  grid-column: 1; grid-row: 2; font-family: var(--mono);
  font-size: 11px; color: var(--muted);
}
.pick-care {
  grid-column: 2; grid-row: 1; align-self: center;
  font-size: 10px; letter-spacing: .06em; text-transform: uppercase;
  padding: 1px 7px; border-radius: 999px;
  color: var(--danger-fg); border: 1px solid var(--danger-line);
  background: var(--danger-bg);
}
.pick-have {
  grid-column: 3; grid-row: 1; align-self: center;
  font-size: 10px; letter-spacing: .06em; text-transform: uppercase;
  padding: 1px 7px; border-radius: 999px;
  color: var(--ok-fg); border: 1px solid var(--ok-line);
  background: var(--ok-bg);
}
/* The warning sits under the row it belongs to, not in a tooltip somebody has
   to hover to find. */
.pick-note { grid-column: 1 / -1; grid-row: 3; font-size: 12px; color: var(--text-2); }
/* Already allowed: still listed, visibly done. Filtering it out would look
   like the entry had vanished. */
.pick:disabled { cursor: default; opacity: .62; }

/* ---------- what it is doing right now ----------
   Sits under the composer while a turn runs. Deliberately quiet: this is
   reassurance, not a headline, and it disappears the moment the reply lands. */
.working {
  margin: 8px 0 0; padding: 9px 11px; border-radius: 10px;
  background: color-mix(in srgb, var(--surface-2) 80%, transparent);
  border: 1px solid var(--line);
}
.working[hidden] { display: none; }
.working-head { display: flex; align-items: baseline; gap: 8px; font-size: 12px; }
.working-head b { color: var(--text); font-weight: 600; }
.working-note { color: var(--muted); margin-left: auto; font-variant-numeric: tabular-nums; }
.working-dot {
  width: 7px; height: 7px; border-radius: 50%; background: var(--gold);
  animation: pulse 1.5s ease-in-out infinite; flex: 0 0 auto;
}
.working-bar {
  height: 3px; margin-top: 8px; border-radius: 999px; overflow: hidden;
  background: color-mix(in srgb, var(--line) 70%, transparent);
}
.working-bar i {
  display: block; height: 100%; width: 0; border-radius: 999px;
  background: var(--wash-meter), var(--gold);
  transition: width .4s linear;
}
/* Past the usual time the bar stops pretending to know and says so in colour
   rather than creeping toward an edge it has already gone past. */
.working.over .working-bar i { background: var(--danger-fg); width: 100% !important; }
.working.over .working-dot { background: var(--danger-fg); }
/* Over the usual time is a state, not a shade: the head says so in words. */
.working.over .working-note::after { content: " · longer than usual"; }
.working-tools {
  margin-top: 7px; font-size: 11px; color: var(--muted);
  font-family: var(--mono); word-break: break-word;
}
.working-tools:empty { display: none; }
/* The cursor at the end of the reply while it is still arriving. Removed when
   the finished text replaces it, so a stalled turn never leaves one blinking
   forever next to an answer that is not coming. */
.caret {
  display: inline-block; width: 7px; height: 1em; margin-left: 2px;
  vertical-align: text-bottom; background: var(--gold); opacity: .8;
  animation: pulse 1.1s steps(2, start) infinite;
}
@media (prefers-reduced-motion: reduce) { .caret { animation: none; } }
@media (prefers-reduced-motion: reduce) {
  .working-dot { animation: none; }
  .working-bar i { transition: none; }
}

/* ---------- the pieces browser control needs ----------
   Same shape as a connector card, because it is the same job: something is
   either set up or it is not, and the row says which and what to do about it. */
.part {
  padding: 12px 14px; margin-bottom: 8px; border-radius: var(--radius);
  background: var(--surface-2); border: 1px solid var(--line);
}
.part.ok { border-color: color-mix(in srgb, var(--ok-fg) 45%, var(--line)); }
.part-head { display: flex; align-items: baseline; gap: 10px; }
.part-name { font-weight: 600; flex: 1; }
.part-state { font-size: 11px; color: var(--muted); white-space: nowrap; }
.part.ok .part-state { color: var(--ok-fg); }
/* Ready is a word first; the border colour only agrees with it. */
.part.ok .part-state::before { content: "✓ "; }
.part-why { font-size: 13px; color: var(--text-2); margin-top: 3px; }
.part-note, .part-manual { font-size: 12px; color: var(--muted); margin-top: 4px; }
.part .btn { margin-top: 10px; }
.part-said { display: block; font-size: 12px; color: var(--gold); margin-top: 6px; }
.parts-log {
  margin: 12px 0 0; padding: 10px 12px; border-radius: 8px; max-height: 260px;
  overflow: auto; background: var(--surface-2); border: 1px solid var(--line);
  font-family: var(--mono); font-size: 11px; line-height: 1.5;
  white-space: pre-wrap; color: var(--text-2);
}
.muted.good { color: var(--sage); }
.muted.bad { color: var(--danger); }

/* ---------- leaving with something typed ----------
   Narrower than the picker: it asks one question and the answer is two
   buttons. "Return" is first and focused, because the safe answer should be
   the one you get by pressing Enter without reading. */
.leave-box { width: min(440px, 100%); gap: 6px; }
.leave-box h2 { margin: 0; font-size: 16px; }
.leave-box p { margin: 0; font-size: 13px; }
.leave-list {
  margin: 6px 0 2px; padding-left: 18px;
  font-size: 12px; color: var(--text-2);
}
.leave-actions { display: flex; gap: 8px; margin-top: 10px; }
.leave-actions .btn { width: auto; flex: 0 1 auto; }

/* ---------- control consent ----------
   Reuses the leave-box shape: one question, room for the "what this does /
   does not do" text, and a checkbox that has to be ticked before the
   affirmative button even becomes clickable — engaging with the text is the
   bar, not a captcha-like flow. */
#control-consent-body { white-space: pre-line; line-height: 1.5; }
.control-consent-check {
  display: flex; align-items: center; gap: 8px; margin-top: 8px;
  font-size: 13px; color: var(--text-2); cursor: pointer;
}
.control-consent-check input[type="checkbox"] {
  width: 15px; height: 15px; accent-color: var(--gold); cursor: pointer;
}
/* .btn:disabled already covers the dimmed, unclickable look — see the base
   button rule above. */
/* Said out loud on the connectors list while a redraw is being held back, so
   "the list looks stale" has an answer on screen rather than in a changelog. */
.conn-held { color: var(--gold); border-color: var(--gold-dim); }

/* ---------- footer ----------
   Present and reachable, not prominent: small text, muted colour, out of the
   way of the actual console. The way most SaaS apps place a ToS/Privacy link
   — worth being able to find, not worth a whole section. */
.app-foot {
  display: flex; gap: 14px; justify-content: center;
  padding: 10px 16px 16px; font-size: 11px;
}
.app-foot a {
  color: var(--muted); text-decoration: none;
}
.app-foot a:hover { color: var(--text-2); text-decoration: underline; }

/* ---------- today ----------
   The day, in the HUD. Read at a glance from across the room, so the headline
   is large and everything else is a quiet list underneath it. */
/* GOALS TAB — a two-column layout, goals on the left (the working surface,
   wants the width), the calendar as a genuine vertical box on the right
   rather than a strip squeezed above the composer. This replaced an earlier
   version that lived in the console HUD: full-width there it pushed the
   composer below the fold, so it got narrowed, then hidden on short screens
   as a stopgap. Moving it here (its actual request -- "under Goals") removes
   the tradeoff entirely: it no longer competes with the composer for space
   at all, so nothing needs to shrink or disappear. */
.goals-layout {
  display: grid; grid-template-columns: 1fr 300px; gap: 18px; align-items: start;
}
@media (max-width: 860px) { .goals-layout { grid-template-columns: 1fr; } }
/* A grid item defaults to a min-width set by its content, and the goals
   table is wide enough to blow the track out and push the calendar off the
   right edge without this. */
.goals-main { min-width: 0; }

.today {
  padding: 14px; min-height: 340px;
}
.today-head { display: flex; align-items: baseline; gap: 10px; }
.today-head em {
  font-style: normal; font-size: 11px; letter-spacing: .09em;
  text-transform: uppercase; color: var(--muted);
}
.today-head b {
  flex: 1; font-size: 14px; color: var(--gold);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* No longer capped short: this box has its own tab now instead of sitting
   between the HUD and the composer, so there is nothing left for its height
   to cost. */
.today-body { margin-top: 10px; max-height: 60vh; overflow-y: auto; }

/* A timeline, not a table. The day runs down a rail with a marker per entry —
   which is what a calendar looks like everywhere else, and costs one pseudo-
   element rather than a rewrite of the markup. The rail is drawn on the body so
   it is continuous between rows instead of restarting at every border. */
.today-body { position: relative; }
.today-body::before {
  content: ""; position: absolute; left: 58px; top: 10px; bottom: 10px; width: 1px;
  background: linear-gradient(
    180deg,
    transparent,
    color-mix(in srgb, var(--gold) 30%, transparent) 12%,
    color-mix(in srgb, var(--gold) 30%, transparent) 88%,
    transparent);
}
.ev {
  position: relative;
  display: grid; grid-template-columns: 54px 1fr auto; gap: 8px;
  align-items: baseline; padding: 6px 0; font-size: 13px;
  border-top: 1px solid color-mix(in srgb, var(--line) 45%, transparent);
}
.ev:first-child { border-top: 0; }
/* The marker sits on the rail. Hollow by default, filled for the thing
   happening now — the same convention as every timetable ever printed. */
.ev::before {
  content: ""; position: absolute; left: 55px; top: 12px;
  width: 7px; height: 7px; border-radius: 50%;
  border: 1px solid color-mix(in srgb, var(--gold) 45%, transparent);
  background: var(--surface-2);
}
.ev b { font-family: var(--mono); font-size: 12px; color: var(--text-2); font-weight: 500; }
.ev span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding-left: 10px; }
.ev i { font-style: normal; font-size: 11px; color: var(--muted); white-space: nowrap; }
/* An import row waiting on a person's choice. The global label and input
   rules stack each option and stretch the radio, which clipped both labels
   off the edge of the row; here they sit inline and stay readable. */
.crm-dup i { display: inline-flex; gap: 16px; }
.crm-dup label {
  display: inline-flex; align-items: center; gap: 6px; margin: 0;
  font-size: 12px; color: var(--text-2); cursor: pointer; min-height: 32px;
}
.crm-dup input[type="radio"] {
  width: auto; height: auto; padding: 0; margin: 0; accent-color: var(--gold);
}
@media (max-width: 720px) {
  .crm-dup { grid-template-columns: 54px 1fr; }
  .crm-dup i { grid-column: 2; padding-left: 10px; }
  .crm-dup label { min-height: 44px; }
}

/* Happening now. Colour *and* a mark and a band, so it is never colour alone. */
.ev.now { background: color-mix(in srgb, var(--gold) 7%, transparent); border-radius: 6px; }
.ev.now b { color: var(--gold); }
.ev.now b::after { content: " ●"; }
.ev.now span { color: var(--text); font-weight: 600; }
.ev.now::before {
  background: var(--gold); border-color: var(--gold);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--gold) 22%, transparent);
}
.ev.now i { color: var(--gold); }

/* What is due is not on the clock, so it is not on the rail: a square marker,
   off to the side, saying "this is a different kind of thing". Reading a task
   as a 3pm meeting is the one confusion this panel must not create. */
.ev.due b { color: var(--sage); }
.ev.due::before {
  border-radius: 1px; border-color: color-mix(in srgb, var(--sage) 55%, transparent);
  background: color-mix(in srgb, var(--sage) 22%, var(--surface-2));
}
.ev.due.late b { color: var(--danger); }
.ev.due.late::before {
  border-color: var(--danger); background: color-mix(in srgb, var(--danger) 45%, var(--surface-2));
}
/* The first task after the meetings gets air, so the two lists read as two. */
.ev:not(.due) + .ev.due { margin-top: 6px; padding-top: 10px; }
/* A calendar that could not be read says so in place, where the meetings would
   have been — never as an empty day. */
.ev.warn b, .ev.warn span { color: var(--danger); }
.ev.warn span, .ev.hintrow span { white-space: normal; }
.ev.hintrow span { color: var(--text-2); }

/* Starred models. A star is the one control people already understand for
   "pin this", and the HUD picker shows what is starred. */
.starbtn {
  background: none; border: 0; cursor: pointer; padding: 0 4px;
  font-size: 16px; line-height: 1; color: var(--muted);
}
.starbtn.on { color: var(--gold); }
.starbtn:hover { color: var(--gold); }
.starbtn:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; border-radius: 4px; }

/* "+4 more" under the picker: the list is trimmed on purpose, and saying so
   stops three rows reading as models having disappeared. */
.pickmore {
  display: block; width: 100%; margin-top: 4px; padding: 4px 6px;
  background: none; border: 1px dashed var(--line); border-radius: 6px;
  color: var(--muted); font: inherit; font-size: 11px; cursor: pointer;
}
.pickmore:hover { border-color: var(--gold); color: var(--gold); }

/* The practice shelf. Collapsed by default — the title and the trigger are
   what you scan for; the body is what you read once you have found it. */
.prac {
  padding: 7px 10px; margin-bottom: 4px; border-radius: 8px;
  background: var(--surface-2); border: 1px solid var(--line); font-size: 13px;
}
.prac summary { cursor: pointer; list-style: none; }
.prac summary::-webkit-details-marker { display: none; }
.prac summary::before { content: "▸ "; color: var(--muted); }
.prac[open] summary::before { content: "▾ "; }
.prac-when { color: var(--muted); font-size: 12px; margin-left: 8px; }
.prac p { margin: 8px 0 0; color: var(--text-2); }


/* ---------- legibility over decoration ----------
   Every remaining backdrop blur is over the page background rather than over
   text — the picker's dimmed backdrop is the only one, and it is a modal where
   blurring what is behind is the point. Anybody who would rather have none of
   it, including screen-reader and low-vision users whose systems already ask
   for reduced transparency, gets none of it. */
@media (prefers-reduced-transparency: reduce) {
  .picker-back { backdrop-filter: none; background: rgba(8, 6, 4, .88); }
  .hud::after { display: none; }
}

/* The stale-engine banner and its siblings sit under the sticky bar when the
   page scrolls. Nothing blurs them now, and they keep a solid background so
   they stay readable over whatever is behind. */
.banner { background-color: var(--surface); }
.banner.err { background-color: color-mix(in srgb, var(--danger-fg) 12%, var(--surface)); }
.banner.ok { background-color: color-mix(in srgb, var(--ok-fg) 10%, var(--surface)); }
.banner.warn { background-color: color-mix(in srgb, var(--warn-fg) 11%, var(--surface)); }

/* ---------- "how do I get this key?" ----------
   Answered next to the empty box, because that is the moment the question is
   asked. "Paste your API key" with no route to one is where linking a service
   fails, and a support article is three clicks and a search away. */
.info {
  width: 16px; height: 16px; padding: 0; margin-left: 6px;
  border-radius: 50%; border: 1px solid var(--muted);
  background: none; color: var(--muted);
  font: 600 11px/14px var(--font); font-style: italic;
  cursor: pointer; vertical-align: middle;
}
.info:hover, .info[aria-expanded="true"] { border-color: var(--gold); color: var(--gold); }
.info:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }

.how[hidden] { display: none; }
.how {
  margin: 6px 0 8px; padding: 10px 12px; border-radius: 8px;
  background: var(--surface-2); border: 1px solid var(--line);
  font-size: 12.5px; line-height: 1.5;
}
.how-steps { margin: 0; color: var(--text-2); }
.how-meta { margin: 8px 0 0; display: flex; gap: 12px; align-items: baseline; flex-wrap: wrap; }
.how-cost { color: var(--sage); }
.how-meta a { color: var(--gold); }
/* What the key can do if it leaks. Only present where the answer is alarming,
   so it should look different from the steps rather than blend into them. */
.how-care {
  margin: 8px 0 0; padding-left: 10px;
  border-left: 2px solid color-mix(in srgb, var(--danger) 55%, transparent);
  color: var(--text-2);
}

/* ---------- phones ----------
   The existing breakpoints stop at two columns, which is right for a tablet and
   wrong for a 390px screen: two 1fr columns leave ~180px a side, and the
   right-hand gauges were being clipped mid-word — "shared acro", "$0.(". One
   column, and nothing is allowed to set a width the viewport cannot honour.

   Layout only. No control moves, nothing is hidden that was not already hidden,
   and every rule here is inside this query. */
@media (max-width: 640px) {
  main { padding: 10px; }

  /* One column, and `minmax(0, 1fr)` rather than `1fr`, which is the whole
     fix. A bare `1fr` is `minmax(auto, 1fr)`, and that `auto` floor is
     min-content — so one wide child sets the width of the track and the track
     sets the width of the panel. The HUD was computing 556px of columns inside
     a 373px section, and everything past the edge was simply cut off. Nothing
     about stacking helps while the tracks refuse to be narrow. */
  .hud { grid-template-columns: minmax(0, 1fr); }
  .hud-grid { grid-template-columns: minmax(0, 1fr); gap: 12px; }
  .hud-col { gap: 12px; }

  /* The rule that actually fixes the clipping, and the one that is invisible
     from reading the CSS: a grid or flex item defaults to `min-width: auto`,
     which means it refuses to shrink below the width of its own content. The
     HUD grid was computing to 556px inside a 373px panel — the model picker's
     monospace tags set the floor — and everything past the edge was cut off.
     Stacking the columns did nothing on its own because the item was never
     allowed to be narrow in the first place. */
  .hud > .hud-grid, .hud-grid, .hud-col, .gauge, .gauge > * { min-width: 0; }

  /* And once it can be narrow, the content has to cope with being narrow. A
     model tag is one long token with no space to break at. */
  .gauge b, .gauge i, .gauge em, .picktag, .pickrole { overflow-wrap: anywhere; }
  .picklist, .pickrow { min-width: 0; max-width: 100%; }

  /* Seven figures across a 375px screen is two characters a figure. Two per
     row is the floor at which the caption, the number and its note each still
     fit on one line -- and it is a floor rather than auto-fit's answer,
     because minmax(124px, 1fr) would give three cramped columns here. */
  .hud-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }

  /* Truncation limits written for a narrow column are wrong once the column is
     the whole screen. */
  .hud-read b, .stat { max-width: none; }
  .stat { font-size: 22px; }

  /* Rows of chrome that fit side by side at 1400px and do not at 390. Wrapping
     beats a horizontal scrollbar on a page whose content scrolls vertically. */
  .topbar { flex-wrap: wrap; row-gap: 8px; }
  .topbar .spacer { flex: 1 1 100%; height: 0; }
  .hud-controls { flex-wrap: wrap; row-gap: 10px; }
  .hud-strip { flex-wrap: wrap; row-gap: 6px; }
  .composer-actions { flex-wrap: wrap; row-gap: 8px; }

  /* A sidebar that pushes the page sideways on a screen this size. It is
     already an overlay below 760px; this stops it being wider than the phone. */
  .rail { width: min(232px, 86vw); }

  /* Tables are the one thing that genuinely cannot reflow, so they scroll
     inside their own box rather than widening the page. */
  .scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
  .tbl, .scroll table { min-width: 480px; }

  /* Dialogs sized for a desktop window. */
  .picker { padding: 10px; }
  .picker-box { max-height: 88vh; }

  /* The day strip and the practice cards read as prose on a phone; the
     three-column grid squeezes the summary to two words a line. */
  .ev { grid-template-columns: 46px 1fr; }
  .ev i { grid-column: 2; font-size: 10px; }

  /* Touch targets. 16px on inputs also stops iOS zooming the page in on focus,
     which then leaves it zoomed and the layout apparently broken. */
  input, textarea, select { font-size: 16px; }
  .btn, .chip, .key { min-height: 36px; }
}

/* ---------- 390x844: the phone pass on Enterprise Home --------------------
   Everything here is behind the narrow query and nothing above it moves:
   1280 and 1440 were verified against the redesign and are deploying, and a
   regression there costs more than any of the five defects below. Each was
   measured at 390x844 before it was written, and the numbers in the comments
   are the measurements rather than estimates.

   Deliberately NOT touched: the vertical order (the command bar already
   leads and the Eye already sits below the fold), and the three module
   states, whose dashed-vs-solid stripe plus glyph plus status word survive
   the narrow width intact. */
@media (max-width: 640px) {
  /* 1. The metrics band, which was the worst thing on the page.
     `minmax(0, 1fr) 144px` is right on a laptop -- the disclosure needs a
     fixed column so the readings do not shuffle sideways as their notes
     change length -- and wrong here. At 390 the band is 328px, so the
     disclosure took 144 of it (44%, for four words) and the four primary
     readings shared the remaining 168 at ~84px each: "COMPLETED" wrapped
     mid-word to "COMPLET / ED" and its note clamped to "nothi / ng...".
     Stacked, the readings get the whole 328 across the two columns
     .hud-metrics already asks for at this width -- 164px a reading, and the
     longest caption (COMPLETED, tracked at .2em) measures 82px -- and the
     disclosure becomes a full-width row beneath them, which is also how it
     stops being a 156px-tall sliver of a control. */
  .hud-metrics-band { grid-template-columns: minmax(0, 1fr); row-gap: 4px; }
  .metrics-more-btn { align-self: start; min-height: 44px; padding: 12px 0 12px 12px; }

  /* 2. The Eye, contained rather than changed. .orbit-front overhangs the
     reactor by 34% each way so a specialist at a ring's far edge is not cut
     off. At 390 the reactor is 335px, so that overhang is 114px, and the
     reactor's right edge is 24px from the viewport's -- the canvas ran to
     x=480 and body.scrollWidth read 480 against a 390 client.
     The instrument itself is untouched: hud.js draws the front layer in host
     coordinates (frame() translates by hb.left - fb.left before it draws
     anything), so this box is a clipping window and not a size. 20px is the
     overhang measured to still hold every arc the instrument actually paints
     at this width -- the widest reaches 6px past the reactor -- and it is px
     rather than a percentage because the room to the right of the reactor is
     a constant 24px at every narrow width while a percentage of the reactor
     grows with the screen. Only the horizontal axis is constrained; vertical
     overhang costs nothing, since the page scrolls that way anyway. */
  .orbit-front { left: -20px; width: calc(100% + 40px); }
  /* ...and the second, smaller source underneath it, which the canvas had
     been hiding: with the canvas contained, body.scrollWidth still read 419
     one frame and 432 the next. The reactor's graticule is an SVG whose arcs
     carry a drop-shadow filter and turn on a `rot` animation, so their ink
     overflow breathes past the 300x300 viewBox while nothing visibly moves,
     and none of it is painted -- the arcs stop at x=372 and the HUD's own
     padding box ends at 380. So this clips where the panel already ends.
     `clip` rather than `hidden` on purpose: hidden would make the HUD a
     scroll container, which is how body came to report 480 in the first
     place; clip is a clip and nothing else. The x axis only, so the panel
     stays free to grow downwards. */
  .hud { overflow-x: clip; }

  /* 3. Touch targets. 37 controls measured under 44px tall. Where the glyph
     is the control (the week strip's refresh at 22x17, the tier "i" at
     16x16) the painted mark stays exactly the size it is and a transparent
     ::after centred on it carries the hit area -- it is absolutely
     positioned, so it takes no space and moves nothing. Where the control is
     a row of text (the eight module disclosure triangles at 24px, the four
     prompt chips at 28px) the box itself grows: the module header band is
     already 44px tall, so the fold filling it changes no layout at all. */
  .weekbar-refresh, .tier-info-btn, .switch { position: relative; }
  .weekbar-refresh::after, .tier-info-btn::after, .switch::after {
    content: ""; position: absolute; left: 50%; top: 50%;
    width: 44px; height: 44px; transform: translate(-50%, -50%);
  }
  .home-module-fold { min-height: 44px; }
  .btn, .chip, .key, .chips button, .home-module-pin,
  .seg button, .engine-btn { min-height: 44px; }
  .btn.send { width: 44px; height: 44px; }
  .rail-toggle { min-width: 44px; min-height: 44px; justify-content: center; }
  /* Found by walking the console on a 375px phone: these kept their desktop
     padding and measured under 44px. */
  button.btn.sm, .topbar-state .btn, .topbar-account .btn, .mode-toggle,
  .rail-nav button, .rail-dest-head { min-height: 44px; }

  /* 4. The type floor. 325 visible text nodes measured at 11.5px or smaller
     here, 12 of them at 9px and 149 at 10px -- the metric eyebrows, the
     panel headers, the module status chips, the machine-bar labels and every
     table heading. 12px is the floor, and it is a floor rather than a scale:
     nothing listed here is ever larger than that, so this only ever raises.
     The tier "i" is left at its own size on purpose -- it is a 16px disc
     with a glyph in it, not a line of text, and it got its target above. */
  h2, .gauge em, .gauge i, .pill, .home-module-top .pill, .home-module-chev,
  .weekday-num, .weekday-clear, .reactor-hint, .rail-meta,
  .composer-hint, .composer-model, .model-tier-note, .model-tier-detail,
  .agent-name, .media-tag, .dash-subhead, .perm-tag, .webllm-opt .size,
  .orbit-node-name, th { font-size: 12px; }

  /* 5. "Find a company". `.row > *` is `1 1 150px`, and two 150px items plus
     the 10px gutter fit inside 328 -- so the company field and the site
     field sat abreast at 159px each and the second placeholder truncated to
     "Site URL — optional,". Text fields take the whole row on a phone; the
     Discover button is in a .narrow, keeps its `0 0 auto`, and stays
     button-sized rather than stretching across the screen. */
  .row > input, .row > select, .row > textarea { flex-basis: 100%; }
}

/* Nothing should ever scroll the page sideways. If a rule above misses a case,
   this contains it rather than letting the whole layout drift off-screen —
   which is what "it looks broken on my phone" almost always is. */
html, body { max-width: 100%; overflow-x: hidden; }

/* Contacts. Reuses the tile and .ev shapes rather than inventing a third card
   style — the console's look is settled and a CRM bolted on in its own idiom
   would read as a different product. */
.linky { background:none; border:0; padding:0; color:var(--gold); font:inherit; cursor:pointer;
         text-align:left; }
.linky:hover { text-decoration:underline; }
.stage-pick { padding:3px 6px; font-size:12px; background:var(--surface-2);
              border:1px solid var(--line); border-radius:6px; color:var(--text); }
ul.plain { margin:4px 0 0; padding-left:18px; font-size:12.5px; color:var(--text-2); }

/* --- token modal ----------------------------------------------------------- */
.modal { position:fixed; top:0; left:0; right:0; bottom:0; z-index:1000; display:flex; align-items:center; justify-content:center; background:rgba(0,0,0,0.85); }
.modal.hidden { display:none; }
.modal-content { background:var(--surface); border:1px solid var(--line); border-radius:16px; padding:32px; max-width:500px; width:90%; text-align:center; box-shadow:0 32px 100px rgba(0,0,0,0.8); animation:modal-in 0.2s ease-out; }
@keyframes modal-in { from { opacity:0; transform:scale(0.95); } to { opacity:1; transform:scale(1); } }
.modal-content h3 { margin:0 0 16px; color:var(--gold); font-size:20px; }
.modal-content .muted { font-size:13px; margin-bottom:16px; }
.modal-content code { display:block; background:var(--bg); border:1px solid var(--line); border-radius:8px; padding:16px; font-size:14px; word-break:break-all; color:var(--text); font-family:var(--mono); user-select:all; margin-bottom:20px; }
.modal-content .btn { width:auto; }

/* === visual overhaul (2026-08-27) ========================================= */

/* --- settings sub-tabs ----------------------------------------------------
   One group of sections visible at a time; every section still exists in the
   DOM so Save settings keeps reading all of them. .stab-off is its own class
   rather than .hidden because applyRole() also drives .hidden on the
   data-owner sections, and two owners of one class fight. */
.settings-tabs {
  display: flex; gap: 2px; margin: 0 0 18px;
  border-bottom: 1px solid var(--line);
  overflow-x: auto; scrollbar-width: none;
}
.settings-tabs::-webkit-scrollbar { display: none; }
.settings-tabs button {
  background: none; border: 0; border-bottom: 2px solid transparent;
  color: var(--muted); font: inherit; font-size: 13px;
  padding: 8px 14px; margin-bottom: -1px; cursor: pointer;
  white-space: nowrap; letter-spacing: .02em;
}
.settings-tabs button:hover { color: var(--text-2); }
/* The global nav button[aria-selected="true"] rule paints a filled gold
   pill (right for the rail, wrong here — gold text on a gold fill is
   invisible). This is an underline tab, so the fill is explicitly off. */
.settings-tabs button.on,
.settings-tabs button[aria-selected="true"] {
  color: var(--gold); border-bottom-color: var(--gold);
  background: none; font-weight: 600; border-radius: 0;
}
.stab-off { display: none !important; }

/* --- approval-mode cards ------------------------------------------------- */
.perm-cards {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 10px;
}
.perm-card {
  text-align: left; font: inherit; cursor: pointer;
  display: flex; flex-direction: column; gap: 6px;
  padding: 14px 16px; border-radius: 10px;
  border: 1px solid var(--line);
  background: var(--wash-raised), var(--surface);
  color: var(--text-2);
  transition: border-color .15s, box-shadow .15s;
}
.perm-card:hover { border-color: var(--gold-mid); }
.perm-card[aria-checked="true"] {
  border-color: var(--gold); color: var(--text);
  box-shadow: inset 0 0 0 1px var(--gold-mid);
}
.perm-tag.perm-risk { color: var(--danger-fg); }
.perm-card:disabled { opacity: .45; cursor: not-allowed; }
.perm-name {
  font-weight: 600; font-size: 14px; color: var(--text);
  display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap;
}
.perm-tag {
  font-style: normal; font-size: 10px; letter-spacing: .08em;
  text-transform: uppercase; color: var(--sage);
}
.perm-desc { font-size: 12px; color: var(--muted); line-height: 1.5; }

/* The consent dialogs' inline links pick up the theme rather than browser
   default blue. */
#bypass-consent a, #control-consent a { color: var(--gold); }

/* --- the reusable AI-agency warning --------------------------------------
   One muted line with an amber dot, under every surface where the user
   grants Anvella a capability. Quiet on purpose: it must be present
   everywhere without turning every panel into a wall of caution tape. */
.ai-warn {
  display: flex; gap: 8px; align-items: baseline;
  font-size: 12px; color: var(--muted); line-height: 1.5;
  margin: 12px 0 0;
}
.ai-warn-dot {
  flex: 0 0 auto; width: 7px; height: 7px; border-radius: 50%;
  background: var(--warn-fg); align-self: center;
  box-shadow: 0 0 6px color-mix(in srgb, var(--warn-fg) 45%, transparent);
}
.ai-warn a { color: var(--muted); text-decoration: underline; text-underline-offset: 2px; }
.ai-warn a:hover { color: var(--gold); }

/* --- model recommendation cards ------------------------------------------
   Replaces nested panel-in-panel cards with yellow pill badges and a
   full-width Approve slab. Card: a whisper of vertical gold, hairline
   border. Chip: small-caps category, not a glowing pill. Actions: compact
   ghost-gold Approve beside a quiet text Deny. */
.rec-card {
  border: 1px solid var(--line); border-radius: 10px;
  padding: 13px 16px; margin: 8px 0;
  background: var(--wash-raised), var(--surface);
  line-height: 1.45;
}
.rec-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.rec-chip {
  font-size: 10.5px; font-variant-caps: all-small-caps; letter-spacing: .12em;
  color: var(--gold); border: 1px solid var(--gold-mid);
  border-radius: 4px; padding: 1px 7px; background: transparent;
}
.rec-swap { font-size: 13px; color: var(--text); }
.rec-swap b { font-weight: 600; }
.rec-swap i { font-style: normal; color: var(--muted); margin: 0 6px; }
.rec-reason { margin: 7px 0 0; font-size: 12.5px; color: var(--text-2); }
.rec-apply { margin: 5px 0 0; font-size: 12px; color: var(--muted); }
.rec-apply em { font-style: normal; color: var(--text-2); }
.rec-actions { display: flex; align-items: center; gap: 14px; margin-top: 10px; }
.rec-actions .btn.rec-approve {
  width: auto; padding: 5px 14px; font-size: 12.5px; font-weight: 600;
  background: transparent; color: var(--gold); border: 1px solid var(--gold-mid);
}
.rec-actions .btn.rec-approve:hover { border-color: var(--gold); filter: none; background: var(--gold-dim); }
.rec-deny {
  background: none; border: 0; padding: 5px 2px; cursor: pointer;
  font: inherit; font-size: 12.5px; color: var(--muted);
}
.rec-deny:hover { color: var(--danger); }

/* --- model catalog table --------------------------------------------------
   Fixed column plan instead of the browser guessing, and a real minimum
   width so narrow screens scroll the table inside .scroll instead of
   crushing every column into a one-word-per-line tower. */
[data-panel="brains"] .scroll table { table-layout: fixed; min-width: 1060px; }
[data-panel="brains"] .scroll th:nth-child(1) { width: 34px; }
[data-panel="brains"] .scroll th:nth-child(2) { width: 22%; }
[data-panel="brains"] .scroll th:nth-child(3) { width: 58px; }
[data-panel="brains"] .scroll th:nth-child(4) { width: 168px; }
[data-panel="brains"] .scroll th:nth-child(5) { width: 92px; }
[data-panel="brains"] .scroll th:nth-child(6) { width: 104px; }
[data-panel="brains"] .scroll th:nth-child(8) { width: 64px; }
[data-panel="brains"] .scroll th:nth-child(9),
[data-panel="brains"] .scroll td:last-child { width: 148px; text-align: right; }
[data-panel="brains"] .scroll td { overflow-wrap: break-word; }
[data-panel="brains"] .scroll td:last-child { white-space: nowrap; }
/* NOTES breathes instead of cramming: a hard cap plus comfortable leading. */
[data-panel="brains"] .scroll td.model-note {
  max-width: 340px; line-height: 1.5; font-size: 12px;
}
/* The model name on its own line; its badges one quiet inline row below,
   smaller and dimmer than the pills elsewhere, so they annotate the name
   instead of shouting over it. */
.model-tag { font-family: var(--mono); font-size: 12.5px; color: var(--text); }
.model-badges { margin-top: 3px; line-height: 1.4; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.model-badges .pill {
  font-size: 9.5px; padding: 0 6px; margin-right: 4px;
  background: transparent; border-color: var(--line); color: var(--muted);
}
.model-badges .pill.gold { color: var(--gold); border-color: var(--gold-mid); }
/* REQUIRES: one compact chip per fact, consistently shaped. */
.req-chips { display: flex; flex-direction: column; gap: 3px; align-items: flex-start; }
.req-chips .pill {
  font-size: 10.5px; padding: 1px 7px;
  background: transparent; border-color: var(--line); color: var(--muted);
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.req-chips .pill.danger { color: var(--danger-fg); border-color: var(--danger-line); }
.req-chips .pill.sage { color: var(--ok-fg); border-color: var(--ok-line); }
.req-chips .muted { font-size: 11px; }

/* #conn-summary is a .row, and `.row > *` stretches every child to fill --
   right for the form fields that rule exists for, wrong for a two-word count
   chip, which was being pulled to half the panel width. */
#conn-summary .pill { flex: 0 0 auto; }

/* --- connectors: the direct key link -------------------------------------- */
.conn-keylink { margin-left: auto; font-size: 12px; white-space: nowrap; }
.conn-keylink a { color: var(--gold); text-decoration: none; border-bottom: 1px dotted var(--gold-mid); }
.conn-keylink a:hover { border-bottom-color: var(--gold); }

/* --- ad campaigns (enterprise) -------------------------------------------- */
.ad-card {
  border: 1px solid var(--line); border-radius: 10px;
  padding: 14px 16px; margin-bottom: 12px;
  background: var(--wash-raised), var(--surface);
}
.ad-card .row { margin-bottom: 8px; }
.ad-copy { min-height: 84px; font-size: 13px; line-height: 1.5; }
.ad-status {
  font-size: 10.5px; font-variant-caps: all-small-caps; letter-spacing: .1em;
  color: var(--muted); border: 1px solid var(--line); border-radius: 4px;
  padding: 1px 7px;
}
.ad-status.active { color: var(--ok-fg); border-color: var(--ok-line); }

/* ---------- reduced motion: the safety net ---------------------------------
   Everything above stops motion rule by rule, around twenty times, each rule
   naming the selector it was written against. That arrangement stops working
   silently the moment an animating rule gains specificity, and it already has:
   the mic button kill rule below names `.btn.mic.active` (three classes) while
   the rule it has to beat became `.btn.ghost.tiny.mic.active` (five). The
   comment at that rule documents the very same specificity trap being found
   and fixed for the colour -- and missed, two lines later, for the animation.

   Measured in a browser with the query forced on: six animations still ran,
   five of them infinite (the mic pulse, the reactor radar sweep, both reactor
   ring rotations, and the modal zoom). Nobody reading the twenty rules above
   would have guessed which six.

   So this neutralises motion once, for everything, with !important. It does
   not set animation-name, so every considered per-rule treatment above still
   applies in full (the skeleton holding its brighter mid-tone rather than
   simply freezing, and so on) -- this only catches what they miss. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }

  /* One deliberate exception. A busy spinner is status, not decoration: frozen,
     "still working" and "stuck" look identical, and nothing else on screen says
     which -- it is what stands in for a reply that has not arrived yet
     (anvella.js renders it as the pending message body). It is 11px, turns in
     place, and moves nothing around it, so it carries none of what makes motion
     a vestibular problem. Kept, and slowed to half speed. */
  .spin {
    animation-duration: 1.4s !important;
    animation-iteration-count: infinite !important;
  }
}

/* Light chassis: the instrument retains its restored optical environment. */
:root[data-mode="light"] body[data-plan] {
  --gold: inherit;
  --gold-dim: inherit;
  --gold-mid: inherit;
}

:root[data-mode="light"] body,
:root[data-mode="light"] .topbar,
:root[data-mode="light"] .rail {
  background: var(--bg);
}

:root[data-mode="light"] select {
  color-scheme: light;
}

:root[data-mode="light"] .panel,
:root[data-mode="light"] .hud {
  background: var(--surface);
  border-color: color-mix(in srgb, var(--line) 25%, transparent);
  box-shadow: 0 2px 8px color-mix(in srgb, var(--reactor-bg) 6%, transparent);
}

:root[data-mode="light"] .composer {
  background: var(--surface);
  border-color: var(--line);
  box-shadow: 0 2px 6px color-mix(in srgb, var(--reactor-bg) 10%, transparent);
}

:root[data-mode="light"] .composer:focus-within {
  border-color: var(--gold);
  box-shadow: var(--focus-halo);
}

:root[data-mode="light"] .composer textarea::placeholder {
  color: var(--text-2);
  opacity: 1;
}

:root[data-mode="light"] button.btn:hover {
  filter: none;
}

:root[data-mode="light"] .btn.ghost.tiny.mic.active {
  color: var(--on-accent);
}

:root[data-mode="light"] .msg.you .who {
  text-shadow: none;
}

:root[data-mode="light"] .pickrow:hover {
  background: var(--surface-2);
}

:root[data-mode="light"] .check,
:root[data-mode="light"] .key:hover,
:root[data-mode="light"] .thread-toggle:hover,
:root[data-mode="light"] button.btn.ghost:hover {
  border-color: var(--gold);
}

:root[data-mode="light"] .check:hover {
  color: var(--gold);
}

:root[data-mode="light"] input:focus,
:root[data-mode="light"] textarea:focus,
:root[data-mode="light"] select:focus {
  border-color: var(--gold);
}

:root[data-mode="light"] :is(
  a, button, input, select, textarea, summary,
  [role="switch"], [role="tab"], [tabindex]
):focus-visible {
  outline: var(--focus-ring);
  outline-offset: 3px;
  box-shadow: var(--focus-halo);
}

:root[data-mode="light"] :is(.pill.sage, .banner.ok, .ad-status.active, .pick-have) {
  border-color: var(--ok-fg);
  box-shadow: inset 0 0 0 1px var(--ok-line);
}

:root[data-mode="light"] :is(.pill.warn, .banner.warn, .dash-tile.warn) {
  border-color: var(--warn-fg);
  box-shadow: inset 0 0 0 1px var(--warn-line);
}

:root[data-mode="light"] :is(.pill.danger, .banner.err, .dash-tile.error, .pick-care) {
  border-color: var(--danger-fg);
  box-shadow: inset 0 0 0 1px var(--danger-line);
}

:root[data-mode="light"] :is(.pill.gold, .banner) {
  border-color: var(--info-fg);
}

:root[data-mode="light"] .pill.muted {
  border-color: var(--neutral-fg);
  box-shadow: inset 0 0 0 1px var(--neutral-line);
}

/* Keep explicit state variants after the generic banner rule. */
:root[data-mode="light"] .banner.ok { border-color: var(--ok-fg); }
:root[data-mode="light"] .banner.warn { border-color: var(--warn-fg); }
:root[data-mode="light"] .banner.err { border-color: var(--danger-fg); }

:root[data-mode="light"] .req-chips .pill.sage {
  border-color: var(--ok-fg);
}

:root[data-mode="light"] .req-chips .pill.danger {
  border-color: var(--danger-fg);
}

:root[data-mode="light"] :is(
  .composer-secondary,
  .pick-why,
  .weekday-num,
  .weekday-clear,
  .dash-tile em,
  .dash-tile i,
  .dash-tile .hint,
  .dash-tile.off,
  .dash-tile.off b,
  .dash-tile.warn em::after,
  .dash-tile.error em::after,
  .agent.off
) {
  opacity: 1;
}

:root[data-mode="light"] :is(
  .dash-tile em,
  .dash-tile i,
  .dash-tile .hint,
  .weekday-clear
) {
  color: var(--muted);
}

:root[data-mode="light"] .stat {
  text-shadow: none;
}

:root[data-mode="light"] .reactor {
  --bg: var(--reactor-bg);
  --text: var(--reactor-text);
  --text-2: var(--reactor-text-2);
  --muted: var(--reactor-muted);
  --gold: var(--reactor-gold);
  --ok-fg: var(--reactor-ok);
  --warn-fg: var(--reactor-warn);
  --danger-fg: var(--reactor-danger);
  --sage: var(--reactor-ok);
  --danger: var(--reactor-danger);

  background: var(--reactor-bg);
  border-radius: 50%;
  isolation: isolate;
  overflow: visible;
  box-shadow:
    inset 0 2px 5px color-mix(in srgb, var(--reactor-bg) 80%, transparent),
    0 1px 0 var(--surface),
    0 3px 9px color-mix(in srgb, var(--reactor-bg) 16%, transparent);
}


:root[data-mode="light"] .reactor:focus-visible {
  outline: 2px solid var(--line); outline-offset: 4px; filter: none;
}
/* Preserve the restored canvas opacity, map, scrim, ring lighting and sweep. */
:root[data-mode="light"] .orbit-node { color: var(--reactor-text); }
:root[data-mode="light"] .orbit-node::before {
  background: var(--reactor-bg); border-color: var(--reactor-text);
}
:root[data-mode="light"] .orbit-node-name {
  background: var(--reactor-bg); color: var(--reactor-text);
}
:root[data-mode="light"] .orbit-node.recede { opacity: 1; }
:root[data-mode="light"] :is(.skel-card, .pickrow, .agent) { background: var(--surface-2); }
:root[data-mode="light"] .pickrow.active { background: var(--gold-dim); }
:root[data-mode="light"] :is(.brand, .mark) stop { stop-color: var(--gold); }
:root[data-mode="light"] .agent { --dept-color: var(--gold) !important; }
:root[data-mode="light"] .model-tier.locked { opacity: 1; }

/* A decision's outline is a control boundary, not a decorative state edge. */
:root[data-mode="light"] .rec-actions .btn.rec-approve {
  border-color: var(--gold);
}

/* Onboarding ingest preview rows */
.ing-row { display: flex; gap: 10px; align-items: flex-start; padding: 8px 0;
  border-bottom: 1px solid var(--line, rgba(127, 127, 127, .25)); }
.ing-take { padding-top: 2px; }
.ing-body { flex: 1; min-width: 0; }
.ing-field { display: inline-block; margin: 0 8px 4px 0; overflow-wrap: anywhere; }
.ing-field b { font-weight: 600; margin-right: 4px; }
.ing-field i { font-style: normal; opacity: .6; font-size: .85em; }
.ing-low { opacity: .6; text-decoration: underline dotted; }
.ing-warn, .ing-conflict, .ing-error { font-size: .9em; margin-top: 4px; }
.ing-conflict { border-left: 3px solid currentColor; padding-left: 8px; }
.ing-flag { outline: 1px dashed currentColor; outline-offset: 2px; }
.ing-over { display: block; margin-top: 4px; font-size: .9em; }
.ing-src { display: block; font-size: .8em; }
