:root {
  /* Laminar Praxis identity v2.1. Every colour in the stylesheet lives here;
     nothing below this block may carry a hex or rgba() literal. */

  /* --- text --------------------------------------------------------------- */
  /* Three tiers, matching the guide's fg-1 / fg-2 / fg-3. */
  --ink:        #333333;   /* LP Black — body text. Never #000000. 12.6:1. */
  /* The guide DOES have a mid-grey: --lp-slate: #5A5A5A, published as --fg-2,
     its secondary-text tier. An earlier pass collapsed this onto --ink on the
     assumption no such value existed. 6.9:1 on white — AA at every size, so it
     is safe for metadata but is NOT a licence to soften errors or warnings. */
  --ink-soft:   #5A5A5A;   /* --fg-2 — secondary text, metadata, timestamps */
  /* --fg-3, the guide's faintest text tier. 3.45:1 on white, so it clears AA
     only for >=18.66px bold / >=24px text and for non-text UI. The rule is that
     it must never carry the sole copy of something the reader needs.
     KNOWN EXCEPTION, pre-dating this change and not fixed by it:
     `.streaming-tokens` is --ink-mute at 15px and IS the sole copy of the answer
     while it streams. It sits on --paper-2, not white, so the real ratios are
     ~3.33:1 before this change and ~3.24:1 after — an existing AA failure, very
     slightly worsened. It needs a design decision about the streaming tier, not
     a token tweak, and is tracked in the deferred-work ledger. Do not cite this
     comment as evidence that every consumer conforms — one does not. */
  --ink-mute:   #8A8A8A;   /* small grey type (captions, eyebrows) */

  /* --- surfaces ----------------------------------------------------------- */
  --paper:      #FFFFFF;   /* "White content area" */
  --paper-2:    #F5F8FC;   /* blue-neutral tinted panel */
  --paper-edge: #EEEEEE;   /* grid lines */
  --rule:       #CCCCCC;   /* thin 1px light grey border */

  /* --- brand -------------------------------------------------------------- */
  --accent:      #2A4B7C;  /* LP Blue — headings, primary surfaces. 8.8:1 on white. */
  --accent-soft: #7FA6E0;  /* Light Blue — tinted panels, secondary data */
  /* LP Blue -700, the guide's own hover step. The earlier objection — that a
     hover desaturating toward charcoal travels in the same direction as the
     disabled state — was correct about #333333 but is void here: #1F3A62 is a
     blue, not a charcoal, so hover now steps the fill instead of growing a ring.
     11.4:1 on white, so a white label survives the step. */
  --accent-hover:  #1F3A62;
  --accent-pressed:#333333;  /* LP Black; the guide's own dark-surface partner to LP Blue */
  /* LP Red -700, the alert-side partner of --accent-hover. Declared because the
     spine's hover table names it; no red-filled control exists to consume it
     yet, since LP Red is spent only as a border, an icon or a marker. */
  --alert-hover:   #C40000;
  --highlight:   #F5F8FC;  /* cited-passage panel — blue-neutral, deliberately not red */
  --on-accent:   #FFFFFF;  /* white on LP Blue, for full-bleed surfaces */

  /* --- status ------------------------------------------------------------- */
  /* No green exists in the palette, so the "verified" state is LP Blue too. */
  --ok:         #2A4B7C;
  --warn:       #F8992B;   /* Orange — status indicators */
  --warn-soft:  #FDCC9C;   /* Light Orange — progress/status markers */
  /* LP Red. Alerts and required-action states ONLY: a border, an icon or a
     marker. #FF0000 on white is 4.0:1 — it fails WCAG AA for normal text, so it
     never carries a message body and never appears as text below 18px. Never
     paired with LP Blue in either direction. */
  --alert:      #FF0000;
  --alert-soft: #F0C5C5;   /* Soft Pink — risk highlights, exposure callouts */
  --alert-wash: rgba(255,0,0,.06);  /* LP Red as a panel tint behind a red rule */

  /* --- type --------------------------------------------------------------- */
  /* Self-hosted; see the @font-face block in index.html. Helvetica Neue is the
     guide's body face and Inter its sanctioned open-source fallback, so it leads
     the stack where it exists locally. The guide is silent on monospace, and a
     third webfont is out of scope, so --mono is a system stack. */
  --display:    "Montserrat", "Helvetica Neue", Arial, sans-serif;
  --sans:       "Inter", "Helvetica Neue", Arial, sans-serif;
  --mono:       ui-monospace, SFMono-Regular, Menlo, monospace;

  /* --- motion ------------------------------------------------------------- */
  /* Smooth-out only. Laminar means smooth, so the curve decelerates into rest
     and never overshoots — no bounces, no springs, no back-easing. The four
     ad-hoc `ease` values these replace (.1s/.12s/.15s/.25s) were inferred, not
     sourced. Every duration here is collapsed by the prefers-reduced-motion
     block at the foot of this file. */
  --ease-out:   cubic-bezier(0.22, 1, 0.36, 1);
  --dur-fast:   120ms;   /* micro — colour, border, small state flips */
  --dur-med:    220ms;   /* standard — the cited-passage ring */
  --dur-slow:   420ms;   /* slow — declared by the ramp; no consumer yet */
  --dur-press:  90ms;    /* press — the scale(0.98) beat only */

  /* --- shadows & rings ---------------------------------------------------- */
  /* "Drop shadows, if used at all, are subtle (0–2 px blur, 10–15% opacity)." */
  --shadow:        0 1px 2px rgba(51,51,51,.12);
  --shadow-sm:     0 1px 1px rgba(51,51,51,.10);
  /* A selection ring, not a drop shadow, so the opacity rule above does not
     apply to the 3px halo — but the shadow half of it still obeys it. */
  --shadow-cited:  0 0 0 3px rgba(127,166,224,.55), 0 1px 2px rgba(51,51,51,.12);
  --ring-ok:       rgba(42,75,124,.15);
  --ring-warn:     rgba(248,153,43,.15);
  --ring-alert:    rgba(255,0,0,.15);

  /* --- washes & derived --------------------------------------------------- */
  --texture-1:     rgba(51,51,51,.022);
  --texture-2:     rgba(51,51,51,.018);
  --ok-wash:       rgba(42,75,124,.08);
  --warn-wash:     rgba(248,153,43,.08);
  --scrim:         rgba(51,51,51,.45);   /* LP Black at 45%, not pure black */
  --on-accent-mute: rgba(255,255,255,.72);
  --rule-on-accent: rgba(255,255,255,.28);

  /* Alert surfaces: Soft Pink panel, LP Red left border, LP Black message text —
     the only WCAG-safe way to spend LP Red on a 11–14px error string. */
  --alert-bg:      var(--alert-soft);
  --alert-ink:     var(--ink);

  /* Priority badges are 10px, so none of them may be red text. */
  --prio-high-bg:  var(--alert-soft);
  --prio-high-ink: var(--ink);
  --prio-med-bg:   var(--warn-soft);
  --prio-med-ink:  var(--ink);
  --prio-low-bg:   var(--paper-2);
  --prio-low-ink:  var(--ink);
}
* { box-sizing: border-box; }
html, body {
  margin: 0; padding: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 16px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
/* The guide's web scale. H1 is the only level that is LP Blue; H2 and H3 are
   LP Black. Element selectors so markup that predates a class still lands on
   the scale. No margin is set here: spacing is the sibling spec's, and Montserrat
   is vendored at 600/700/800 only, so every --display rule states its weight
   rather than letting an inherited 400 be silently down-matched to 600. */
h1, h2, h3 { font-family: var(--display); font-weight: 700; line-height: 1.2; }
h1 { font-size: 38px; font-weight: 800; color: var(--accent); }
h2 { font-size: 28px; font-weight: 700; color: var(--ink); }
h3 { font-size: 22px; font-weight: 600; color: var(--ink); }
body::before {
  content: "";
  position: fixed; inset: 0;
  background-image:
    radial-gradient(var(--texture-1) 1px, transparent 1px),
    radial-gradient(var(--texture-2) 1px, transparent 1px);
  background-size: 3px 3px, 7px 7px;
  background-position: 0 0, 1px 2px;
  pointer-events: none;
  z-index: 0;
}
a { color: var(--accent); text-decoration: none; }
button { font-family: inherit; cursor: pointer; }
/* Inter is vendored at 400/500/600. The UA default for b/strong is 700, which
   would silently down-match to 600 everywhere; state the vendored weight. */
b, strong { font-weight: 600; }

/* Deliberately a plain block, not a grid. app.js:66 prepends #boot-error here,
   and inside a block parent that is a full-width bar above everything with no
   placement rules needed. The three-zone grid lives on .shell instead, and the
   1440px cap moves to .appbar-inner and .shell so the header bar is full-bleed. */
.app {
  position: relative;
  z-index: 1;
  padding-bottom: 80px;
}

/* ===== header bar =====
   "Dark header bar in LP Blue. White content area." Full-bleed LP Blue, white
   type, and no red anywhere on it per the guide's colour-blocking rule. */
.appbar {
  background: var(--accent);
}
.appbar-inner {
  max-width: 1440px;
  margin: 0 auto;
  padding: 18px 48px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 32px;
}
/* Brand type sits on LP Blue now, so it inverts. --on-accent-mute is the guide's
   white-at-72% for the secondary line. */
.appbar .brand-text .eyebrow { color: var(--on-accent-mute); }
.appbar .brand-text .wordmark { color: var(--on-accent); }
/* app.js sets #brand-logo — and only that one — to cfg.logo_path, which is a
   tenant mark drawn for a light surface. So the chip KEEPS its white background:
   that white disc is what makes an arbitrary operator logo legible once this bar
   turned LP Blue. (#login-logo is left alone by app.js precisely so the
   white-stroke on-blue variant survives there; the two marks are not
   interchangeable, and this is why the sign-in chip is transparent and this one
   is not.) */
.appbar .brand-mark { border-color: var(--rule-on-accent); }
/* No longer a centred editorial masthead: the product name at the right of the
   bar. Same ids, same text, different role.
   38px would make the bar ~90px tall and stops being a bar; 22px collided with
   .brand-text .wordmark at the other end of the same bar, leaving the two names
   competing at one size. 26px/800 keeps a distinct top tier in a compact bar.
   This is a type-scale call inside a layout spec — flag it to the palette spec's
   owner rather than treating it as settled. */
.appbar .masthead { text-align: right; }
.appbar .masthead .title { font-size: 26px; color: var(--on-accent); }
.appbar .masthead .subtitle { margin-top: 4px; color: var(--on-accent-mute); }
/* A long company_name or activity from /api/ui-config must not push the product
   name out of the bar and give the page a horizontal scrollbar. */
.appbar .brand { min-width: 0; }
.appbar .brand-text { min-width: 0; overflow-wrap: anywhere; }

/* ===== three-zone shell =====
   The width budget is the constraint, not the aesthetics. At NFR-2's 1280px
   floor the fixed chrome is 48+48 padding + 220 sidenav + 40 gap + 40 gap + 300
   rail = 696px, leaving 584px of prose — about 72 characters at 16px Inter,
   which is a healthy measure. A 260px sidebar against the original 360px rail
   left only 484px and made the content column narrower than the two rails
   flanking it. Change any of these five numbers and re-do this arithmetic. */
.shell {
  max-width: 1440px;
  margin: 0 auto;
  padding: 0 48px;
  display: grid;
  grid-template-columns: 220px 1fr;
  gap: 40px;
  align-items: start;
}
/* Explicit placement, because the DOM order is workspace-then-sidenav: the
   question field must precede Sign-out in the tab order, and source order is the
   only thing that controls that. Placement puts the sidebar back on the left
   visually without putting it first for a keyboard. */
.sidenav   { grid-column: 1; grid-row: 1; }
.workspace { grid-column: 2; grid-row: 1; min-width: 0; }
/* The divider runs the full height of the content, so it lives on the tall
   column. On .sidenav it would stop where the controls stop — and, being
   sticky, would slide down the page as a detached stub. */
.workspace {
  align-self: stretch;
  border-left: 1px solid var(--rule);
  padding-left: 40px;
  margin-left: -40px;
}

.sidenav {
  position: sticky;
  top: 24px;
  /* Same guard the rail carries. Without it, a sidebar holding a wrapped corpus
     error, a version line, several injected model pills and a prepended
     #logout-error grows past a 800px-tall viewport and pins Sign-out
     off-screen with nothing to scroll. */
  max-height: calc(100vh - 48px);
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 24px;
  /* overflow-y: auto forces overflow-x from visible to auto, making this a
     clipping box on BOTH axes. The focus ring is drawn with box-shadow, which
     paints outside the border box, so the pills, What's new and Sign out would
     have their rings sliced off flush against the left and bottom edges.
     4px of ring plus a hair of clearance. */
  padding-top: 4px;
  padding-right: 24px;
  padding-left: 5px;
  padding-bottom: 5px;
}
/* Label on its own row, pills wrapping beneath. A wrapper div is not an option:
   app.js appends model pills straight into #model-picker. */
.sidenav .control-group {
  flex-wrap: wrap;
  gap: 8px;
}
.sidenav .control-label {
  flex: 0 0 100%;
  margin-right: 0;
  margin-bottom: 2px;
}
/* .corpus-status was right-aligned in the old header, so every alignment
   decision inverts in a left sidebar — including .corpus-error's rule, which
   DW-014 put on the right *because* the block was right-aligned. */
.sidenav .corpus-status {
  justify-self: auto;
  align-items: flex-start;
  text-align: left;
}
.sidenav .corpus-error {
  max-width: none;
  border-right: 0;
  padding-right: 0;
  border-left: 2px solid var(--alert);
  padding-left: 8px;
}
.sidenav .corpus-progress .stalled {
  border-right: 0;
  padding-right: 0;
  border-left: 2px solid var(--warn);
  padding-left: 8px;
}
.sidenav .version-line { justify-content: flex-start; }
/* Stacked, so the #logout-error banner app.js prepends here gets its own row
   rather than being squeezed beside the Sign-out button. justify-content is
   reset explicitly: the base rule packs to flex-end, which after the axis flip
   would mean "pack to the bottom" rather than "to the right". */
.sidenav .session-bar {
  flex-direction: column;
  align-items: flex-start;
  justify-content: flex-start;
  gap: 8px;
  margin-bottom: 0;
}
/* A sign-out address can be longer than the column is wide, and it is uppercase
   with letter-spacing, so it does not wrap on its own. */
.sidenav .session-user {
  max-width: 100%;
  overflow-wrap: anywhere;
}
/* The prepended banner inherits body-copy metrics that belong in the content
   column, not in a 156px-wide sidebar where every other string is 10-11px. */
.sidenav .error-banner {
  margin: 0;
  padding: 8px 10px;
  font-size: 11px;
  line-height: 1.4;
}
.brand { display: flex; align-items: center; gap: 14px; }
.brand-mark {
  width: 38px; height: 38px;
  border: 1px solid var(--rule);
  border-radius: 50%;
  background: var(--paper);
  object-fit: cover;
}
.brand-text { line-height: 1; }
.brand-text .eyebrow {
  font-family: var(--display);
  font-weight: 600;
  font-size: 10px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--ink-mute);
  margin-bottom: 4px;
}
.brand-text .wordmark {
  font-family: var(--display);
  font-weight: 700;
  font-size: 22px;
  letter-spacing: -0.01em;
}
.masthead { text-align: center; font-family: var(--display); font-weight: 600; }
.masthead .title { font-size: 38px; font-weight: 800; letter-spacing: -0.02em; line-height: 1; color: var(--accent); }
/* No Montserrat italic is vendored, and a synthetic oblique on a geometric
   display face at 38px reads as a rendering fault. Weight carries the split. */
.masthead .title em { font-style: normal; font-weight: 600; }
.masthead .subtitle {
  margin-top: 8px;
  font-family: var(--display);
  font-weight: 600;
  font-size: 10px;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--ink-mute);
}
.corpus-status {
  justify-self: end;
  display: flex;
  align-items: center;
  gap: 10px;
  font-family: var(--sans);
  font-size: 11px;
  color: var(--ink-mute);
  text-align: right;
}
.corpus-status .dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--ok);
  box-shadow: 0 0 0 3px var(--ring-ok);
}
.corpus-status.indexing .dot { background: var(--warn); box-shadow: 0 0 0 3px var(--ring-warn); }
.corpus-status.has-error .dot { background: var(--alert); box-shadow: 0 0 0 3px var(--ring-alert); }
.corpus-status .lines { line-height: 1.4; }
/* Secondary metadata, so --fg-2. This and the two other --ink-soft sites below
   (.actions .rationale, .source-where b) are the only three that move; the
   errors, the warnings and the demo banner stay at --ink deliberately —
   de-emphasising a failure is a regression, not an alignment. */
.corpus-status .lines b { color: var(--ink-soft); font-weight: 600; }
/* Indexing progress (DW-002). --ink-soft, not --ink-mute: this is 10px, so it is
   small text needing 4.5:1, and --ink-mute gives 3.45:1 — the exact failure
   DW-032 fixed in the dialogs. It is also load-bearing copy, which :root's own
   rule for --ink-mute forbids that token from carrying. */
.corpus-progress {
  margin-top: 3px;
  color: var(--ink-soft);
  font-size: 10px;
  line-height: 1.35;
}
/* A scan that has gone quiet. Same treatment as .corpus-error one step down:
   --ink for the text and a rule in the margin carrying the colour, because 10px
   of --warn as *text* is 2.1:1 on --paper and unreadable. The rule is on the
   RIGHT for the same reason .corpus-error's is — see there — and the .sidenav
   override below flips both together. */
.corpus-progress .stalled {
  display: block;
  color: var(--ink);
  border-right: 2px solid var(--warn);
  padding-right: 8px;
}
.corpus-error {
  margin-top: 3px;
  max-width: 320px;
  /* 10px, so it may not be red text. The rule carries the alert — and it is on
     the RIGHT because .corpus-status is text-align: right inside a 320px box, so
     a left rule lands up to ~250px away from the string it is marking. */
  color: var(--ink);
  border-right: 2px solid var(--alert);
  padding-right: 8px;
  font-size: 10px;
  line-height: 1.35;
  cursor: help;
}

/* A disclaimer, not a section label. It had drifted to the exact recipe used by
   .section-eyebrow and .control-label (10px / .18em / uppercase / --display), so
   it read as structure. Weight, size and a boxed rule pull it back to a stamp;
   no token value changes. The rules are --rule again, as task 1 intended — the
   --warn they had carried was an unrecorded colour change in a no-op task. */
.demo-banner {
  text-align: center;
  font-family: var(--display);
  font-weight: 700;
  font-size: 11px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink);
  /* Directly below the header bar. .app no longer caps or pads its children, so
     this and .boot-error — the only two direct children left — restate the same
     1440px cap the appbar and shell use. Without it they centre on the viewport
     while everything else centres on 1440px, and drift apart on a wide monitor. */
  max-width: 1440px;
  margin: 0 auto;
  padding: 16px 48px;
}
.demo-banner span {
  border: 1px solid var(--rule);
  border-radius: 2px;
  padding: 6px 18px;
}

/* ===== composer ===== */
.composer {
  background: var(--paper-2);
  border: 1px solid var(--rule);
  border-radius: 2px;
  padding: 28px 32px 24px;
  box-shadow: var(--shadow);
  margin-bottom: 40px;
}
.composer-label {
  font-family: var(--display);
  font-weight: 600;
  font-size: 10px;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--ink-mute);
  margin-bottom: 12px;
}
.composer-input {
  width: 100%;
  background: transparent;
  border: 0;
  outline: 0;
  resize: vertical;
  font-family: var(--sans);
  font-size: 24px;
  font-weight: 400;
  line-height: 1.4;
  color: var(--ink);
  padding: 0;
  min-height: 72px;
}
/* No Inter italic is vendored, so `font-style: italic` here would be synthetic
   oblique — the same fault rejected for Montserrat above. The mute grey already
   distinguishes placeholder from typed text. */
.composer-input::placeholder { color: var(--ink-mute); }
.composer-controls {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-top: 20px;
  padding-top: 20px;
  border-top: 1px dashed var(--rule);
  flex-wrap: wrap;
}
.control-group { display: flex; align-items: center; gap: 8px; }
.control-label {
  font-family: var(--display);
  font-weight: 600;
  font-size: 10px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--ink-mute);
  margin-right: 8px;
}
.pill {
  border: 1px solid var(--rule);
  background: transparent;
  padding: 6px 12px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 500;
  color: var(--ink-soft);
  transition: all var(--dur-fast) var(--ease-out);
  display: inline-flex; align-items: center; gap: 6px;
}
/* An inactive pill has no fill to step, so hover is carried by the border and
   the label. An ACTIVE pill does have one — and without the rule below it would
   inherit `color: var(--accent)` over its own --accent fill, i.e. blue on blue,
   because :hover:not([disabled]) (0,3,0) outranks .pill.active (0,2,0). */
.pill:hover:not([disabled]) { border-color: var(--accent); color: var(--accent); }
.pill.active {
  background: var(--accent);
  color: var(--on-accent);
  border-color: var(--accent);
}
.pill.active:hover:not([disabled]) {
  background: var(--accent-hover);
  border-color: var(--accent-hover);
  color: var(--on-accent);
}
/* The genuinely idle-disabled control — unlike .ask-btn[disabled] below, which
   is an in-flight state. 40% per the guide, applied as opacity alone with no
   additional darkening.
   Note the compounding, so it is a choice rather than an accident: .pill's
   label is --ink-soft, which this change moved from the body tier down to the
   guide's --fg-2, so a disabled pill composites to roughly 1.9:1 (it was ~2.5:1
   at 45% over the old value). WCAG 1.4.3 exempts inactive controls from the
   contrast minimum, and
   the reason a model is unavailable is carried by the title attribute rather
   than the label (app.js sets .disabled and .title together), so the label is
   an identifier here, not the explanation. */
.pill[disabled] { opacity: .4; cursor: not-allowed; }
.pill .pill-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; opacity: .6; }
.ask-btn {
  background: var(--accent);
  color: var(--on-accent);
  border: 0;
  padding: 10px 22px;
  border-radius: 2px;
  font-family: var(--sans);
  font-size: 15px;
  font-weight: 600;
  letter-spacing: .01em;
  /* box-shadow stays in this list so the :focus-visible ring at the foot of this
     file animates on the same curve as every other control's. */
  transition: transform var(--dur-press) var(--ease-out),
              background var(--dur-fast) var(--ease-out),
              box-shadow var(--dur-fast) var(--ease-out);
  display: inline-flex; align-items: center; gap: 8px;
}
/* Hover steps the fill to LP Blue -700. The halo this replaces existed only
   because the sole hover value then available was LP Black, and a fill moving
   toward charcoal travelled the same direction as [disabled]. --accent-hover is
   a blue, so the fill can carry hover again — and box-shadow is now free to mean
   one thing only on this control: focus. */
.ask-btn:hover:not([disabled]) { background: var(--accent-hover); }
/* No `box-shadow: none` here any more. It existed solely to cancel the hover
   halo removed above, and at (0,3,0) it outranked the (0,2,0) :focus-visible
   rule — so holding Space on a keyboard-focused Ask erased the focus ring for
   as long as the key was down. */
.ask-btn:active:not([disabled]) {
  background: var(--accent-pressed);
  transform: scale(0.98);
}
/* NOT the 40%-opacity disabled treatment, deliberately. #ask-btn ships enabled
   and app.js disables it only while a question is in flight, so this selector IS
   the "asking" wait state, not an idle disabled one — hence the wait cursor.
   Dimming the in-flight button to 40% over an --ink-mute fill would also breach
   the spine's own "the label must stay readable" clause. */
.ask-btn[disabled] { background: var(--ink-mute); cursor: wait; }
.ask-btn svg { width: 16px; height: 16px; }

/* ===== answer grid ===== */
/* 300px, not 360px. See the width-budget note on .shell: the sidebar took 260px
   out of the content column and the rail gave nothing back, which left the prose
   narrower than the two rails beside it. */
.answer-shell {
  display: grid;
  grid-template-columns: 1fr 300px;
  gap: 40px;
  align-items: start;
}
/* min-width:0 on .workspace only fixes the OUTER grid. #answers is itself the
   1fr track of this inner grid and carries the same auto minimum, so a single
   unbreakable token — a regulation reference, a source path — would still widen
   the page. Both levels need the guard, and the text needs somewhere to break. */
#answers {
  min-width: 0;
  overflow-wrap: anywhere;
}

/* ===== answer ===== */
.answer { background: var(--paper); position: relative; }
.answer + .answer {
  margin-top: 56px;
  padding-top: 40px;
  border-top: 1px solid var(--rule);
}
.answer-meta {
  display: flex; align-items: baseline; justify-content: space-between;
  margin-bottom: 8px;
}
/* Weight, not italic: no Inter italic face is vendored (see the placeholder
   rule above), and 500 is. */
.answer-meta .model {
  font-family: var(--sans);
  font-weight: 500;
  font-size: 14px;
  color: var(--accent);
}
.answer-meta .stats {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: .12em;
  color: var(--ink-mute);
  text-transform: uppercase;
}
.answer h2.question-echo {
  font-family: var(--display);
  font-weight: 700;
  font-size: 28px;
  line-height: 1.25;
  letter-spacing: -0.015em;
  margin: 0 0 28px;
  color: var(--ink);
  max-width: 60ch;
}
.answer h2.question-echo::before {
  content: "“";
  font-size: 56px;
  line-height: 0;
  vertical-align: -0.35em;
  color: var(--accent-soft);
  margin-right: 6px;
}
.section-eyebrow {
  font-family: var(--display);
  font-weight: 600;
  font-size: 10px;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--ink-mute);
  margin: 32px 0 12px;
  display: flex; align-items: center; gap: 12px;
}
.section-eyebrow::after { content: ""; flex: 1; height: 1px; background: var(--rule); }
.exec-summary {
  font-family: var(--sans);
  font-size: 18px;
  line-height: 1.5;
  color: var(--ink);
  border-left: 2px solid var(--accent);
  padding: 4px 0 4px 20px;
  margin: 0;
}
.analysis p {
  font-size: 16px;
  line-height: 1.5;
  color: var(--ink);
  margin: 0 0 16px;
  max-width: 68ch;
}
.analysis p:last-child { margin-bottom: 0; }

.cite {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 22px; height: 18px;
  padding: 0 5px;
  margin: 0 1px 0 2px;
  border-radius: 3px;
  background: var(--paper-2);
  border: 1px solid var(--paper-edge);
  color: var(--accent);
  font-family: var(--sans);
  font-size: 10px;
  font-weight: 500;
  vertical-align: 1px;
  cursor: pointer;
  transition: all var(--dur-fast) var(--ease-out);
}
.cite:hover { background: var(--accent); color: var(--on-accent); border-color: var(--accent); }
.cite.active { background: var(--accent); color: var(--on-accent); border-color: var(--accent); }

.actions ol { list-style: none; counter-reset: action; padding: 0; margin: 0; }
.actions li {
  counter-increment: action;
  display: grid;
  grid-template-columns: 36px 1fr auto;
  gap: 16px;
  padding: 16px 0;
  border-bottom: 1px solid var(--rule);
  align-items: start;
}
.actions li:last-child { border-bottom: 0; }
.actions li::before {
  content: counter(action, decimal-leading-zero);
  font-family: var(--display);
  font-weight: 600;
  font-size: 11px;
  color: var(--ink-mute);
  padding-top: 2px;
  letter-spacing: .1em;
}
.actions .title {
  font-family: var(--display);
  font-size: 17px;
  font-weight: 600;
  color: var(--ink);
  margin-bottom: 4px;
}
.actions .rationale {
  font-size: 14px;
  color: var(--ink-soft);
  line-height: 1.5;
}
/* The base rule is what an empty or unrecognised action.priority lands on —
   app.js writes `priority ${action.priority}` unconditionally — so it carries a
   fill, an ink and a border of its own rather than leaving bare text.
   The 1px border is the point: .low's --paper-2 fill is 1.07:1 against the white
   answer ground, so the chip shape existed only in theory. --ink-mute at 3.45:1
   gives every variant an outline without changing a single token value. */
.priority {
  font-family: var(--display);
  font-weight: 600;
  font-size: 10px;
  letter-spacing: .15em;
  text-transform: uppercase;
  padding: 4px 8px;
  border: 1px solid var(--ink-mute);
  border-radius: 2px;
  background: var(--prio-low-bg);
  color: var(--prio-low-ink);
  align-self: start;
  margin-top: 2px;
}
.priority.high   { background: var(--prio-high-bg); color: var(--prio-high-ink); }
.priority.medium { background: var(--prio-med-bg); color: var(--prio-med-ink); }
.priority.low    { background: var(--prio-low-bg); color: var(--prio-low-ink); }

.open-q {
  background: var(--paper-2);
  border: 1px dashed var(--rule);
  padding: 18px 22px;
  border-radius: 2px;
}
.open-q .q {
  font-family: var(--sans);
  font-weight: 500;
  font-size: 16px;
  color: var(--ink);
  margin-bottom: 4px;
}
.open-q .why { font-size: 13px; color: var(--ink-mute); }
.open-q + .open-q { margin-top: 14px; }

.warnings {
  margin-top: 20px;
}
/* Orange, not red. app.js builds these and .verify-note.has-warnings from the
   same payload.warnings array, so one array cannot carry two severities — and
   LP Red is reserved for alerts and required actions, never a fill on a content
   area. This was the only red *fill* left in the answer column. */
.warning-item {
  background: var(--warn-wash);
  border-left: 2px solid var(--warn);
  padding: 10px 14px;
  font-size: 13px;
  color: var(--ink);
  margin-bottom: 6px;
}
/* The scan handle: it prefixes the message and must not look like it. */
.warning-item .kind {
  font-family: var(--display);
  font-size: 10px;
  letter-spacing: .14em;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--warn);
  margin-right: 8px;
}

.reask {
  margin-top: 40px;
  padding: 20px 22px;
  background: var(--paper-2);
  border: 1px solid var(--paper-edge);
  border-radius: 2px;
  display: flex; align-items: center; justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
}
.reask-label {
  font-family: var(--sans);
  font-weight: 400;
  font-size: 14px;
  color: var(--ink-soft);
}
.reask-models { display: flex; gap: 6px; flex-wrap: wrap; }
.reask-models .pill { background: var(--paper); }

/* streaming state — the caret rides the progress line, which is where the
   streaming text actually lives (it is not a <p> inside .analysis) */
.answer.streaming .streaming-tokens::after {
  content: "▌";
  margin-left: 4px;
  animation: blink 1s steps(2) infinite;
  color: var(--accent);
}
@keyframes blink { 50% { opacity: 0; } }

/* ===== sources rail ===== */
.rail {
  position: sticky;
  top: 32px;
  align-self: start;
  max-height: calc(100vh - 64px);
  overflow-y: auto;
}
.rail-header {
  display: flex; align-items: baseline; justify-content: space-between;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--rule);
  margin-bottom: 16px;
  position: sticky; top: 0;
  background: var(--paper);
  z-index: 1;
}
.rail-title { font-family: var(--display); font-size: 22px; font-weight: 600; color: var(--ink); }
.rail-count {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--ink-mute);
}
.source {
  background: var(--paper-2);
  border: 1px solid var(--paper-edge);
  border-left: 3px solid var(--rule);
  border-radius: 2px;
  padding: 14px 16px;
  margin-bottom: 14px;
  box-shadow: var(--shadow-sm);
  scroll-margin-top: 64px;
  transition: box-shadow var(--dur-med) var(--ease-out),
              border-left-color var(--dur-fast) var(--ease-out);
}
.source.cited { border-left-color: var(--accent); }
.source.highlight {
  box-shadow: var(--shadow-cited);
}
.source-meta {
  display: flex; justify-content: space-between; align-items: baseline;
  margin-bottom: 8px;
  gap: 10px;
}
.source-id {
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 500;
  color: var(--accent);
  background: var(--paper);
  padding: 2px 6px;
  border-radius: 2px;
  border: 1px solid var(--paper-edge);
}
.source-where {
  font-family: var(--mono);
  font-size: 10px;
  color: var(--ink-mute);
  text-align: right;
  line-height: 1.4;
}
.source-where b { color: var(--ink-soft); font-weight: 500; }
.source-passage {
  font-family: var(--sans);
  font-size: 14px;
  line-height: 1.5;
  color: var(--ink);
}
.source-corpus {
  display: inline-block;
  margin-top: 8px;
  font-family: var(--display);
  font-weight: 600;
  font-size: 9px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--ink-mute);
}

.verify-note {
  margin-top: 24px;
  padding: 12px 14px;
  background: var(--ok-wash);
  border-left: 2px solid var(--ok);
  font-size: 12px;
  color: var(--ink-soft);
  font-family: var(--sans);
  line-height: 1.5;
}
/* Emphasis follows severity, not the other way round. The healthy state was the
   coloured one and the warning state was plain, so the note that needed reading
   drew the least eye. Healthy is now plain bold ink; the warning headline takes
   the severity colour of its own rule plus an underscore, so it carries a shape
   as well as a hue and does not depend on --warn's contrast alone. */
.verify-note b { color: var(--ink); font-weight: 600; }
.verify-note.has-warnings { background: var(--warn-wash); border-left-color: var(--warn); }
.verify-note.has-warnings b {
  color: var(--warn);
  font-weight: 600;
  border-bottom: 2px solid var(--warn);
}

/* boot failure — the API is unreachable or misrouted.
   app.js:66 prepends this into .app, which is why .app is a block rather than a
   grid: the bar then lands above the header bar with no placement rule at all.
   Capped at 1440px like every other zone, so its alert rule stays beside the
   content it refers to instead of against the screen edge on a wide monitor. */
.boot-error {
  max-width: 1440px;
  margin: 0 auto;
  padding: 12px 48px;
  background: var(--alert-bg);
  border-left: 3px solid var(--alert);
  color: var(--alert-ink);
  font-family: var(--sans);
  font-size: 12px;
  line-height: 1.5;
}

/* error & loading states */
.error-banner {
  margin: 20px 0;
  padding: 14px 18px;
  background: var(--alert-bg);
  border-left: 3px solid var(--alert);
  font-size: 14px;
  color: var(--alert-ink);
  font-family: var(--sans);
}
/* border-left is --ink-mute again, as task 1 intended; the --accent-soft it had
   been carrying was an unrecorded colour change inside a declared no-op. */
.streaming-tokens {
  font-family: var(--sans);
  font-weight: 400;
  color: var(--ink-mute);
  font-size: 15px;
  line-height: 1.65;
  padding: 16px;
  background: var(--paper-2);
  border-left: 2px solid var(--ink-mute);
}

/* ===== view switching =====
   One utility, used by the login/app swap. Everything else in this file toggles
   style.display ad hoc; new view-level state should use this instead. */
.hidden { display: none !important; }

/* A node present for assistive technology and for nothing else.
   **Deliberately not `display: none` and not `visibility: hidden`** — both take
   the node out of the accessibility tree, and a live region that is out of the
   tree announces nothing, which is the defect `#documents-remove-live` exists to
   fix. The clip-rect form is the one that leaves the node rendered, and therefore
   announced, while occupying no space anybody can see.
   `white-space: nowrap` is part of the recipe, not decoration: without it a long
   sentence wraps inside the 1px box and some engines grow it. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ===== login, and the shared field-label pair ===== */
/* The sign-in view is the one full-bleed surface in the product, so it takes the
   guide's full-bleed treatment: LP Blue ground, white Montserrat wordmark, white
   type throughout. The card is therefore a layout box only — giving it a white
   fill would put the wordmark back on white and lose the treatment entirely.
   NOT everything below is sign-in-only: `.field-label` / `.field-label-on-accent`
   are shared with the admin and upload dialogs, which sit on --paper. Anything
   here that reads "white because the ground is LP Blue" must be checked against
   that before it is reused — assuming otherwise is what caused DW-032. */
.login-screen {
  position: relative;
  z-index: 1;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 48px 24px;
  background: var(--accent);
  color: var(--on-accent);
}
.login-card {
  width: 100%;
  max-width: 380px;
  background: transparent;
  border: 0;
  border-radius: 2px;
  box-shadow: none;
  padding: 32px 32px 28px;
}
/* The mark is the white-stroke variant here, so the disc must not carry the
   light-surface fill and border it uses in the app header. */
.login-brand .brand-mark {
  background: transparent;
  border-color: var(--rule-on-accent);
}
.login-brand {
  display: flex;
  align-items: center;
  gap: 14px;
  padding-bottom: 20px;
  margin-bottom: 24px;
  border-bottom: 1px solid var(--rule-on-accent);
}
.login-brand .eyebrow {
  font-family: var(--display);
  font-weight: 600;
  font-size: 10px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--on-accent-mute);
  margin-bottom: 4px;
}
.login-title {
  font-family: var(--display);
  font-size: 28px;
  font-weight: 800;
  line-height: 1;
  color: var(--on-accent);
}
/* Was oxblood on cream. On LP Blue it may be neither red (the guide forbids the
   pairing outright) nor blue (invisible), so the split is carried by weight. */
.login-title em { font-style: normal; font-weight: 600; color: var(--on-accent); }
/* Field labels are the ONE thing in this section that is not sign-in-only, so
   they do not carry a `.login-` name. This class was `.login-label`, written for
   the two fields on the LP Blue gate and therefore white — and then reused by
   eight fields inside `.modal-dialog`, whose fill is --paper. White on white:
   not low contrast, the same colour, composited to nothing. A super_admin
   provisioning a board saw four unlabelled boxes, three of which are written
   into the system prompt of every answer that board receives and none of which
   can be edited afterwards (DW-032).

   The fix is the NAME, not the value. A surface-specific colour on a
   surface-neutral class is a trap the next reuse walks into as well, so the two
   surfaces are now spelled out and neither is the default:

     .field-label            on --paper   (the eight modal fields)
     .field-label-on-accent  on LP Blue   (the two sign-in fields)

   Typography is grouped so the two cannot drift in size or tracking; only the
   colour differs. Do not fold either into the other, and do not add a
   `.modal .field-label` override — that would fix by location instead of by
   meaning and re-arm the trap. */
.field-label,
.field-label-on-accent {
  display: block;
  font-family: var(--display);
  font-weight: 600;
  font-size: 10px;
  letter-spacing: .16em;
  text-transform: uppercase;
  margin-bottom: 6px;
}
/* --ink, not --ink-mute. These are 10px uppercase, so WCAG's large-text
   exemption does not apply and the 4.5:1 threshold is the real one: --ink-mute
   is #8A8A8A on #FFFFFF, about 3.45:1, and fails. --ink is 12.6:1.
   styles.test.mjs asserts the ratio so the wrong answer cannot land.

   Do NOT read the neighbouring .admin-hint as a licence to reuse --ink-mute
   here. An earlier version of this comment claimed that hint was "correct to"
   use it because it is "body-sized"; that was wrong twice over. "Body-sized" is
   not a WCAG category, and .admin-hint is 12px — small text, so it needs 4.5:1
   and gets 3.45:1 like everything else on this token. It is a real failure, not
   a sanctioned exception, and it is load-bearing: the hints at index.html:305
   and :318 carry the ONLY statement that a tenant id and a board's identity
   cannot be changed after provisioning, which is exactly what :root says
   --ink-mute must never do ("must never carry the sole copy of something the
   reader needs"). Tracked as deferred work; not fixed here because it is a
   different element with its own design question. */
.field-label { color: var(--ink); }
.field-label-on-accent { color: var(--on-accent-mute); }
.login-input {
  width: 100%;
  padding: 10px 12px;
  margin-bottom: 18px;
  border: 1px solid var(--paper-edge);
  border-radius: 2px;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 16px;
}
/* The focused border darkens the field's own edge against its white fill. This
   is NOT a sign-in-gate detail, whatever an earlier version of this comment
   said: .login-input is used by nine fields, eight of them inside modals on
   --paper, plus one built in app.js:1457. The value is surface-neutral so
   nothing renders wrongly — but the name still says otherwise, which is the
   same trap .field-label was extracted from (see the section header).
   The ring itself is NOT special-cased here: the uniform three-band ring at the
   foot of this file already survives the full-bleed LP Blue ground, because its
   OUTER band is --paper. */
.login-input:focus-visible { border-color: var(--accent); }
/* Inverted against the blue ground: LP Blue on white, never blue on blue. */
.login-submit {
  width: 100%;
  justify-content: center;
  background: var(--paper);
  color: var(--accent);
}
/* Hover and press are carried by the FILL, never by box-shadow. An earlier
   attempt used a white halo here and had to guard it with :not(:focus-visible)
   so it would not paint over the focus ring — but that guard handed the fill
   back to .ask-btn:hover the moment the button was focused, painting a dark
   blue under this button's --accent label at about 1.3:1. Keeping box-shadow
   off this control entirely removes both problems: the focus ring is then the
   only box-shadow it ever draws, so nothing can cancel it. */
/* .login-submit inverts .ask-btn — light fill, accent label — so it must also
   override .ask-btn's *state* fills, not just the resting one. Without these,
   .ask-btn:hover (0,3,0) and .ask-btn:active (0,3,0) paint a dark fill under
   this button's --accent label: roughly 1.3:1 on hover and 1.4:1 while pressed.
   Both are (0,4,0) so they win, and neither writes box-shadow — the focus ring
   is the only box-shadow on this control, so nothing can cancel it and no
   :not(:focus-visible) guard is needed. */
.login-submit:hover:not([disabled]) {
  background: var(--on-accent-mute);
}
.login-submit:active:not([disabled]) {
  background: var(--on-accent-mute);
  color: var(--accent);
  transform: scale(0.98);
}
/* The button ships disabled and app.js enables it, so this is the state the page
   is in whenever the module did not run — it must read as "not yet", not broken.
   It also has to outrank .ask-btn[disabled] (0,2,0), which otherwise painted
   --ink-mute under --accent text and multiplied the pair by opacity .55 to
   ~1.65:1; .ask-btn.login-submit[disabled] is (0,3,0) and wins outright.
   The fill is 72% white over LP Blue, so the label reads at 5.3:1 and the button
   is visibly tinted against the pure-white enabled state. No opacity.
   Its `box-shadow: none` cannot cancel a focus ring: a disabled button is not
   focusable, so :focus-visible never matches while this rule applies. */
.ask-btn.login-submit[disabled],
.ask-btn.login-submit:disabled {
  opacity: 1;
  background: var(--on-accent-mute);
  color: var(--accent);
  box-shadow: none;
  cursor: not-allowed;
}
.login-noscript {
  display: block;
  margin-top: 12px;
  font-family: var(--sans);
  font-size: 12px;
  line-height: 1.5;
  color: var(--on-accent);
}
.login-error {
  margin: 0 0 18px;
  padding: 10px 12px;
  background: var(--alert-bg);
  border-left: 3px solid var(--alert);
  color: var(--alert-ink);
  font-family: var(--sans);
  font-size: 12px;
  line-height: 1.5;
}

/* ===== session bar ===== */
.session-bar {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 12px;
  margin-bottom: 20px;
}
.session-user {
  font-family: var(--display);
  font-weight: 600;
  font-size: 10px;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-mute);
}
.logout-btn {
  background: none;
  border: 1px solid var(--rule);
  border-radius: 2px;
  padding: 4px 10px;
  font-family: var(--display);
  font-weight: 600;
  font-size: 10px;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.logout-btn:hover { border-color: var(--accent); color: var(--accent); }

/* ===== version badge + history modal (DW-016) ===== */
/* Lives inside .corpus-status, which is already mono/11px/right-aligned, so the
   badge inherits rather than restating the type. DW-015 moved this whole zone
   into the sidebar; the .sidenav overrides above handle the alignment flip. */
.version-line {
  margin-top: 4px;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
  font-size: 10px;
  letter-spacing: .08em;
}
#app-version { color: var(--ink-mute); }
/* A button, not a link: it opens an overlay rather than navigating anywhere. */
.whats-new {
  background: none;
  border: 0;
  padding: 0;
  font-family: var(--display);
  font-weight: 600;
  font-size: 10px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--accent);
  border-bottom: 1px solid transparent;
}
.whats-new:hover { border-bottom-color: var(--accent); }
/* Focus lives in the one uniform block at the foot of this file — this control
   used to carry a treatment of its own, which is exactly what made the app's
   focus indicator inconsistent from screen to screen. */

.modal {
  position: fixed;
  inset: 0;
  z-index: 50;               /* above .app (z-index 1) and the body texture */
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 32px;
}
.modal-backdrop {
  position: absolute;
  inset: 0;
  background: var(--scrim);
}
.modal-dialog {
  position: relative;        /* above the backdrop, same stacking context */
  width: min(680px, 100%);
  max-height: 82vh;
  display: flex;
  flex-direction: column;
  background: var(--paper);
  border: 1px solid var(--paper-edge);
  border-radius: 4px;
  box-shadow: var(--shadow);
}
.modal-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  padding: 20px 24px 12px;
  border-bottom: 1px solid var(--rule);
}
.modal-title {
  margin: 0;
  font-family: var(--display);
  font-size: 22px;
  font-weight: 600;
  color: var(--ink);
  letter-spacing: -0.01em;
}
.modal-close {
  background: none;
  border: 0;
  padding: 0 4px;
  font-size: 22px;
  line-height: 1;
  color: var(--ink-mute);
}
.modal-close:hover { color: var(--ink); }
/* The only scroll container — the dialog itself is capped at 82vh so a long
   history scrolls inside the overlay rather than the page behind it. */
.modal-body {
  overflow-y: auto;
  padding: 4px 24px 24px;
  font-size: 14px;
}
/* The renderer enables `extra`, so a release note may legitimately contain a
   table or a fenced code block. Neither wraps, and the dialog is a fixed
   min(680px, 100%) — so without their own horizontal scrollers they are clipped
   and unreadable rather than merely wide. */
.modal-body table, .modal-body pre {
  display: block;
  max-width: 100%;
  overflow-x: auto;
}
.modal-body table { border-collapse: collapse; }
.modal-body th, .modal-body td {
  border: 1px solid var(--rule);
  padding: 4px 8px;
  text-align: left;
}
.modal-body pre {
  background: var(--paper-2);
  padding: 10px 12px;
  font-family: var(--mono);
  font-size: 12px;
}
.modal-body code { font-family: var(--mono); font-size: 12px; }
.modal-body h1 { display: none; }   /* the dialog is already titled */
.modal-body h2 {
  font-family: var(--display);
  font-size: 17px;
  font-weight: 600;
  color: var(--ink);
  margin: 22px 0 8px;
}
.modal-body h2:first-of-type { margin-top: 8px; }
/* A `###` in a release note was falling through to the global h3 (22px) inside
   14px modal prose — a child heading larger than its own parent. Sized below the
   h2 above it. Spacing is left to the UA so this stays a type rule. */
.modal-body h3 {
  font-family: var(--display);
  font-size: 15px;
  font-weight: 600;
  color: var(--ink);
}
.modal-body p { margin: 0 0 10px; }
.modal-body ul { margin: 0 0 12px; padding-left: 20px; }
.modal-body li { margin-bottom: 4px; }
.modal-body hr { border: 0; border-top: 1px solid var(--rule); margin: 20px 0; }
.modal-body strong { color: var(--ink); font-weight: 600; }
/* Markdown `*emphasis*` would inherit the UA's italic and be synthesised, since
   no Inter italic is vendored. Colour and weight carry it instead, as they do
   for every other em in this file. */
.modal-body em { color: var(--ink-mute); font-style: normal; font-weight: 500; }
/* An error line inside prose: the rule carries the alert, the text stays legible. */
.modal-error { color: var(--ink); border-left: 2px solid var(--alert); padding-left: 10px; }

/* ===== user administration (DW-001 goal 5) =====
   The dialog reuses .modal / .modal-dialog / .modal-body wholesale, so nothing
   here restates the overlay, the scrim, the head or the scroll container. What
   follows is only the two sections inside it, plus the sidebar trigger. */

/* The trigger sits in the sidebar with the other session controls, and is sized
   as one of those rather than as a primary action: administering users is
   occasional, and a button competing with Ask would misdescribe the screen. */
.manage-users {
  margin-top: 16px;
  background: none;
  border: 1px solid var(--rule);
  border-radius: 3px;
  padding: 7px 10px;
  font-family: var(--display);
  font-weight: 600;
  font-size: 11px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-mute);
}
.manage-users:hover { color: var(--ink); border-color: var(--ink-mute); }

.admin-section { margin-bottom: 24px; }
.admin-section:last-child { margin-bottom: 0; }

/* One row per account. Grid rather than a table: the remove control is a third
   column of variable width and is absent more often than not — for every
   administrator, every removed account, and the caller's own row — so the row
   has to stay readable without it. */
.admin-user {
  display: grid;
  grid-template-columns: 1fr auto auto;
  align-items: center;
  gap: 12px;
  padding: 8px 0;
  border-bottom: 1px solid var(--rule);
  font-size: 13px;
}
.admin-user:last-child { border-bottom: 0; }
.admin-user-email { color: var(--ink); overflow-wrap: anywhere; }
.admin-user-role { font-family: var(--mono); font-size: 11px; color: var(--ink-mute); }
/* A removed account stays listed — that is what a status is for — but must not
   read as a live one at a glance. */
.admin-user.is-removed .admin-user-email { color: var(--ink-mute); }
.admin-user-status {
  font-family: var(--display);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink-mute);
}
.admin-user.is-removed .admin-user-status { color: var(--alert); }

.admin-remove {
  background: none;
  border: 0;
  padding: 0;
  font-family: var(--display);
  font-weight: 600;
  font-size: 10px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--alert);
  border-bottom: 1px solid transparent;
}
.admin-remove:hover { border-bottom-color: var(--alert); }
.admin-remove[disabled] { color: var(--ink-mute); border-bottom-color: transparent; }

/* The register's Deactivate control. It looks like `.admin-remove` on purpose —
   both are destructive-looking text buttons in a row of the same dialog — and it
   is deliberately NOT that class: one withdraws one person's access within a
   board, this one retires a whole board for the whole instance, and a later
   restyle of "the remove button" must not silently restyle this. */
.admin-deactivate {
  background: none;
  border: 0;
  padding: 0;
  font-family: var(--display);
  font-weight: 600;
  font-size: 10px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--alert);
  border-bottom: 1px solid transparent;
}
.admin-deactivate:hover { border-bottom-color: var(--alert); }
.admin-deactivate[disabled] { color: var(--ink-mute); border-bottom-color: transparent; }

/* The confirmation sits INSIDE the row it is about, so the question and the name
   it names cannot be separated by a scroll. */
.admin-confirm {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px 0 2px;
  font-size: 12px;
  color: var(--ink);
}
.admin-confirm-text { flex: 1; }

.admin-form .login-input { margin-bottom: 12px; }
.admin-form .ask-btn { margin-top: 4px; }

.admin-error {
  color: var(--ink);
  border-left: 2px solid var(--alert);
  padding-left: 10px;
  margin-bottom: 12px;
  font-size: 13px;
}
.admin-empty { color: var(--ink-mute); font-size: 13px; }

/* Confirmation of a create. Deliberately not an error's opposite in colour
   alone — the rule carries the distinction, so it survives a forced-colors mode
   and a monochrome screen. */
.admin-success {
  color: var(--ink);
  border-left: 2px solid var(--ink-mute);
  padding-left: 10px;
  margin-bottom: 12px;
  font-size: 13px;
}

.admin-field { margin-bottom: 4px; }
.admin-hint {
  color: var(--ink-mute);
  font-size: 12px;
  margin: -6px 0 12px;
}

/* One row per recorded action. Same grid discipline as the member list: the
   timestamp is fixed-width and the identities wrap. */
.admin-action {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 4px 12px;
  padding: 8px 0;
  border-bottom: 1px solid var(--rule);
  font-size: 13px;
}
.admin-action:last-child { border-bottom: 0; }
.admin-action-what { color: var(--ink); overflow-wrap: anywhere; }
/* `.admin-action.is-refused` lived here, dimming a refused attempt among the
   changes. It went with the rows it styled: the trail records effects, so every
   row in this list is something that happened and there is no second kind left to
   tell apart.

   DW-020 reopens that ground on the same class, and the answer to "why is a state
   variant on `.admin-action` justified this time" is that this list DOES have two
   kinds of row. The register shows documents, and a document the member has
   withdrawn stays listed until the owner's next scan reconciles it — that window
   is the feature's central compromise, and a row inside it that renders
   identically to a live one tells the member their removal did not work.

   The other half of why the earlier rule died: it was never applied. The renderer
   hard-assigned `el.className = "admin-action"`, so no element ever carried the
   variant. `.is-withdrawn` below is written by `documentRowClass(row)` in
   `documents.js` — a pure function of the row, asserted in `documents.test.mjs`
   — and that the renderer calls it is the manual check the spec records, because
   there is no jsdom here to see a class land on a node. */
.admin-action-when {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--ink-mute);
  white-space: nowrap;
}
.admin-action-meta {
  grid-column: 1 / -1;
  font-family: var(--mono);
  font-size: 11px;
  color: var(--ink-mute);
  overflow-wrap: anywhere;
}

/* ─── a document row's action slot and its withdrawn state (DW-020) ────────── */

/* The Remove control's slot.
   **`grid-template-columns` is deliberately NOT widened.** `.admin-action` is
   also the register's column header row (`index.html`, "Document / Last
   changed"), and a third track would put a 12px gap after the second one on that
   row too — moving the heading out of line with the rows it heads, which is the
   one thing this slice was told not to do. The control is placed on an implicit
   row instead, in the second column, which the header row does not have and
   therefore never grows. */
.admin-action-controls {
  grid-column: 2;
  justify-self: end;
  align-self: center;
  display: flex;
  gap: 12px;
  align-items: center;
}

/* A withdrawn row: modelled on `.admin-user.is-removed`, which dims a removed
   account's address while leaving it listed. The strike-through is the part that
   is not a colour, so the distinction survives a forced-colors mode and a
   monochrome screen — and the row also carries the state IN WORDS, because a
   colour and a line through a name are invisible to a screen reader. */
/* **`--ink-soft`, not `--ink-mute`, and that is a WCAG fix rather than a taste
   one.** The first attempt dimmed the name to `--ink-mute` #8A8A8A: 3.45:1 on
   --paper and 3.24:1 on --paper-2, against the 4.5:1 AA floor for 13px text —
   in the one row a member most needs to read, because it is the row they are
   asking "did my removal work?" about. This is DW-032's exact shape, in a repo
   that keeps `styles.test.mjs` for precisely that, and the ratios below are now
   asserted there. #5A5A5A is 6.90:1 on --paper and 6.47:1 on --paper-2, still
   visibly quieter than --ink's 12.6:1.
   The strike-through carries the distinction that is not a colour, so the row
   survives a forced-colors mode and a monochrome screen — and the row also
   carries the state IN WORDS below, because neither a colour nor a line through
   a name reaches a screen reader. */
.admin-action.is-withdrawn .admin-action-what {
  color: var(--ink-soft);
  text-decoration: line-through;
}
/* The state in words. **`--alert-hover`, not `--alert`**, for the reason above:
   #FF0000 is 4.00:1 on --paper and 3.75:1 on --paper-2, and this is 10px
   uppercase — the small-text floor of 4.5:1 applies with nothing to argue about.
   #C40000 is 6.27:1 and 5.89:1 and stays inside the red family the alert tokens
   define, so the label still reads as the same kind of mark as
   `.admin-action-flag` beside it.
   `.admin-action-flag` keeps --alert and is NOT corrected here: it is a
   pre-existing surface this slice does not own, and it is recorded rather than
   folded in silently. */
.admin-action-state {
  grid-column: 1 / -1;
  font-family: var(--display);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--alert-hover);
}
/* An action taken by the provider rather than by this board. Marked, because
   "was that us?" is the first question a reader has about any row here, and it
   is the whole reason this panel ships in this release. */
.admin-action.is-provider { border-left: 2px solid var(--alert); padding-left: 10px; }
.admin-action-flag {
  font-family: var(--display);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--alert);
}

/* ===== narrowed desktop window (DW-015) =====
   The only WIDTH media query in this stylesheet — the other two, at the foot of
   the file, query a user preference (forced-colors, prefers-reduced-motion)
   rather than a viewport. This one is deliberately scoped to a resized
   desktop window rather than to devices: `meta viewport` stays pinned at 1280,
   so a phone renders the wide layout scaled and never reaches this breakpoint.
   That is intentional — spec.md R13 is "desktop only", NFR-2 specifies 1280x800
   or larger, and mobile responsive design is on the explicit out-of-scope list.
   This exists so dragging a window narrow stays usable, not to support mobile.

   1180px, NOT 900px. The three-column layout's fixed chrome is 96 padding + 220
   sidenav + 40 + 40 gaps + 300 rail = 696px. At 900px that leaves 204px of
   prose, and at 901px — one pixel above a 900px breakpoint — it leaves 205px.
   A breakpoint only helps if it fires before the layout it guards has already
   failed, so it belongs just under the width where the measure gets tight. */
@media (max-width: 1180px) {
  .appbar-inner {
    flex-direction: column;
    align-items: flex-start;
    gap: 10px;
    padding: 14px 24px;
  }
  .appbar .masthead { text-align: left; }

  /* Sidebar above content, rail below answers. The DOM order is workspace-then-
     sidenav for tab order, so both are placed explicitly here too — otherwise
     the controls would land below the composer. */
  .shell {
    grid-template-columns: 1fr;
    gap: 24px;
    padding: 20px 24px 0;
  }
  .sidenav   { grid-column: 1; grid-row: 1; }
  .workspace {
    grid-column: 1;
    grid-row: 2;
    /* The divider is a two-column device; there is one column now. */
    border-left: 0;
    padding-left: 0;
    margin-left: 0;
  }
  .sidenav {
    position: static;
    /* No sticky column to overflow inside, so release the height guard. */
    max-height: none;
    overflow-y: visible;
    /* Horizontal again at this width — a 100%-wide column of stacked pills
       wastes the whole viewport. */
    flex-direction: row;
    flex-wrap: wrap;
    gap: 20px 32px;
    padding-right: 0;
  }
  .sidenav .control-group { flex: 1 1 240px; }
  /* Back to a row, but the prepended #logout-error still needs its own line —
     otherwise the squeezed-banner problem the desktop rule solves reappears here. */
  .sidenav .session-bar { flex-direction: row; align-items: center; flex-wrap: wrap; }
  .sidenav .error-banner { flex: 1 1 100%; }

  .answer-shell { grid-template-columns: 1fr; gap: 32px; }
  /* Sticky positioning with no side column left to stick beside would pin the
     rail over the answers as they scroll. overflow must go back to visible too,
     or .rail-header sticks to a scrollport that no longer scrolls. */
  .rail { position: static; max-height: none; overflow: visible; }

  .demo-banner { padding: 16px 24px; }
  .boot-error { padding: 12px 24px; }
}

/* ===== focus (DW-025) =====
   One treatment, every interactive control. The page's interactive set is
   exactly <button>, <input>, one <textarea> and one `tabindex="0"` scroll region
   — no <a> and no <select> — so this list is the whole focusable surface. (.cite
   is listed for completeness but is a <span>: it carries a :hover rule and a
   pointer cursor without being reachable by keyboard at all. Making it focusable
   is a markup change and therefore out of scope here.)

   The ring is THREE-BAND and each band answers a different ground. Two earlier
   attempts failed here, so the reasoning is worth keeping:
     - A single accent ring vanished on the controls that are themselves filled
       --accent (.ask-btn, .pill.active) — accent on accent reads as the control
       being 2px larger, not as a ring.
     - A two-band paper-then-accent ring fixed those and then vanished on the
       sign-in gate, where a --paper control sits on a full-bleed --accent page:
       white band on a white button, accent band on an accent page.
   The middle accent band is what reads on ordinary light surfaces; the inner
   paper band separates it from an accent fill; the outer paper band separates
   it from an accent page. Do not drop a band or reorder them.

   Deliberately NOT the guide's 3px Light-Blue-at-55% halo. That exact value is
   --shadow-cited, the cited-passage highlight, and both appear at once on the
   answer screen: adopt the guide literally and "where I am" becomes
   indistinguishable from "what was cited" on the one screen the product exists
   for. A banded hard ring and a soft single-tone halo stay plainly different.

   `outline: none` is permitted here only because something stronger provably
   replaces it — and only because the forced-colors block below restores a real
   outline where box-shadow is not painted at all. Drawn as box-shadow so the
   indicator costs no layout.

   Nothing may out-specify this rule and blank it. The two rules that did have
   been fixed at their source, both by removing box-shadow rather than by
   juggling specificity: .ask-btn:active no longer sets it, and .login-submit
   carries hover and press on the fill instead of a halo. */
.pill:focus-visible,
.ask-btn:focus-visible,
.cite:focus-visible,
.login-submit:focus-visible,
.logout-btn:focus-visible,
.composer-input:focus-visible,
.login-input:focus-visible,
.whats-new:focus-visible,
.manage-users:focus-visible,
.admin-remove:focus-visible,
.admin-deactivate:focus-visible,
/* Not a control: the document register's list, made a tab stop so a keyboard-only
   reader can scroll a register longer than the dialog — its rows contain nothing
   focusable, and nothing focused is nothing the arrow keys scroll. It is in this
   list for the same reason everything else is: a tab stop with no visible
   indicator is a reader who cannot tell where they are. */
.documents-list:focus-visible,
.modal-close:focus-visible {
  outline: none;
  /* Three bands, each answering a different ground. The cases that actually
     occur in this app:

       light control, light page   (.pill, .composer-input; transparent over
                                    --paper or --paper-2)  -> middle accent reads
       accent-filled control       (.ask-btn, .pill.active) -> inner paper reads
       light control, ACCENT page  (.login-submit, .login-input on the sign-in
                                    gate)                   -> outer paper reads

     The last row is why the outer band exists: a ring of paper-then-accent puts
     a white band on a white button and an accent band on an accent page, and
     vanishes completely. Note the outer band is near-invisible against
     --paper-2 (they differ by a few percent) — that is fine, because on those
     surfaces the middle accent band is already doing the work. Do not drop a
     band or reorder them. */
  box-shadow: 0 0 0 1px var(--paper),
              0 0 0 3px var(--accent),
              0 0 0 4px var(--paper);
}

/* box-shadow is not painted in forced-colors mode, so the ring above simply
   does not exist there and `outline: none` would leave nothing behind. Restore
   a real outline in the system's own text colour — the one place this file is
   allowed to hand the decision back to the OS.

   The selector list is repeated in full rather than wrapped in :where(). A
   media query contributes no specificity, so this block wins only by matching
   the (0,2,0) of the rule above and coming later in the file. :where() would
   make it (0,1,0), lose to `outline: none`, and leave forced-colors users with
   no focus indicator at all. */
@media (forced-colors: active) {
  .pill:focus-visible,
  .ask-btn:focus-visible,
  .cite:focus-visible,
  .login-submit:focus-visible,
  .logout-btn:focus-visible,
  .composer-input:focus-visible,
  .login-input:focus-visible,
  .whats-new:focus-visible,
  .manage-users:focus-visible,
  .admin-remove:focus-visible,
  .admin-deactivate:focus-visible,
  .documents-list:focus-visible,
  .modal-close:focus-visible {
    outline: 2px solid CanvasText;
    outline-offset: 2px;
  }
}

/* ===== reduced motion =====
   Collapses every duration to effectively zero rather than removing the
   properties, so state changes still land — they simply arrive instantly.
   `animation-iteration-count: 1` is what actually stops an infinite loop; a
   short duration alone would only make it spin faster. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    transition-duration: 0.01ms !important;
    transition-delay: 0ms !important;
    animation-duration: 0.01ms !important;
    animation-delay: 0ms !important;
    animation-iteration-count: 1 !important;
    scroll-behavior: auto !important;
  }
  /* @keyframes blink, explicitly. The streaming caret is the stylesheet's only
     animation and the only motion a reader cannot look away from, so it is
     stopped outright rather than merely sped up. The caret itself stays — the
     glyph is the streaming signal; the flashing was only its decoration. */
  .answer.streaming .streaming-tokens::after { animation: none; }
}

/* The per-member storage allowance control (DW-020). Sized to the number it
   holds — a whole-megabyte figure — rather than stretching like the login inputs
   it borrows its styling from, because it sits inside a table-like row. */
.admin-allowance {
  width: 6em;
  margin: 0 8px 0 0;
  padding: 4px 8px;
}
