/* collabd shared UI layer — the chrome primitives every page composes.
 *
 * Sits on top of tokens.css (the verdict + surface variables) and owns the pieces
 * that used to be copy-pasted per page: topbar, buttons, pills, panels, form
 * controls, reason rows, provenance, the verdict banner. Pages keep ONLY their own
 * layout. One source means the five surfaces can't drift apart again.
 *
 * DESIGN.md rules enforced here: color is the verdict (chrome stays slate), every
 * status signal is icon + text + color, radii are tight + hierarchical (4/6/8),
 * focus is a visible --accent ring on every interactive element, motion is
 * minimal-functional (80/160/240ms).
 */

/* ---- reset + base ---------------------------------------------------------- */
* { box-sizing: border-box; margin: 0; padding: 0; }
/* the hidden attribute always wins — even over display:flex/grid on the same
   element (the "visible hidden chip" class of bug) */
[hidden] { display: none !important; }
html, body { height: 100%; }
body {
  background: var(--bg); color: var(--text-primary);
  font-family: "General Sans", system-ui, sans-serif;
  font-size: 16px; line-height: 1.5; -webkit-font-smoothing: antialiased;
}
.mono { font-family: "JetBrains Mono", ui-monospace, monospace; font-feature-settings: "tnum"; }
a { color: var(--accent); }
a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 2px; }

/* the mono micro-label used for section/panel headings ("THE PLAN · 6 STEPS") */
.panel-label, .micro-label {
  font-family: "JetBrains Mono", monospace; font-size: 10.5px;
  text-transform: uppercase; letter-spacing: .1em; color: var(--text-muted);
}
.panel-label { margin-bottom: 12px; }

/* ---- app chrome ------------------------------------------------------------ */
.topbar {
  position: sticky; top: 0; z-index: 30;
  display: flex; align-items: center; gap: 18px; height: 56px; padding: 0 20px;
  background: color-mix(in srgb, var(--surface-1) 92%, transparent);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--border);
}
.wordmark { font-weight: 600; font-size: 16px; letter-spacing: -.01em; display: flex; align-items: center; gap: 8px; color: var(--text-primary); text-decoration: none; }
/* When the wordmark is a home button (an <a>), give it a gentle affordance — the mark
   and text stay put; only the emphasis softens on hover. */
a.wordmark { cursor: pointer; transition: opacity 80ms ease; }
a.wordmark:hover { opacity: .82; }
a.wordmark:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; }
/* Brand mark (the Loop, /shared/logo.svg) is neutral slate — DESIGN.md: color is
   the verdict, and almost nothing else. The ONE sanctioned exception is the live
   verdict dot: inside the app's own top bar, data-verdict mirrors the worst
   outstanding verdict — and only beside the .vcounts strip, whose glyph + count +
   word carry the signal (icon + text + color, never color alone). */
.logo { color: var(--text-primary); flex: none; display: block; }
/* A live mark is an instrument, not a decoration: the dot grows (logo.svg scales
   it via --logo-dot-scale) so the verdict reads clearly at chrome size. The slate
   mark keeps the brand geometry untouched. */
.logo[data-verdict] { --logo-dot-scale: 1.3; }
.logo[data-verdict="safe"] { --logo-accent: var(--verdict-safe); }
.logo[data-verdict="review"] { --logo-accent: var(--verdict-review); }
.logo[data-verdict="block"] { --logo-accent: var(--verdict-block); }
/* the topbar verdict-count strip — worst first, zero counts omitted */
.vcounts { display: flex; align-items: center; gap: 12px; font-family: "JetBrains Mono", monospace; font-size: 11.5px; font-feature-settings: "tnum"; }
.vcounts .vcount { display: inline-flex; align-items: center; gap: 5px; }
/* review's count icon is the mark's own square dot (logo.svg#dot) — the strip
   points back at the part of the logo that carries the verdict. */
.vcounts .vdot { width: 7px; height: 7px; flex: none; }
.vcounts .vcount.safe { color: var(--verdict-safe); }
.vcounts .vcount.review { color: var(--verdict-review); }
.vcounts .vcount.block { color: var(--verdict-block); }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
/* The signed-in account's avatar pill. Folded here from byte-identical blocks that were
   duplicated in board/index.html and gate/index.html (the source plan's T9, absorbed into
   T11/#521). `.who` deliberately stays page-level — the two pages genuinely differ there
   (gap 8px vs 10px), so it is not duplication. See web/shared/topbar.mjs. */
.avatar { width: 22px; height: 22px; border-radius: 9999px; border: 1px solid var(--border); display: block; }
.crumb { font-size: 13px; color: var(--text-muted); display: flex; align-items: center; gap: 8px; }
.crumb a { color: var(--accent); text-decoration: none; }
.crumb a:hover { text-decoration: underline; }
.crumb .mono { color: var(--text-secondary); }
.spacer { margin-left: auto; }

/* ---- buttons ---------------------------------------------------------------- */
.btn {
  font-family: inherit; font-size: 13.5px; font-weight: 500;
  padding: 9px 14px; border-radius: 6px; cursor: pointer;
  border: 1px solid transparent; text-align: center;
  transition: background 80ms ease, filter 80ms ease, opacity 80ms ease;
}
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* primary = high-contrast neutral (never the accent, never a verdict color) */
.btn-primary { background: var(--text-primary); color: var(--bg); font-weight: 600; }
.btn-primary:hover { filter: brightness(.92); }
.btn-secondary { background: var(--surface-2); color: var(--text-primary); border-color: var(--border); }
.btn-secondary:hover { background: var(--surface-3); }
.btn-ghost { background: transparent; color: var(--text-secondary); border-color: transparent; }
.btn-ghost:hover { color: var(--text-primary); background: var(--surface-2); }
/* Ship — the ONE committal action; verdict-green because it executes the verdict */
.btn-ship { background: var(--verdict-safe); color: var(--verdict-safe-ink); font-weight: 600; }
.btn-ship:hover { filter: brightness(1.08); }
/* A locked Ship must never show verdict-green beside a non-safe verdict. */
.btn-ship:disabled { background: var(--surface-3); color: var(--text-muted); border-color: var(--border); opacity: 1; }
.btn-ship:disabled:hover { filter: none; }
/* destructive-adjacent (reject): bordered, block-tinted on hover only */
.btn-reject { background: transparent; color: var(--verdict-block); border-color: color-mix(in srgb, var(--verdict-block) 45%, var(--border)); }
.btn-reject:hover { background: var(--verdict-block-bg); }
.btn[disabled] { opacity: .4; cursor: not-allowed; }
.btn-block { width: 100%; }
/* the compact chrome toggle (theme etc.) */
.toggle { font-family: inherit; font-size: 13px; font-weight: 500; padding: 6px 12px; border-radius: 6px; cursor: pointer; background: var(--surface-2); color: var(--text-primary); border: 1px solid var(--border); }
.toggle:hover { background: var(--surface-3); }
.toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ---- account menu (#528) -------------------------------------------------------
 * Settings / theme / sign-out collapsed behind one hamburger next to the identity
 * pill. A native <details> disclosure — the summary is the button, the panel drops
 * below it. Slate only: an account menu carries no verdict. Radius 6px = DESIGN md.
 */
/* the standalone Home icon (Change 3) — same chrome-icon box as the account button, so the
 * two sit as a matched pair; slate, icon + aria-label, never a verdict colour. The wordmark
 * goes to the marketing landing now; this returns the user to their board/gate. */
.topbar-home {
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 30px; border-radius: 6px; cursor: pointer;
  background: var(--surface-2); color: var(--text-secondary); border: 1px solid var(--border);
  text-decoration: none;
}
.topbar-home:hover { background: var(--surface-3); color: var(--text-primary); }
.topbar-home:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.topbar-home svg { width: 17px; height: 17px; }

.acct { position: relative; }
.acct-btn {
  display: inline-flex; align-items: center; justify-content: center;
  font-family: inherit; font-size: 14px; line-height: 1;
  width: 32px; height: 30px; border-radius: 6px; cursor: pointer;
  background: var(--surface-2); color: var(--text-primary); border: 1px solid var(--border);
  list-style: none; /* strip the native disclosure triangle (Firefox) */
}
.acct-btn::-webkit-details-marker { display: none; } /* …and WebKit/Blink */
.acct-btn:hover { background: var(--surface-3); }
.acct-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.acct[open] .acct-btn { background: var(--surface-3); }
.acct-panel {
  position: absolute; top: calc(100% + 6px); right: 0; z-index: 40;
  min-width: 168px; display: flex; flex-direction: column; gap: 2px; padding: 4px;
  background: var(--surface-1); border: 1px solid var(--border);
  border-radius: 6px; box-shadow: var(--shadow);
}
/* One row treatment for every item — a link (#settingsGear) or a button (#logout) alike;
 * overrides their .btn look so the panel reads as one menu. (Theme is no longer a row here —
 * it moved out to the standalone icon button below.) */
.acct-item {
  display: flex; align-items: center; gap: 8px; width: 100%;
  font-family: inherit; font-size: 13.5px; font-weight: 500; text-align: left;
  padding: 8px 10px; border-radius: 4px; cursor: pointer;
  background: transparent; color: var(--text-primary); border: 0; text-decoration: none;
}
.acct-item:hover { background: var(--surface-3); }
.acct-item:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* the standalone theme toggle — a chrome-icon box that sits LAST in the topbar's right-hand
 * group, after the account hamburger, replacing the old "◐ Theme" row inside the menu and
 * mirroring the landing's top-right icon control. Deliberately a touch smaller than the
 * neighbouring chrome boxes (Home / hamburger are 32×30) so it reads as an ancillary control,
 * not another peer button. Moon = dark, sun = light; which glyph shows is driven entirely by
 * :root[data-theme], so there's no per-page icon JS and no first-paint flash. Slate, icon +
 * aria-label — no verdict colour. */
.theme-toggle {
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; border-radius: 6px; cursor: pointer; padding: 0;
  background: var(--surface-2); color: var(--text-secondary); border: 1px solid var(--border);
}
.theme-toggle:hover { background: var(--surface-3); color: var(--text-primary); }
.theme-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.theme-toggle svg { width: 15px; height: 15px; }
.theme-toggle .ic-sun { display: none; }                              /* dark (default): moon */
:root[data-theme="light"] .theme-toggle .ic-moon { display: none; }
:root[data-theme="light"] .theme-toggle .ic-sun  { display: block; }  /* light: sun */

/* ---- verdict pills + stale ------------------------------------------------- */
.vpill { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; padding: 4px 9px 4px 8px; border-radius: 9999px; }
.vpill svg { width: 14px; height: 14px; }
.vpill.safe { color: var(--verdict-safe); background: var(--verdict-safe-bg); }
.vpill.review { color: var(--verdict-review); background: var(--verdict-review-bg); }
.vpill.block { color: var(--verdict-block); background: var(--verdict-block-bg); }
.stale-tag { display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px; font-weight: 600; color: var(--verdict-review); background: var(--verdict-review-bg); padding: 3px 8px; border-radius: 9999px; }
.stale-tag svg { width: 13px; height: 13px; }

/* ---- panels ----------------------------------------------------------------- */
.box { border: 1px solid var(--border); border-radius: 8px; background: var(--surface-1); padding: 16px; }
.box h3, .box h4 { font-size: 14px; font-weight: 600; margin-bottom: 12px; letter-spacing: -.01em; }
.notice { border: 1px solid var(--border); border-radius: 8px; background: var(--surface-1); padding: 18px 20px; font-size: 15px; color: var(--text-secondary); line-height: 1.5; }
.notice b { color: var(--text-primary); }
/* ---- empty state (T7, #520) ---------------------------------------------------
 * The ONE empty-state concept. `.empty` was already here as a bare muted-text rule with
 * six live consumers, so it was EXTENDED into the parameterized component (icon +
 * heading + body + optional CTA) rather than joined by a second, rival class —
 * two overlapping concepts in one stylesheet is the duplication T7 exists to remove.
 * Bare `<div class="empty">text</div>` still renders exactly as before; the structured
 * children below are additive. See web/shared/empty-state.mjs.
 * Slate only — an empty surface carries no verdict.
 */
.empty { color: var(--text-muted); font-size: 14px; padding: 24px 0; }
.empty-icon { font-size: 18px; color: var(--text-muted); margin-bottom: 8px; }
.empty-heading { font-size: 14px; font-weight: 600; color: var(--text-secondary); margin-bottom: 6px; }
.empty-body { color: var(--text-muted); line-height: 1.5; }
.empty-cta { display: inline-flex; margin-top: 14px; }

/* ---- tooltip (T7, #520) --------------------------------------------------------
 * Replaces native title="", whose whole problem is that it is mouse-only: a keyboard
 * user tabbing to a truncated timestamp gets nothing a mouse user gets for free. So the
 * bubble shows on :focus-visible as well as :hover, and the trigger is focusable.
 * CSS-only by design (T7: "no new JS file") — the trigger needs tabindex="0" plus
 * role="tooltip" + aria-describedby on the bubble, which is what makes a screen reader
 * announce it; CSS alone would only serve sighted keyboard users. Shape:
 *   <span class="tip" tabindex="0" aria-describedby="tip-x">2h ago
 *     <span class="tip-bubble" role="tooltip" id="tip-x">2026-07-16T09:00:00Z</span></span>
 *
 * The existing title="" timestamps on the board and gate CARDS are deliberately NOT
 * migrated: those cards are themselves focusable (a tabindex="0" div on the board, an <a>
 * on the gate), so a focusable trigger inside one would add a nested tab stop per card and
 * fight the rail's j/k navigation (T4). The first consumer is the activity trail's
 * relative timestamps (T5/#526), whose rows are not inside a focusable ancestor.
 * Radius 4px = DESIGN.md's sm (pills/inputs); 160ms, matching every other surface.
 */
.tip { position: relative; display: inline-flex; }
.tip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 2px; }
.tip-bubble {
  position: absolute; bottom: calc(100% + 6px); left: 50%; translate: -50% 0;
  z-index: 70; pointer-events: none;
  background: var(--surface-3); color: var(--text-primary);
  border: 1px solid var(--border); border-radius: 4px; box-shadow: var(--shadow);
  padding: 5px 8px; font-size: 11.5px; line-height: 1.4; white-space: nowrap;
  opacity: 0; transition: opacity 160ms ease-in;
}
.tip:hover .tip-bubble, .tip:focus-visible .tip-bubble { opacity: 1; transition-timing-function: ease-out; }
@media (prefers-reduced-motion: reduce) { .tip-bubble { transition-duration: 1ms; } }

/* ---- dropdown / positioned menu (T7, #520) -------------------------------------
 * Backs the command palette's result list. Radius 6px = DESIGN.md's md (cards) +
 * var(--shadow) — both hardcoded per convention, no token system, no new shadow value.
 * Slate only: a menu carries no verdict. The active row is marked by SURFACE, not hue.
 */
.dd-menu {
  list-style: none; margin: 0; padding: 4px;
  background: var(--surface-1); border: 1px solid var(--border);
  border-radius: 6px; box-shadow: var(--shadow);
  max-height: 320px; overflow-y: auto;
}
.dd-item {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 10px; border-radius: 4px; cursor: pointer;
  font-size: 13.5px; color: var(--text-primary);
}
.dd-item.is-active { background: var(--surface-3); }
.dd-item.is-disabled { color: var(--text-muted); cursor: default; }
.dd-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dd-hint {
  flex: none; font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: 11px; color: var(--text-muted);
}

/* ---- modal / dialog (T1, #518) ------------------------------------------------
 * The ONE modal definition in the codebase — the board's page-level .modal block was
 * deleted when the plan view moved onto this primitive. Built on native <dialog>, so
 * the focus trap, top-layer stacking, and Esc-to-close are the browser's, not ours
 * (see web/shared/modal.mjs for the no-polyfill decision).
 *
 * Radius 8px = DESIGN.md's lg (panels/drawer); motion is `short` 160ms, ease-out on
 * enter / ease-in on exit (DESIGN.md:147-153). Those scales are prose in DESIGN.md and
 * hardcoded by every existing rule here — do NOT hoist them into a token system.
 */
dialog.modal {
  /* the UA's own chrome, reset — native supplies behaviour, we supply the looks */
  padding: 0; border: 1px solid var(--border); border-radius: 8px;
  background: var(--surface-1); color: var(--text-primary);
  box-shadow: var(--shadow);
  width: 100%; max-width: 860px; max-height: calc(100vh - 96px);
  margin: 48px auto auto; /* top-anchored, matching the plan view's old overlay padding */
  overflow: hidden; /* the body scrolls, not the frame — keeps the header pinned */

  /* Exit motion. allow-discrete lets `display` and `overlay` animate, so the dialog
   * fades OUT on close instead of vanishing — including on a native Esc close, which we
   * never intercept. This is the payoff of the no-polyfill call. */
  opacity: 0; translate: 0 -4px;
  transition:
    opacity 160ms ease-in, translate 160ms ease-in,
    display 160ms ease-in allow-discrete, overlay 160ms ease-in allow-discrete;
}
dialog.modal[open] { opacity: 1; translate: 0 0; transition-timing-function: ease-out; }
/* the enter side of the same transition — the state the dialog animates FROM */
@starting-style { dialog.modal[open] { opacity: 0; translate: 0 -4px; } }

dialog.modal::backdrop {
  /* var(--bg) resolves here because ::backdrop inherits custom properties from its
   * originating element — so the scrim follows the light/dark theme exactly as the old
   * .modal-overlay did. Same 78% + 2px blur the plan view shipped with. */
  background: color-mix(in srgb, var(--bg) 78%, transparent);
  backdrop-filter: blur(2px);
  opacity: 0;
  transition: opacity 160ms ease-in, display 160ms ease-in allow-discrete, overlay 160ms ease-in allow-discrete;
}
dialog.modal[open]::backdrop { opacity: 1; transition-timing-function: ease-out; }
@starting-style { dialog.modal[open]::backdrop { opacity: 0; } }

/* Honour a reduced-motion preference: the dialog still opens, it just doesn't move. */
@media (prefers-reduced-motion: reduce) {
  dialog.modal, dialog.modal::backdrop { transition-duration: 1ms; translate: none; }
}

.modal-form { display: flex; flex-direction: column; max-height: calc(100vh - 96px); }
.modal-header { display: flex; align-items: center; gap: 12px; padding: 28px 32px 6px; }
.modal-header h2 { font-size: 24px; font-weight: 600; letter-spacing: -.02em; flex: 1; }
.modal-body { padding: 0 32px 28px; overflow-y: auto; flex: 1; min-height: 0; }
.modal-footer {
  display: flex; justify-content: flex-end; gap: 8px;
  padding: 16px 32px; border-top: 1px solid var(--border); background: var(--surface-2);
}

/* ---- command palette (T3, #522) ------------------------------------------------
 * Built on the modal primitive (dialog.modal), so the focus trap, top-layer stacking and
 * Esc-close are native — the palette adds only an input and a result list.
 *
 * SLATE, ABSOLUTELY — there is no verdict colour anywhere in this block, including on
 * result rows for runs that carry a verdict. The rail is where a verdict is read; the
 * palette is a jump list. DESIGN.md's restraint rule is about visual vocabulary, and a
 * palette is chrome — which is exactly why it must not borrow the verdict's colour to
 * look important.
 */
dialog.modal.palette { max-width: 560px; margin-top: 96px; }
.palette-input {
  width: 100%; font-family: inherit; font-size: 15px; padding: 14px 16px;
  background: transparent; border: 0; border-bottom: 1px solid var(--border);
  color: var(--text-primary);
}
.palette-input::placeholder { color: var(--text-muted); }
.palette-input:focus { outline: none; } /* the dialog itself is the focus context */
.palette-results { padding: 6px; max-height: 380px; overflow-y: auto; }
/* group headers are labels, not options — the keyboard never lands on one */
.palette-group {
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: 10px; text-transform: uppercase; letter-spacing: .1em;
  color: var(--text-muted); padding: 10px 10px 4px;
}
.palette-results .dd-menu { border: 0; box-shadow: none; background: transparent; padding: 0; max-height: none; }
/* The topbar's Search affordance: the palette's visible entry point, monochrome. */
.palette-open {
  display: inline-flex; align-items: center; gap: 8px;
  background: var(--surface-3); border: 1px solid var(--border); border-radius: 6px;
  padding: 5px 8px; cursor: pointer; color: var(--text-muted); font-size: 12.5px;
}
.palette-open:hover { color: var(--text-secondary); }
.palette-open:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.palette-kbd {
  font-family: "JetBrains Mono", ui-monospace, monospace; font-size: 10.5px;
  color: var(--text-muted); border: 1px solid var(--border); border-radius: 4px; padding: 1px 4px;
}

/* ---- activity trail (T5, #526) --------------------------------------------------
 * The visible audit trail. Reuses the .prov-details disclosure IA the Provenance drawer
 * already uses — no new chrome. Slate, deliberately: an audit row records who did what,
 * it is not a verdict on the code, so it carries no verdict colour even for an override.
 */
.act-wrap { margin-top: 8px; }
.act-list { list-style: none; display: flex; flex-direction: column; gap: 2px; }
.act-row { display: flex; align-items: flex-start; gap: 9px; padding: 7px 0; border-bottom: 1px solid var(--border); }
.act-row:last-child { border-bottom: 0; }
.act-glyph { flex: none; font-size: 11px; line-height: 1.6; color: var(--text-muted); }
.act-body { flex: 1; min-width: 0; }
.act-line { display: block; font-size: 12.5px; color: var(--text-secondary); line-height: 1.45; overflow-wrap: anywhere; }
.act-line b { color: var(--text-primary); font-weight: 500; }
.act-note { display: block; margin-top: 2px; font-size: 11.5px; color: var(--text-muted); font-style: italic; overflow-wrap: anywhere; }
.act-when { flex: none; font-size: 11px; color: var(--text-muted); }
.act-status { font-size: 12.5px; color: var(--text-muted); padding: 8px 0; }

/* ---- keyboard shortcuts help (T4, #523) ----------------------------------------
 * The `?` overlay's list, rendered from the SHORTCUTS registry in shortcuts.mjs (the same
 * source the Settings keyboard table reads, so the two can't drift). Slate — a shortcut
 * list carries no verdict.
 */
.sc-list { display: flex; flex-direction: column; gap: 2px; }
.sc-row { display: flex; align-items: center; gap: 14px; padding: 8px 0; border-bottom: 1px solid var(--border); }
.sc-row:last-child { border-bottom: 0; }
.sc-key {
  flex: none; min-width: 44px; text-align: center;
  font-family: "JetBrains Mono", ui-monospace, monospace; font-size: 11.5px;
  color: var(--text-secondary); background: var(--surface-3);
  border: 1px solid var(--border); border-radius: 4px; padding: 3px 6px;
}
.sc-desc { font-size: 13.5px; color: var(--text-secondary); }

/* ---- toast (T2, #519) ----------------------------------------------------------
 * The one transient-feedback surface, replacing three bespoke inline banners and every
 * blocking alert(). Bottom-right, never blocking input.
 *
 * NEUTRAL SLATE, ALWAYS — there is deliberately no .toast-error colour rule below, and
 * adding one is the leak DESIGN.md's "color is the verdict, and almost nothing else"
 * exists to prevent. The kind is carried by the GLYPH (✔/◉/✖) + the text, never a hue.
 * Radius 8px (lg, panels) and 160ms motion match the modal primitive — same scales,
 * hardcoded per DESIGN.md:147-153 convention, no token system.
 */
.toast-stack {
  position: fixed; right: 24px; bottom: 24px; z-index: 60;
  display: flex; flex-direction: column; gap: 8px;
  max-width: min(420px, calc(100vw - 48px));
  pointer-events: none; /* the gap between toasts must not eat clicks on the page */
}
.toast {
  pointer-events: auto;
  display: flex; align-items: flex-start; gap: 10px;
  border: 1px solid var(--border); border-radius: 8px;
  background: var(--surface-1); color: var(--text-primary);
  box-shadow: var(--shadow);
  padding: 12px 14px; font-size: 14px; line-height: 1.45;
  animation: toast-in 160ms ease-out;
}
@keyframes toast-in { from { opacity: 0; translate: 0 6px; } to { opacity: 1; translate: 0 0; } }
.toast-leaving { opacity: 0; translate: 0 6px; transition: opacity 160ms ease-in, translate 160ms ease-in; }
.toast-glyph { color: var(--text-secondary); font-size: 13px; line-height: 1.6; }
.toast-msg { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.toast-dismiss {
  background: none; border: 0; cursor: pointer; padding: 0 2px;
  color: var(--text-muted); font-size: 13px; line-height: 1.6;
}
.toast-dismiss:hover { color: var(--text-primary); }
@media (prefers-reduced-motion: reduce) {
  .toast { animation-duration: 1ms; }
  .toast-leaving { transition-duration: 1ms; translate: none; }
}

/* ---- form controls ----------------------------------------------------------- */
.field { margin-bottom: 16px; }
.field-label {
  display: block; font-size: 11px; text-transform: uppercase; letter-spacing: .08em;
  color: var(--text-muted); margin-bottom: 7px; font-weight: 500;
}
/* gate labels (the review/ship action forms) read as sentences, not micro-labels */
.gate label { display: block; font-size: 13px; font-weight: 500; }
.field-label .opt { text-transform: none; letter-spacing: 0; font-weight: 400; }
.input, .textarea, .select {
  width: 100%; font-family: inherit; font-size: 14px; padding: 10px 12px;
  border-radius: 6px; background: var(--surface-2); border: 1px solid var(--border);
  color: var(--text-primary); line-height: 1.45;
}
.textarea { resize: vertical; min-height: 62px; }
.input::placeholder, .textarea::placeholder { color: var(--text-muted); }
.input:focus-visible, .textarea:focus-visible, .select:focus-visible,
.input:focus, .textarea:focus, .select:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
.textarea.invalid, .input.invalid { border-color: color-mix(in srgb, var(--verdict-block) 55%, var(--border)); }
/* machine-data inputs (tokens, repo slugs) read as machine data */
.input.mono { font-family: "JetBrains Mono", ui-monospace, monospace; }

/* ---- reason rows (verdict findings) — class contract with verdict-component.mjs */
.reason { display: flex; gap: 10px; align-items: flex-start; font-size: 13.5px; line-height: 1.45; padding: 9px 0; }
.reason > div { min-width: 0; overflow-wrap: anywhere; }
.rtag { font-family: "JetBrains Mono", monospace; font-size: 10px; text-transform: uppercase; letter-spacing: .05em; padding: 3px 6px; border-radius: 4px; flex: none; margin-top: 1px; border: 1px solid var(--border); }
.rtag.sec { color: var(--verdict-block); border-color: color-mix(in srgb, var(--verdict-block) 40%, var(--border)); background: var(--verdict-block-bg); }
.rtag.test { color: var(--verdict-review); border-color: color-mix(in srgb, var(--verdict-review) 40%, var(--border)); background: var(--verdict-review-bg); }
.rtag.scope { color: var(--text-secondary); }
.reason .jump { display: block; margin-top: 3px; font-family: "JetBrains Mono", monospace; font-size: 11px; color: var(--accent); text-decoration: none; }
.reason .jump:hover { text-decoration: underline; }
.reason .loc { font-family: "JetBrains Mono", monospace; font-size: 11.5px; color: var(--accent); }

/* ---- checks (icon + text + color, never color alone) ------------------------- */
.checks { display: flex; flex-direction: column; gap: 8px; }
.check { display: flex; align-items: center; gap: 9px; font-size: 13px; color: var(--text-secondary); }
.check svg { width: 14px; height: 14px; flex: none; }
.check.pass { color: var(--verdict-safe); }
.check.fail { color: var(--verdict-block); }
.check.warn { color: var(--verdict-review); }
.check.muted { color: var(--text-muted); }

/* ---- provenance — class contract with verdict-component.mjs ------------------ */
.provenance { margin-top: 4px; font-family: "JetBrains Mono", monospace; font-size: 11px; color: var(--text-muted); line-height: 1.6; }
.prov-stale { color: var(--verdict-review); font-weight: 700; display: inline-flex; align-items: center; gap: 3px; }
.prov-stale svg { width: 12px; height: 12px; fill: currentColor; }
.prov-details { margin-top: 8px; }
.prov-details > summary { font-size: 11.5px; color: var(--text-secondary); cursor: pointer; user-select: none; }
.prov-details > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.provenance-drawer { margin-top: 8px; display: flex; flex-direction: column; gap: 4px; }
.prov-row { font-size: 11px; color: var(--text-muted); line-height: 1.5; word-break: break-all; }
.prov-row .mono { font-family: "JetBrains Mono", monospace; color: var(--text-secondary); }
.prov-note { font-style: italic; }
.prov-tuple { color: var(--text-muted); }

/* ---- dominant verdict banner — class contract with verdict-component.mjs ----- */
.verdict-banner { border: 1px solid var(--border); border-radius: 8px; padding: 18px 20px; background: var(--surface-1); box-shadow: var(--shadow); border-left-width: 4px; }
.verdict-banner.block { border-left-color: var(--verdict-block); }
.verdict-banner.safe { border-left-color: var(--verdict-safe); }
.verdict-banner.review { border-left-color: var(--verdict-review); }
/* STALE: an invalidated verdict shows no verdict color — de-rates to amber, loud. */
.verdict-banner.stale { border-left-color: var(--verdict-review); background: var(--verdict-review-bg); }
.verdict-banner.stale .vb-line { color: var(--verdict-review); }
.vb-was { color: var(--text-muted); text-decoration: line-through; }
.vb-line { display: flex; align-items: center; gap: 13px; font-size: 28px; font-weight: 600; letter-spacing: -.02em; }
.vb-line svg { width: 32px; height: 32px; flex: none; }
.verdict-banner.block .vb-line { color: var(--verdict-block); }
.verdict-banner.safe .vb-line { color: var(--verdict-safe); }
.verdict-banner.review .vb-line { color: var(--verdict-review); }
.coverage { margin-top: 12px; font-size: 14px; color: var(--text-secondary); line-height: 1.5; overflow-wrap: anywhere; }
/* the checked-facts list inside the verdict banner: one fact per row, no bullets
   (each row carries its own glyph — icon + text, never punctuation soup) */
.coverage-list { list-style: none; display: flex; flex-direction: column; gap: 5px; }
.coverage-list + .coverage { margin-top: 8px; }
.coverage .ok { color: var(--verdict-safe); }
.coverage .bad { color: var(--verdict-block); }
.coverage .gap { color: var(--text-muted); }
.coverage .muted { color: var(--verdict-review); }

/* ---- open-question answer blocks --------------------------------------------- */
.q-block { margin-top: 12px; padding: 10px 12px; border-radius: 6px; background: var(--surface-2); border: 1px solid var(--border); }
.q-block:first-child { margin-top: 0; }
.q-block .q-text { font-size: 13px; font-weight: 500; margin-bottom: 8px; line-height: 1.4; }
.q-opt { display: flex; align-items: flex-start; gap: 8px; font-size: 13px; padding: 4px 0; line-height: 1.4; cursor: pointer; }
.q-opt input { margin-top: 3px; flex: none; accent-color: var(--accent); }
.q-rec { font-family: "JetBrains Mono", monospace; font-size: 9.5px; text-transform: uppercase; letter-spacing: .06em; color: var(--verdict-safe); border: 1px solid color-mix(in srgb, var(--verdict-safe) 40%, var(--border)); border-radius: 4px; padding: 1px 5px; margin-left: 6px; }
.q-other { width: 100%; margin-top: 6px; font-family: inherit; font-size: 13px; padding: 7px 9px; border-radius: 6px; border: 1px solid var(--border); background: var(--surface-1); color: var(--text-primary); }
.q-other:focus { outline: 2px solid var(--accent); outline-offset: 1px; }

/* ---- acceptance criteria (C1 / #643) — class contract with criteria-editor.mjs ----- */
/* THE HERO SURFACE: the one place a non-coder is in control. It is SLATE end to end and NO
   --verdict-* token may appear in this block — not in the editor, and not (C5) on the
   per-criterion status rows. The criteria are what the human asked for, and a criterion's
   status is EVIDENCE-class (like the test fact), not a verdict. All saturated color stays on
   the verdict banner (DESIGN.md: color is the verdict, and almost nothing else). A CI test
   enforces this; the repo's three prior verdict-color leaks were each caught only after
   shipping wrong, so this one is enforced before the surface exists.
   Single-sourced here (not page-local) because BOTH approve surfaces render the panel and both
   verdict surfaces render the read-only block. */
.crit-panel { margin-top: 22px; }
.crit-hint { font-size: 12.5px; color: var(--text-muted); line-height: 1.5; margin: -6px 0 12px; max-width: 72ch; }
.crit-count { font-family: "JetBrains Mono", monospace; font-size: 11px; color: var(--text-secondary); border: 1px solid var(--border); border-radius: 4px; padding: 1px 5px; margin-left: 4px; }
.crit-rows { list-style: none; display: flex; flex-direction: column; gap: 8px; }
/* Row grid: label above, then input + remove on one line. The label is VISIBLE (never
   placeholder-only) — design D6. */
.crit-row { display: grid; grid-template-columns: 1fr auto; grid-template-areas: "label label" "input remove"; gap: 4px 8px; align-items: center; }
.crit-rowlabel { grid-area: label; font-size: 10.5px; text-transform: uppercase; letter-spacing: .07em; color: var(--text-muted); font-weight: 500; }
.crit-input { grid-area: input; font-size: 13.5px; padding: 8px 10px; }
.crit-remove { grid-area: remove; width: 30px; height: 30px; padding: 0; display: inline-flex; align-items: center; justify-content: center; font-size: 13px; border-radius: 6px; border: 1px solid transparent; background: transparent; color: var(--text-muted); cursor: pointer; }
.crit-remove:hover { color: var(--text-primary); background: var(--surface-2); }
.crit-remove:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.crit-foot { display: flex; align-items: center; gap: 12px; margin-top: 12px; flex-wrap: wrap; }
.crit-foot .btn { font-size: 13px; padding: 8px 12px; }
.crit-cap { font-size: 12px; color: var(--text-muted); }
.crit-empty { padding: 14px 0 4px; }
/* The polite live region: announced, never seen (visually-hidden, not display:none — a
   display:none region announces nothing). */
.crit-live { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
/* The drawer block: what you approved, above what we found. */
.crit-block { margin: 14px 0; }
.crit-list { list-style: none; display: flex; flex-direction: column; gap: 8px; }
.crit-item { display: flex; gap: 9px; align-items: flex-start; font-size: 13.5px; line-height: 1.45; }
.crit-item > * { min-width: 0; overflow-wrap: anywhere; }
.crit-text { color: var(--text-primary); }
.crit-note { font-size: 12.5px; color: var(--text-secondary); line-height: 1.5; margin-bottom: 10px; max-width: 72ch; }
/* A GRADED row (C5): the criterion in the human's own words LEADS, the slate status sits beside
   it, the anchored evidence reads as machine data beneath. Rows stack rather than sitting in a
   table so a long criterion wraps instead of forcing the 360px drawer wider. */
.crit-graded { display: grid; grid-template-columns: 1fr auto; grid-template-areas: "text status" "evidence evidence"; gap: 3px 10px; align-items: baseline; padding: 8px 0; border-bottom: 1px solid var(--border); }
.crit-graded:last-child { border-bottom: none; }
.crit-graded .crit-text { grid-area: text; }
/* The status: glyph + mono word, SLATE. Icon + text, never colour alone — and never a verdict
   colour: a criterion's status is EVIDENCE-class (like the test fact), not a verdict, so every
   saturated colour stays on the banner above. Enforced in CI over this block. */
.crit-status { grid-area: status; display: inline-flex; align-items: center; gap: 5px; color: var(--text-secondary); flex: none; }
.crit-status svg { width: 13px; height: 13px; }
.crit-glyph { font-family: "JetBrains Mono", monospace; font-size: 12px; width: 13px; text-align: center; }
.crit-word { font-family: "JetBrains Mono", monospace; font-size: 11px; letter-spacing: .02em; }
.crit-evidence { grid-area: evidence; font-size: 11.5px; color: var(--text-muted); line-height: 1.5; overflow-wrap: anywhere; }

/* ---- next-prompt draft panel: SLATE, shared by pr-review + the board drawer -------- */
/* A drafted next prompt is a PROPOSAL about work not yet done — it has judged nothing, so
   it stays slate and NEVER wears a verdict color (DESIGN.md: color is the verdict). NO
   --verdict-* token may appear in this block; an invariant test enforces it. The block is
   single-sourced here (not board-local): .np-body must stay robust in the narrow, resizable
   board drawer (360px default, drag-resizes narrower), so a long file:line wraps/scrolls
   instead of forcing the drawer wider. */
.nextprompt { margin-top: 12px; padding: 12px; border: 1px solid var(--border); border-radius: 6px; background: var(--surface-2); }
.np-head { font-size: 13px; font-weight: 600; color: var(--text-secondary); }
.np-help { margin-top: 4px; font-size: 12.5px; color: var(--text-muted); line-height: 1.45; }
.np-body { margin: 8px 0; padding: 8px 10px; border: 1px solid var(--border); border-radius: 6px; background: var(--surface-1); color: var(--text-primary); font-size: 12px; line-height: 1.5; white-space: pre-wrap; overflow-wrap: anywhere; overflow-x: auto; max-height: 220px; overflow-y: auto; }
.np-body:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.nextprompt .btn { margin-top: 0; }

/* ---- live-run indicator: the mark's ring, turning -------------------------------- */
/* Replaces the generic border-spinner everywhere. The RING alone spins (the agent
   loop turning); the dot — the human — never orbits. Slate (currentColor), never a
   verdict color — running is a state. Markup: <svg class="spin-mark" viewBox="0 0 32 32">
   <use href="/shared/logo.svg#ring"/></svg> */
.spin-mark { width: 13px; height: 13px; color: var(--text-secondary); animation: spin 1.1s linear infinite; flex: none; }
@keyframes spin { to { transform: rotate(360deg); } }

/* ---- scrollbars: slate instruments, not chrome defaults ------------------------- */
* { scrollbar-width: thin; scrollbar-color: var(--border) transparent; }
*::-webkit-scrollbar { width: 8px; height: 8px; }
*::-webkit-scrollbar-thumb { background: var(--border); border-radius: 9999px; }
*::-webkit-scrollbar-track { background: transparent; }

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
}
