/* Component rules for the dossier engine's screens (#5).

   Every colour, size, radius, spacing and duration below resolves through a custom property in
   tokens.css. There is no raw hex and no one-off type or spacing literal, which is what makes an
   agency's branding a variable swap rather than a fork of this file.

   #8 absorbs this into the one screen. It is written as a shared component grammar — button,
   field, card, heading, rail — rather than as one page's stylesheet, so that absorption is a
   move rather than a rewrite. */

*, *::before, *::after { box-sizing: border-box; }

body {
  margin: 0;
  /* Top padding lives on the top bar now, so the hairline sits close to the window edge. */
  padding: 0 var(--space-4) var(--space-8);
  background: var(--background);
  color: var(--text-primary);
  font-family: var(--font-body);
  font-size: var(--text-note);
  line-height: 1.5;
}

main {
  max-width: var(--max-width);
  margin: 0 auto;
}

/* ── type ──────────────────────────────────────────────────────────────────────────────── */

/* Weight 600, not 500 and not the browser's 700. Geist ships 400 and 600 only (fonts.css), and
   the CSS font-matching algorithm resolves a desired weight between 400 and 500 DOWNWARD — so a
   500 heading would render at regular weight rather than snapping up to the 600 sitting right
   there. This read 500 while a single-500-face family carried the UI; that family is gone, and
   600 is now the file's one bold, shared with .claim-requirement and .client-name.

   The page title takes the ramp's top step from #59 (--text-display, 35px; it was --text-h2's
   24px, four points above a subheading, which is not a ramp). This is SHARED CHROME and the
   blast radius is deliberate: `h1` is a bare element selector, so /, /clients, /counts and
   404.html all take it — epic AC1's "one token set drives both surfaces" working as intended.
   The portal's two role-title pages are unaffected, because prep.css already overrides
   `.brief .page-head h1` / `.session .page-head h1` to --text-h1. The letter-spacing is the
   optical tracking correction display sizes need, the same value and the same reason as
   prep.css's own top-step heading. The phone step-down is in the mobile-density block. */
h1 {
  font-family: var(--font-ui);
  font-size: var(--text-display);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.2;
  margin: 0 0 var(--space-4);
  text-wrap: balance;
}

h2 {
  font-family: var(--font-ui);
  font-size: var(--text-h3);
  font-weight: 600;
  line-height: 1.2;
  margin: 0;
  text-wrap: balance;
}

p { margin: 0 0 var(--space-4); }
p:last-child { margin-bottom: 0; }

.muted { color: var(--text-muted); }

code {
  font-family: var(--font-mono);
  font-size: var(--text-body);
}

/* --accent measures 3.88:1 on --background, which still fails the 4.5:1 body-text floor, so link
   text is --text-primary and the accent carries the underline instead. */
a {
  color: var(--text-primary);
  text-decoration-color: var(--accent);
  text-underline-offset: var(--space-1);
}

/* ── focus, one rule for everything ────────────────────────────────────────────────────── */

:focus-visible {
  outline: var(--focus-width) solid var(--accent);
  outline-offset: var(--focus-inner);
  /* The hairline that fills the offset gap. The CONCLUSION here changed with the repalette and
     the rule survives it: --accent is now 3.53:1 against --surface and clears the 3:1 non-text
     floor on its own, which the blue it replaced did not. The hairline stays anyway,
     because the ring must also read on the tints — --accent is 2.57:1 on --tint-info, and #60
     puts focusable controls there. */
  box-shadow: 0 0 0 var(--focus-inner) var(--text-primary);
}

/* ── the top bar ───────────────────────────────────────────────────────────────────────── */

/* Shared by both screens; the hairline is what separates chrome from work. Not sticky: the
   pages scroll long in act 3 and chrome that follows would sit over the pack. */
.topbar {
  border-bottom: 1px solid var(--border-hairline);
  padding: var(--space-2) 0;
  margin-bottom: var(--space-8);
}

.topbar-inner {
  max-width: var(--max-width);
  margin: 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  flex-wrap: wrap;
}

.topbar-brand {
  font-family: var(--font-ui);
  font-size: var(--text-body);
  color: var(--text-muted);
}

/* The nav wraps, which .topbar-inner above already does and this did not. Measured at a 360px
   viewport: the four links are 359px wide against a 328px content box (body padding is
   --space-4 a side), so the nav overflowed the page by 15px on all three screens — a horizontal
   page scroll on every phone, and the two /clients probes that measure scrollWidth (H6, V18t)
   failing since before #58. */
.topbar-nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

/* Hit area 44px from padding, per the tap-target floor. The active screen is never colour
   alone: full text colour AND the accent underline; the inactive one is muted with no line.
   Hover adds contrast (muted to primary), never removes it. */
.topbar-nav a {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-target);
  padding: 0 var(--space-3);
  font-family: var(--font-ui);
  font-size: var(--text-body);
  color: var(--text-muted);
  text-decoration: none;
}

.topbar-nav a:hover { color: var(--text-primary); }

.topbar-nav a[aria-current="page"] {
  color: var(--text-primary);
  text-decoration: underline;
  text-decoration-color: var(--accent);
  text-decoration-thickness: 2px;
  text-underline-offset: var(--space-1);
}

/* ── card ──────────────────────────────────────────────────────────────────────────────── */

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space-6);
}

/* ── buttons ───────────────────────────────────────────────────────────────────────────── */

/* The default .btn is the ghost secondary (stackai's pattern): visible border, plain surface,
   so "Start again" and its kin read pressable rather than being grey text that happens to
   respond. The primary below overrides the border to transparent as before. */
.btn {
  font-family: var(--font-ui);
  font-size: var(--text-body);
  line-height: 1.5;
  padding: var(--space-2) var(--space-4);
  background: var(--background);
  color: var(--text-primary);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  cursor: pointer;
  /* CRAFT.md's hit-area floor, actually met. This read --space-8 with a comment claiming 44px;
     --space-8 is 32px and the rendered button was 39px. */
  min-height: var(--tap-target);
}

/* Hover lifts; press lands. The lift is motion and shadow ADDED on hover (never contrast
   removed), and :active below overrides it so a held press always reads as down. */
.btn:hover {
  border-color: var(--text-primary);
  transform: translateY(-1px);
  box-shadow: var(--shadow-card);
}

.btn-primary {
  /* The fill is the accent DARKENED, not the accent. The owner's decided --accent measures
     4.03:1 with white and 3.19:1 with the ink — both under the 4.5:1 body-text floor, and this
     is the one place in the codebase a label sits on the accent. --accent-strong is the minimum
     darkening that clears it: white on it is 5.04:1. #58 AC2 says the gates win over a raw
     reference value, and --accent keeps the decided hex at its six decorative sites. */
  background: var(--accent-strong);
  color: var(--on-accent);
  border-color: transparent;
}

/* The destructive one. --danger text (4.5:1 held by the tokens gate) on the plain surface;
   hover draws the border in the same colour so the whole control says what it is before the
   confirm does. */
.btn-danger { color: var(--danger); }
.btn-danger:hover { border-color: var(--danger); }

/* Hover draws the border in rather than darkening the fill. The fill is already the darkest
   value that keeps its white label clear of the floor (5.04:1), so darkening it on hover would
   be hover REMOVING contrast — the thing CRAFT.md forbids. The border is already there at 1px
   transparent, so nothing moves. */
.btn-primary:hover { border-color: var(--text-primary); }
.btn:active { transform: translateY(1px); box-shadow: none; }

/* Muted colour rather than opacity: opacity dims the label below its contrast floor, and
   stackai names opacity-for-disabled as an anti-pattern.

   aria-disabled is matched alongside `disabled` because the in-flight buttons on this screen
   use the former: setting `disabled` on the focused button drops focus to <body> and does not
   give it back, so a keyboard user lost their place on every save. */
.btn[disabled],
.btn[aria-disabled="true"] {
  background: var(--surface);
  color: var(--text-muted);
  border-color: var(--border);
  cursor: default;
}

.btn[disabled]:active,
.btn[aria-disabled="true"]:active { transform: none; }
.btn[disabled]:hover,
.btn[aria-disabled="true"]:hover {
  border-color: var(--border);
  transform: none;
  box-shadow: none;
}

/* ── fields ────────────────────────────────────────────────────────────────────────────── */

.field {
  display: block;
  font-family: var(--font-ui);
  font-size: var(--text-body);
  color: var(--text-muted);
  margin-bottom: var(--space-2);
}

.input,
.select,
.textarea {
  width: 100%;
  background: var(--background);
  color: var(--text-primary);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space-2) var(--space-3);
  font-family: var(--font-body);
  font-size: var(--text-body);
  line-height: 1.5;
}

.textarea {
  font-size: var(--text-note);
  /* Height comes from the element's own rows attribute rather than a magic min-height, so
     there is no px literal here and the box stays honest about how much it holds. Resizable
     vertically because the note grows; horizontally it is pinned to the column. */
  resize: vertical;
}

.input::placeholder,
.textarea::placeholder { color: var(--text-muted); }

/* The frozen state, visible. Act 1's boxes go readOnly when the prompt is copied (the verifier
   must search the exact text the model read) and previously looked editable while refusing
   edits — the surface tone is what says "kept, not editable" without taking the text away. */
.textarea[readonly] { background: var(--surface); }

/* ── layout ────────────────────────────────────────────────────────────────────────────── */

/* --space-16 rather than --space-12 from #59: the head and the work are two zones, and the gap
   between them has to be visibly larger than the gaps inside either. 48px was doing both jobs at
   once. The phone counterpart is in the mobile-density block, unchanged at --space-8. */
.page-head { margin-bottom: var(--space-16); }
.page-sub  { color: var(--text-muted); margin: 0; }

/* The journey map: the four act chips, shown before the acts exist. Its one job is the whole
   journey visible before any of it happens (index.html:39-43), and as a wrapping inline row it
   had stopped doing that — four full sentences with `flex-wrap: wrap` broke into two ragged
   lines at 1024px and the numerals stopped being scannable, which is the only thing a map is
   for. Four equal 1fr tracks with the numeral over its label is the same information as a band
   you read across, and equal tracks are what keep the numbers on one line regardless of how
   long any single label runs.

   On a phone it is four rows, numeral beside label, because vertical space is the scarce thing
   there — the mobile-density argument at the foot of this file. That shape is the BASE here and
   the band is the override, so the map needs no entry in the max-width: 859px block. */
.steps {
  list-style: none;
  display: grid;
  gap: var(--space-4);
  margin: var(--space-6) 0 0;
  padding: 0;
  font-size: var(--text-body);
  color: var(--text-muted);
}

.steps li {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}

/* The label is wrapped in its own span rather than left as a bare text node beside the chip.
   A bare text node in a flex container becomes an ANONYMOUS flex item: it lays out, but it
   cannot be selected, so it can carry no min-width: 0 and no later rule can reach it. A 1fr
   grid track whose child holds a full sentence and cannot be styled is a blowout waiting for
   the first long label. The span costs one element and makes the item addressable. */
.step-label { min-width: 0; }

@media (min-width: 860px) {
  .steps {
    grid-template-columns: repeat(4, 1fr);
    gap: var(--space-6);
  }
  /* The numeral moves above its label, and align-items: start is what holds every numeral on
     one line across the band while the labels below them wrap to different depths. */
  .steps li {
    flex-direction: column;
    align-items: start;
    gap: var(--space-2);
  }
}

.workspace {
  display: grid;
  gap: var(--space-8);
  margin-bottom: var(--space-12);
}

/* The base gap stays --space-8: on a phone the rail sits ABOVE the work rather than beside it,
   and 32px already reads as a zone break in that direction. The 48px gutter below is what makes
   the two columns read as chrome-and-work rather than as two columns of a table. */
@media (min-width: 860px) {
  .workspace {
    grid-template-columns: 280px 1fr;
    gap: var(--space-12);
    align-items: start;
  }
}

/* The editor column holds pasted prose with long unbreakable lines. min-width: 0 keeps a grid
   item from being sized by its widest child, which is the real Safari and Chrome blowout. */
.editor { min-width: 0; }

/* ── the client rail ───────────────────────────────────────────────────────────────────── */

.rail {
  background: var(--surface);
  border: 1px solid var(--border-hairline);
  border-radius: var(--radius);
  padding: var(--space-4);
  min-width: 0;
  box-shadow: var(--shadow-card);
}

.rail-head {
  font-family: var(--font-ui);
  font-size: var(--text-caption);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin: 0 0 var(--space-4);
}

.client-list {
  list-style: none;
  margin: 0 0 var(--space-4);
  padding: 0;
}

.client-row {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: 0;
  border-radius: var(--radius);
  padding: var(--space-2) var(--space-3);
  margin-bottom: var(--space-1);
  cursor: pointer;
}

.client-row:hover { background: var(--background); }

/* The selected row is marked by a bar and by weight, never by colour alone. */
.client-row[aria-current="true"] {
  background: var(--background);
  box-shadow: inset var(--space-1) 0 0 var(--accent);
}

/* Both shadows, restated. The selector above is more specific than the bare :focus-visible
   rule, so without this the marker bar wins and the focus ring loses its dark hairline —
   on the one control that sits against --surface, which is the case the hairline exists for. */
.client-row[aria-current="true"]:focus-visible {
  box-shadow: inset var(--space-1) 0 0 var(--accent),
              0 0 0 var(--focus-inner) var(--text-primary);
}

.client-name {
  display: block;
  font-family: var(--font-ui);
  font-size: var(--text-body);
  color: var(--text-primary);
  /* A client name is agency-authored and can be long. Clip rather than blow out the rail. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.client-row[aria-current="true"] .client-name { font-weight: 600; }

.client-meta {
  display: block;
  font-size: var(--text-caption);
  color: var(--text-muted);
}

.rail-empty {
  font-size: var(--text-body);
  color: var(--text-muted);
  margin: 0 0 var(--space-4);
}

.rail-form { border-top: 1px solid var(--border); padding-top: var(--space-4); }
.rail-form .btn { width: 100%; margin-top: var(--space-2); }

.rail-state { margin: var(--space-4) 0 0; }

/* ── the editor ────────────────────────────────────────────────────────────────────────── */

/* No max-width here, and that is a measured decision rather than an omission. The column already
   sits inside CRAFT's 65-75ch measure: at --max-width (1024px) minus the 280px rail and the
   --space-8 gutter it renders 712px, and 1ch on this column is 10.59px (Geist at --text-note,
   16px, measured in Chrome and Safari), so 712px is 67.2ch. A `max-width: 75ch` reads as
   diligence and does nothing — 75ch resolves to 795px, above a column that can never exceed
   712. .pack below needs its 68ch because it is full-width under --max-width; this column is
   held by the grid instead. The min-width: 0 above is the rule that matters here, and it is a
   different job: it stops a grid item being sized by its widest child. */

.editor-head { margin-bottom: var(--space-4); }

/* Height from the viewport rather than from the row count. See --note-height for the arithmetic
   and for why the floor is what rows="16" already renders. resize: vertical comes from .textarea
   and stays, so a recruiter with a very long note can still drag it taller. */
.editor .textarea { min-height: var(--note-height); }

/* A client name is agency-authored, capped at 120 characters, and need contain no spaces at
   all. Overflowing text does not widen its own border box, so `.editor { min-width: 0 }` above
   does not catch this and neither does any measurement of element geometry — only the page's
   scrollWidth shows it. Measured before this rule: a 120-character unbroken name scrolled the
   page 1,338px at a 360px viewport. */
.editor-head h2 { overflow-wrap: anywhere; }

.save-row {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  flex-wrap: wrap;
  margin-top: var(--space-4);
}

@media (min-width: 860px) {
  /* Save and its live region stay in view for the whole length of a long note, which is the
     answer to "save feedback" this ticket takes: placement, not a louder treatment. Sticky
     rather than fixed so the row releases at the end of the editor and never covers the delete
     action below it. --background because that is what paints behind the editor column (body's
     own ground; the editor is not on a card), so the row hides the scrolling note under it
     rather than reading as a mismatched band.
     Not applied under 860px: there the row wraps to two lines and would eat a third of a phone
     screen for a control that is one short scroll away anyway. */
  .editor .save-row {
    position: sticky;
    bottom: 0;
    background: var(--background);
    padding: var(--space-3) 0;
    border-top: var(--hairline) solid var(--border-hairline);
  }

  /* So a checkbox focused below the sticky row is not scrolled underneath it. The value is the
     row's own height — see --sticky-save, and keep the two in step. */
  html { scroll-padding-bottom: var(--sticky-save); }
}

/* The one authored moment: the saved-state line arriving. Opacity only; the transition itself
   lives in the motion block at the foot of this file. */
.save-state {
  font-size: var(--text-body);
  color: var(--text-muted);
  opacity: 0;
}

.save-state.is-shown { opacity: 1; }
.save-state.is-error { color: var(--danger); }

.editor-empty { color: var(--text-muted); }

/* ── the note scaffold: the signature element ──────────────────────────────────────────── */

/* One muted line between the label and the box, so the four prompts are in view WHILE writing
   — which is the whole job, and which "below a 24-row textarea" never did. */
.scaffold-line {
  font-size: var(--text-caption);
  color: var(--text-muted);
  margin: 0 0 var(--space-2);
}

/* Removal sits well away from Save, past its own stretch of whitespace. */
.editor-remove { margin-top: var(--space-12); }

/* ── what the saved note produces ──────────────────────────────────────────────────────── */

/* The two readouts, as one zone. The note is one thing a person writes and TWO things the
   system reads out of it — what a candidate may see, and whether the five locum facts are
   recorded. One rule above the pair says that; two more field labels said they were two more
   fields. Same shape as .agency below, and for the same reason. */
.note-facts {
  margin-top: var(--space-12);
  padding-top: var(--space-6);
  border-top: var(--hairline) solid var(--border-hairline);
}

/* .visibility's bare rule opens each fieldset with a --space-12 zone break, which is right when
   it IS the zone break and doubles it once the wrapper above draws one. The first readout sits
   flush under the rule; the second keeps a gap, one step down. Scoped by ancestor rather than by
   editing the bare rule, because .visibility is also act 4's on / (index.html). */
.note-facts .visibility { margin-top: var(--space-8); }
.note-facts .visibility:first-child { margin-top: 0; }

/* ── the visibility list ───────────────────────────────────────────────────────────────── */
/* #18. Native checkbox, native label, no custom switch — this file styles no form control it
   does not have to. The whole component is the .radio-row label pattern below, which already
   solved the tap-target floor; nothing here re-solves it. */

/* Same fieldset reset as .radio-group: the browser's border and padding are an artefact, not a
   decision. The space above is a zone break — this describes the saved note, so it must not
   read as part of the save row it sits under. */
.visibility {
  border: 0;
  margin: var(--space-12) 0 0;
  padding: 0;
}

.visibility-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/* `display: flex` above is an AUTHOR rule, and the UA's `[hidden] { display: none }` is not, so
   the author rule wins whatever the specificity — which made `el.visibilityList.hidden = true`
   in clients.js a silent no-op. It hid nothing today only because the list is empty whenever it
   is set, so the bug was invisible rather than absent. */
.visibility-list[hidden] { display: none; }

/* center, exactly as .radio-row label does. baseline was tried and measured wrong: the row is
   44px tall and the heading wraps at 360px, so aligning to the first text baseline dropped the
   checkbox to the bottom of a two-line row. min-width: 0 lets a long heading wrap inside the
   row instead of widening the column. */
.visibility-list label {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-target);
  font-size: var(--text-body);
  min-width: 0;
}

/* flex-shrink: 0 keeps the box square when the heading beside it is long. */
.visibility-list input[type="checkbox"] { flex-shrink: 0; }

/* A section heading is agency-authored and need contain no spaces at all — the same reasoning
   as .editor-head h2, and the same fix. min-width: 0 lets it actually shrink. */
.visibility-name {
  overflow-wrap: anywhere;
  min-width: 0;
}

/* The row's quiet half: how much is in the section, or why it cannot be shared. Caption size,
   which is the floor — nothing on this screen goes below it. margin-left: auto pushes it to
   the row's end so the checkboxes and headings stay in one readable column. */
.visibility-meta {
  color: var(--text-muted);
  font-size: var(--text-caption);
  margin-left: auto;
  text-align: right;
}

/* A disabled row is a statement, not a dead control: the reason is in .visibility-meta beside
   it, so the row only needs to stop looking pressable. */
.visibility-list input[type="checkbox"]:disabled { cursor: default; }
.visibility-list label:has(input:disabled) { color: var(--text-muted); }

/* A long list of sections scans as rows rather than as a block. Both readouts take it: they are
   one component, both <ul>s carry .visibility-list, and a separator on one and not the other
   would read as a difference that means something. Scoped under .note-facts because the bare
   .visibility-list rules are act 4's on / too. */
.note-facts .visibility-list li + li {
  border-top: var(--hairline) solid var(--border-hairline);
}

/* ── the locum checklist ───────────────────────────────────────────────────────────────── */

/* #48 shipped this list with no CSS at all, so its three spans ran together on one line and
   .visibility-meta's `margin-left: auto` did nothing outside a flex row. Same component as the
   visibility rows above, one row taller: the heading and the status on the first line, the hint
   under them, the "Add ## …" button on its own line at the end. Grid rather than flex because
   the hint spans both columns, which is a row a flex line cannot describe. */
.note-facts .locum .visibility-list li {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: var(--space-1) var(--space-3);
  min-height: var(--tap-target);
  padding: var(--space-2) 0;
  font-size: var(--text-body);
}

/* The hint is the second row and spans both columns. The bare .visibility-meta pushes itself
   right, which is correct for a status at the end of a row and wrong for a sentence explaining
   what to write — so both the push and the alignment are reset here. */
.note-facts .locum .locum-detail {
  grid-column: 1 / -1;
  margin-left: 0;
  text-align: left;
}

/* A grid item fills its cell, so without this the button stretches the full column width and
   reads as a page-wide bar rather than as one row's action. */
.note-facts .locum .visibility-list .btn {
  grid-column: 1 / -1;
  justify-self: start;
}

/* Matches .rail-empty: an empty state is an invitation, in the same voice. */
.visibility-empty {
  color: var(--text-muted);
  font-size: var(--text-body);
  max-width: 60ch;
  margin: 0;
}

/* ── the outcome log (#96) ─────────────────────────────────────────────────────────────── */

/* Built by outcome-form.js on two screens. It borrows .visibility's fieldset reset and
   .zone-head's eyebrow, so it reads as one more zone that describes the saved note. */

/* Three fields on one row where they fit, one per row on a phone: .assignments-fields' grid,
   for the same reason and with the same min-content fix. */
.outcome-fields {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(18ch, 1fr));
  gap: var(--space-4);
  margin-bottom: var(--space-4);
}

.outcome-field { min-width: 0; }

/* The hiring manager the form is for, on the Contracts screen. Body size, not caption: it is
   the one fact the recruiter must check before pressing anything. */
.outcome-for {
  font-size: var(--text-body);
  margin: 0 0 var(--space-2);
}

/* Its own row class rather than .save-row: .editor .save-row is sticky at the foot of the
   note, and a second sticky row on the same screen would ride over the first. */
.outcome-row {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  flex-wrap: wrap;
}

/* The interview prompt (#99): the same tint pairing as the Contracts screen's amber chip
   (--unverified on --tint-warn, 5.59:1, gated by test/tokens.test.js), because it is the same
   kind of fact: something has passed and is waiting on the recruiter. Opens the outcome zone,
   so it takes the zone break the fieldset under it would otherwise take. */
.outcome-pending {
  display: inline-block;
  margin: var(--space-12) 0 0;
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-sm);
  background: var(--tint-warn);
  color: var(--unverified);
  font-size: var(--text-body);
}

.outcome-pending:not([hidden]) + #outcome-host .visibility { margin-top: var(--space-3); }

/* Locked while the note above is dirty: `disabled` on the fieldset reaches every control in it,
   so the :disabled pseudo-class is how they take the same quiet treatment .btn[disabled] gets. */
.outcome:disabled .input,
.outcome:disabled .select,
.outcome:disabled .btn {
  background: var(--surface);
  color: var(--text-muted);
  border-color: var(--border);
  cursor: default;
}

/* ── the agency strip ──────────────────────────────────────────────────────────────────── */

/* The one hairline on the page. Whitespace alone cannot say "this is configuration, not
   content", which is the grouping CRAFT.md allows a rule to encode. */
.agency {
  border-top: 1px solid var(--border);
  padding-top: var(--space-6);
}

/* The eyebrow, shared. .zone-head is the editor column's two zone legends and takes the same
   five declarations plus the same bottom margin, so it joins this selector rather than
   restating them — .rail-head above is the third copy and differs only in its margin. What an
   eyebrow says is "a new zone starts here"; .field says "a label for the control below". Those
   are different jobs, and rendering both through .field is what flattened the editor column. */
.agency-head,
.zone-head {
  font-family: var(--font-ui);
  font-size: var(--text-caption);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin: 0 0 var(--space-2);
}

/* A <legend>'s UA padding is a couple of px each side, which is invisible on a field label and
   not on an eyebrow that has to line up with the rule above its zone. */
.zone-head { padding: 0; }

.agency-sub {
  font-size: var(--text-body);
  color: var(--text-muted);
  max-width: 60ch;
  margin: 0 0 var(--space-4);
}

/* The line under a control's label that says what the control means in the reader's words.
   Same voice as .scaffold-line: caption size, muted, close to what it explains. */
.field-hint {
  font-size: var(--text-caption);
  color: var(--text-muted);
  max-width: 60ch;
  margin: 0 0 var(--space-2);
}

.agency-controls {
  display: grid;
  gap: var(--space-6);
}

@media (min-width: 860px) {
  .agency-controls { grid-template-columns: 1fr 1fr; }
}

/* A fieldset's default chrome is a browser artefact, not a design decision. */
.radio-group { border: 0; margin: 0; padding: 0; }

.radio-row {
  display: flex;
  gap: var(--space-4);
  flex-wrap: wrap;
  font-size: var(--text-body);
}

/* min-height, because a bare flex label is the height of its text — about 21px here, under
   CRAFT.md's 24px floor for any pointer and well under the 44px one for touch. */
.radio-row label {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-target);
}

.agency .select { max-width: 280px; }

/* ── the one screen: three acts ────────────────────────────────────────────────────────── */
/* #8. The rail above is /clients' rail unchanged; everything below is this screen's own. */

/* The column holding pasted briefs, CVs and replies. min-width: 0 keeps a grid item from being
   sized by its widest child, which is the real Safari and Chrome blowout — same reason .editor
   carries it at line 183. */
.stage { min-width: 0; }

.rail-hint { font-size: var(--text-body); color: var(--text-muted); margin: 0; }

/* Rhythm BETWEEN the acts is --space-16 and within them --space-4 to --space-6, so the pacing of
   the flow is whitespace rather than dividers. THERE IS STILL NOT ONE RULE BETWEEN THESE
   SECTIONS, and that claim survives #59 deliberately: "numbered sections" is exactly the phrase
   that invites a hairline above each act, and at 64px the whitespace already does the job the
   epic asked for. The phone counterpart steps back to --space-12 in the mobile-density block —
   64px there is most of a viewport of nothing. */
.act + .act { margin-top: var(--space-16); }

/* The one authored moment: an act arriving. Opacity AND an 8px rise from #59 — both compositor
   properties, and the rise is what makes an act read as arriving rather than as appearing.
   Gated behind a class the transition adds rather than sitting on the element, because
   CHECKLIST forbids an entrance on anything rebuilt per render and this file cannot tell the
   two cases apart on its own.

   The transform makes .act a containing block for position: fixed and position: absolute
   descendants, for the ~200ms the class is on. Inert today — this file declares no `position`
   anywhere — but it is the rule that would explain the surprise if #60 adds an absolutely
   positioned chip inside act 3.

   setPhase calls scrollIntoView synchronously right after showAct, so at that instant the
   arriving act is 8px low and the scroll target is 8px off. It settles as the transition runs.
   Known and accepted; do not paper over it with a setTimeout. */
.act {
  opacity: 1;
}

.act.is-entering { opacity: 0; transform: translateY(var(--space-2)); }

/* The act heads and .rail-head were one grammar (#8) — both small, tracked, uppercase and muted.
   #59 SPLITS them, on purpose, because they are not the same kind of thing. The rail is chrome:
   a label over a list the recruiter picks from once. The acts are the work, and the epic's
   decided direction is numbered sections in big type. A section heading set quieter than the
   hint text under a file picker is not a hierarchy. So .rail-head keeps the small tracked label
   treatment — it is still a label — and .act-head takes the ramp's --text-h3 in sentence case
   with the numeral beside it. That pair is what carries "numbered sections"; nothing else in
   this file has to change for it to read.

   The type declarations restate what the base h2 rule already sets at identical values. They are
   here for the same reason .rail-head restates its own: a head class should say what it is
   without the reader having to hold the element it happens to be written on. */
.act-head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  font-family: var(--font-ui);
  font-size: var(--text-h3);
  font-weight: 600;
  color: var(--text-primary);
  margin: 0 0 var(--space-6);
}

/* The step number as a chip, so the four-act shape is visible at a glance rather than read
   out of running text. aria-hidden in the markup: the heading names the act; the number is
   the visual rhythm.

   ONE class in TWO places — the steps map (index.html:45-48) and the four act headings — and
   that is deliberate: "same chip grammar as the acts, so the map and the flow read as one
   thing" (index.html:41-42). Size its CONTEXT, never the chip.

   24px, up from 20px in #59: a 20px chip beside a 20px heading reads as a bullet, and 24px
   reads as a numeral. The 12px mono stays — 12px centred in a 24px box is comfortable and 14px
   is not. --radius on a 24px box is a squircle rather than a pill, which is the child-under-
   parent concentric case and the reason not to reach for border-radius: 50%.

   The letter-spacing: 0 that sat here is GONE: it existed only to cancel the 0.08em .act-head
   used to impose, and .act-head no longer tracks anything. It was a dead declaration whose
   comment had stopped being true. */
.act-num {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--space-6);
  height: var(--space-6);
  /* A flex item's default flex-shrink: 1 lets a fixed width be squashed by a long sibling on
     the main axis, and the chip is now a flex item in two containers — .act-head, and .steps li
     which is a row on a phone. At 360px, "Send the candidate their prep once the interview is
     booked" beside this chip is exactly that case. Same line and same reason as .strike-box and
     the visibility list's checkbox. */
  flex-shrink: 0;
  background: var(--surface);
  border: 1px solid var(--border-hairline);
  border-radius: var(--radius);
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  color: var(--text-primary);
}

.act-head-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
  flex-wrap: wrap;
  margin-bottom: var(--space-4);
}

.act-head-row .act-head { margin-bottom: 0; }

.act-note {
  font-size: var(--text-body);
  color: var(--text-muted);
  margin: 0 0 var(--space-4);
  max-width: 60ch;
}

.act-row {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  flex-wrap: wrap;
  margin-top: var(--space-4);
}

/* ── act 1: the inputs ─────────────────────────────────────────────────────────────────── */

/* The bottom margin puts the two-route button row in its own zone rather than crowding the
   boxes. EXPECT 32px BETWEEN THEM, NOT 48px, and do not "fix" it: .act is a plain block box, so
   this 32px and .act-row's 16px top margin are adjacent sibling margins and COLLAPSE to
   max(32, 16). The gap doubles from today's 16px, which is the intent; it does not sum. */
.input-pair {
  display: grid;
  gap: var(--space-6);
  margin-bottom: var(--space-8);
}

@media (min-width: 860px) {
  .input-pair { grid-template-columns: 1fr 1fr; }
}

/* Both columns hold pasted prose with long unbreakable lines. Same trap as .stage. */
.input-col { min-width: 0; }

/* The two boxes are the work of this screen, so their labels take the ramp step above the
   chrome around them and the ink rather than the muted grey. Scoped to .input-pair rather than
   applied to .field itself, deliberately: .field also labels act 2's reply box, act 4's two
   fields and every control on /clients, where 14px muted is correct — those are form labels,
   and these two name the material the pack is built from. .field is used 11 times across the
   three screens, so editing it here would restyle a screen this ticket does not own. */
.input-pair .field {
  font-size: var(--text-h4);
  color: var(--text-primary);
  margin-bottom: var(--space-3);
}

.file-row { margin-top: var(--space-2); }

.file-label {
  display: block;
  font-family: var(--font-ui);
  font-size: var(--text-caption);
  color: var(--text-muted);
  margin-bottom: var(--space-1);
}

/* The engine's own button, restyled to the ghost-button grammar through the standard
   ::file-selector-button pseudo-element (Chrome 89+, Safari 14.1+ — this deployment's
   engines). The filename text beside it stays muted; the input still only guarantees hit
   area and refuses to widen the column. */
.file-input {
  font-family: var(--font-body);
  font-size: var(--text-body);
  color: var(--text-muted);
  min-height: var(--tap-target);
  max-width: 100%;
}

.file-input::file-selector-button {
  font-family: var(--font-ui);
  font-size: var(--text-body);
  line-height: 1.5;
  padding: var(--space-1) var(--space-3);
  margin-right: var(--space-2);
  background: var(--background);
  color: var(--text-primary);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  cursor: pointer;
}

.file-input::file-selector-button:hover { border-color: var(--text-primary); }

/* The column answering a file dragged over it. Colour is not the only signal: the state is
   entered by the recruiter's own drag, and the file label says "drop one on the box" in
   words. --accent as a border is a fill use, never text: 3.53:1 on --surface, over the 3:1
   non-text floor this state's boundary is held to. */
.input-col.is-dragover .textarea {
  border-color: var(--accent);
  background: var(--surface);
}

.fallback { margin-top: var(--space-6); }

/* ── act 2: the wait ───────────────────────────────────────────────────────────────────── */

/* Act 2's two modes (28 Jul 2026). Manual children hide while a pack is generating here and
   after it arrives; the generating note shows only then. Display, not visibility: the reply
   box must not hold layout space in a mode where it cannot be used. */
#act-waiting .generating-only { display: none; }
#act-waiting.is-generating .generating-only { display: block; }
#act-waiting.is-generating .manual-only { display: none; }

/* Real seconds, monospaced with tabular figures so the count does not jitter the row it sits
   in. Honest by construction: there is no percentage to invent, because the work is happening
   in another application. */
.elapsed {
  font-family: var(--font-mono);
  font-size: var(--text-body);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

/* The clock in act 2's head row is the only thing on screen that changes for a minute or two,
   and the recruiter is watching it from wherever they put the laptop down. It takes the same
   ramp step as the heading beside it. Scoped to .act-head-row because .elapsed is ALSO
   #send-elapsed in act 4's .act-row, where the send takes seconds and a 20px counter beside two
   buttons would shout. .act-head-row aligns on baseline, so a 20px clock and a 20px heading now
   share one — which is why the head row keeps `baseline` rather than switching to `center`. */
.act-head-row .elapsed { font-size: var(--text-h3); }

/* The one thing a recruiter actually reads while waiting, so it takes reading size rather than
   chrome size — the same 16px the pack itself uses, for the same reason (CRAFT: prefer 16px for
   prose a person reads). .act-note's 60ch measure holds: at 16px it grows in px but stays 60
   characters, inside CRAFT's 65-75ch band, so there is no second max-width here. */
#act-waiting .act-note.generating-only { font-size: var(--text-note); }

/* ── act 3: the pack, the signature surface ────────────────────────────────────────────── */

/* The headline number of the whole product: how much of this pack stands on evidence. It is the
   answer to the only question the recruiter has before sending, so it is one step above body and
   one step below the act heading it supports — supporting it, not competing with it. Each count
   wears its mark's colour WITH its word and now on its mark's ground too, so the summary and the
   claims below say the same thing in the same grammar. The gap and the two grounds do the
   separating that a middle dot used to. Wraps rather than overflows when the heading beside it
   leaves the row too little. */
.pack-summary {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2);
  font-size: var(--text-h4);
  color: var(--text-muted);
}

.summary-sourced,
.summary-unverified,
.summary-unverified-none {
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-sm);
  font-weight: 600;
}

.summary-sourced   { color: var(--verified); background: var(--tint-verified); }
.summary-unverified { color: var(--unverified); background: var(--tint-warn); }

/* Nothing unverified is not a warning, so it does not wear the warning's ground. It keeps the
   chip's box so the two counts still sit on one baseline. */
.summary-unverified-none { color: var(--text-muted); }

.renderer-note {
  font-size: var(--text-body);
  color: var(--text-muted);
}

/* Prose a client will read, so it takes CRAFT's 65-75ch measure and the 16px body size rather
   than the 14px the surrounding chrome uses. The card lift matches the rail: the two surfaces
   that hold the product's content are the two that rise off the page. */
.pack {
  background: var(--surface);
  border: 1px solid var(--border-hairline);
  border-radius: var(--radius);
  padding: var(--space-6);
  font-size: var(--text-note);
  max-width: 68ch;
  min-width: 0;
  overflow-wrap: anywhere;
  box-shadow: var(--shadow-card);
}

.pack-role { font-size: var(--text-h3); margin: 0 0 var(--space-1); }

.pack-meta {
  font-size: var(--text-body);
  color: var(--text-muted);
  margin: 0 0 var(--space-4);
}

.pack-headline { margin: 0 0 var(--space-8); }

.pack-section { margin-bottom: var(--space-8); }
.pack-section:last-child { margin-bottom: 0; }

.pack-section-head {
  font-family: var(--font-ui);
  font-size: var(--text-caption);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin: 0 0 var(--space-4);
}

/* The signature element: a claim sitting beside the verbatim line it came from. All of this
   screen's boldness is spent here, because that pairing is the product's thesis. */
.claim { margin-bottom: var(--space-6); }
.claim:last-child { margin-bottom: 0; }

.claim-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
}

.claim-text { margin: 0; min-width: 0; }

.claim-requirement { font-weight: 600; }

/* A hairline that encodes grouping whitespace cannot: it ties the quote to the claim above it.
   CRAFT allows a rule for exactly that and for nothing else.
   The mono face is the second thing saying what this text is: a verbatim span lifted out of a
   document, not more of our prose. It is what the epic decided the second face FOR, and mono
   already means "quoted literally" everywhere else on the deployment — the code element, the
   honest clock, the counts tiles. */
.claim-source {
  font-family: var(--font-mono);
  font-size: var(--text-body);
  color: var(--text-muted);
  margin: var(--space-2) 0 0;
  padding-left: var(--space-3);
  border-left: 1px solid var(--border);
}

/* The evidence disclosure. A native <details>, so the open/closed state is drawn by the browser
   and announced by the screen reader without a line of ours. */
.claim-evidence { margin-top: var(--space-2); }

.claim-evidence-toggle {
  /* No `display` here, deliberately: <summary> is list-item by default and ANY other display
     value silently removes the disclosure marker in every engine — and the marker is the only
     thing carrying open/closed, which is why the label never changes. A custom marker would go
     through ::marker, never through display.
     The padding is the hit area: the label's own line box is well under CRAFT's touch floor, and
     a padded summary clears it while leaving the whole strip pressable. */
  padding: var(--space-3) 0;
  font-size: var(--text-body);
  color: var(--text-muted);
  cursor: pointer;
}

/* Hover raises the contrast rather than lowering it, which is the house rule for every state. */
.claim-evidence-toggle:hover { color: var(--text-primary); }

/* The provenance mark, now a chip. FOUR signals, never colour alone: the WORD, the colour, the
   ground it sits on, and on the two unverified states a left border on the whole claim. A
   recruiter who cannot distinguish the colours still reads "Unverified". The ground is what
   turns a label into a status the eye finds without reading — the pattern act 3 borrows from
   forcanopy's results page. */
.mark {
  flex-shrink: 0;
  font-family: var(--font-ui);
  font-size: var(--text-caption);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-sm);
}

/* --brief is act 4's word for the same fact act 3 calls --cv or --note: found, literally, in
   the material. It shares --verified rather than earning a fourth colour, because a fourth
   provenance colour would mean a fourth thing to tell apart for no extra meaning.
   Mint under the sourced words, blush under both unsourced ones — the two states whose next
   move differs share a ground because the recruiter's question of them is the same one. Each
   pairing is already measured and gated by test/tokens.test.js's TINTS table: 6.24:1 for
   --verified on mint, 5.59:1 for --unverified on blush, 7.08:1 for --failed on blush. No
   border: --border is 2.53:1 on the powder tint and a chip that carries a word does not need
   an edge to be found. */
.mark-cv,
.mark-note,
.mark-brief { color: var(--verified); background: var(--tint-verified); }
.mark-unverified { color: var(--unverified); background: var(--tint-warn); }
.mark-failed { color: var(--failed); background: var(--tint-warn); }

/* On a narrow screen the mark stops sitting beside the claim and sits under it.
   "Quote not found" is about 120px of unbreakable text, and beside a claim inside a 282px pack
   it left the claim roughly twenty characters a line. Measured at 390px before this rule. The
   reading order is unchanged because the mark already follows the claim in the DOM.
   inline-block rather than block now that the mark carries a ground: block would stretch it the
   full width of the column and it would read as a banner across the claim rather than as a chip
   under it. inline-block shrink-wraps to the word and still honours the margin-top, which a
   plain inline element would not. */
@media (max-width: 600px) {
  .claim-head { display: block; }
  .mark { display: inline-block; margin-top: var(--space-1); }
}

.claim-unverified {
  border-left: var(--space-1) solid var(--unverified);
  padding-left: var(--space-4);
}

.claim-failed {
  border-left: var(--space-1) solid var(--failed);
  padding-left: var(--space-4);
}

/* What the model thought it was citing, kept so a bad pack is diagnosable rather than merely
   rejected. This is the line that turns "something is wrong" into "that quote is not in the CV". */
.claim-failed-quote {
  font-size: var(--text-body);
  color: var(--failed);
  margin: var(--space-2) 0 0;
}

.pack-questions {
  margin: 0;
  padding-left: var(--space-6);
  font-size: var(--text-note);
}

.pack-questions li { margin-bottom: var(--space-2); }

/* The one block of the pack that is neither sourced nor unsourced: it is the recruiter's own
   list of things to confirm before sending. Powder is the info tint and this is the info, so
   this is where #58's third tint was always going.
   --text-primary on the head, never --text-muted: muted measures 3.75:1 on this ground and is
   under the body-text floor (tokens.css:68-79), which is why the head is restated here rather
   than inherited. No border either — --border is 2.53:1 on powder, under the non-text floor, and
   the change of ground is the boundary. --radius and not --radius-sm: this is a panel, and
   panels here take the softer corner; the small one belongs to the chips. */
.pack-questions-panel {
  background: var(--tint-info);
  border-radius: var(--radius);
  padding: var(--space-4);
}

.pack-questions-panel .pack-section-head { color: var(--text-primary); }

/* ── act 4: send to candidate ──────────────────────────────────────────────────────────── */

/* The preview is a second zone inside act 4, so the step between it and the two fields above
   is a whole rhythm step larger than the spacing within it — CRAFT's "rhythm between zones
   must be visibly larger than spacing within them", using whitespace rather than a rule.
   --space-16 from #59: the argument was already this, and 32px stopped expressing it once
   --space-16 existed and the acts themselves opened to it. Steps back on a phone. */
.send-preview { margin-top: var(--space-16); }

/* The competency rows ARE provenance rows, so they reuse .claim / .claim-head / .claim-text /
   .claim-source / .mark verbatim from act 3. That reuse is the point: the recruiter has just
   read the pack in exactly this grammar, and a second, similar-but-different treatment for
   the same idea is how one system starts reading as two. */
.strike-list {
  list-style: none;
  margin: 0 0 var(--space-6);
  padding: 0;
}

/* The whole row is the label, so the hit area is the text and not just the 13px box —
   CRAFT's ≥44px effective target, met by the row rather than by growing the control. */
.strike-label {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-target);
  min-width: 0;
  cursor: pointer;
}

/* flex-shrink: 0 keeps the box square beside a long competency label, exactly as the
   /clients visibility list does. */
.strike-box { flex-shrink: 0; }

/* A competency label is model-written and can be long; min-width: 0 lets it wrap inside the
   row instead of widening the column and pushing the mark off the edge. */
.strike-row .claim-text { overflow-wrap: anywhere; }

/* The quote sits under the row it belongs to, indented past the checkbox so the eye reads it
   as that row's evidence rather than as a new item. */
.strike-row .claim-source { margin-left: calc(var(--space-4) + var(--space-2)); }

.send-fields {
  margin: 0 0 var(--space-6);
}

.send-legend {
  padding: 0;
  font-size: var(--text-body);
  font-weight: 600;
}

.send-fields-note {
  color: var(--text-muted);
  font-size: var(--text-body);
  max-width: 60ch;
  margin: var(--space-2) 0;
}

/* Mirrors .visibility-list label's row grammar — the same shape carrying the same two facts,
   minus the checkbox, because nothing here is tickable: this list reports what the toggle on
   /clients already decided. */
.send-field-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-target);
  font-size: var(--text-body);
  min-width: 0;
}

/* The two inputs are act 4's only fields: a date and an email address are both short, and two
   full-width inputs stacked over a 65ch measure read as a form nobody thought about. They are
   one question — when, and to whom — so they sit as one row on a desktop and stack on a phone.
   This REPLACES the max-width: 32ch the two ids carried: the column bounds them now, so the cap
   is the grid's job rather than two id selectors'. The 60ch on the pair is what keeps that true
   — without it the row stretches the full stage and a 500px-wide date picker is worse than the
   stacked version it replaced. With it each column is ~30ch, near the 32ch the ids had, which
   is why removing that cap is safe rather than merely relocating it.

   Same shape as .input-pair / .input-col for act 1, applied to short fields, and the names say
   so. Not a <fieldset>: act 4 already has one (#send-fields) with its own legend, and a second
   one around two fields that are not a group of related choices adds a landmark for nothing. */
.field-pair {
  display: grid;
  gap: var(--space-6);
  margin-bottom: var(--space-6);
}

.field-col { min-width: 0; }

@media (min-width: 860px) {
  .field-pair { grid-template-columns: 1fr 1fr; max-width: 60ch; }
}

/* ── the counts page ───────────────────────────────────────────────────────────────────── */

/* Wide content scrolls inside its own box rather than widening the page — CRAFT's rule, and
   a four-column table with agency-authored client names is exactly the case it names. */
.counts {
  max-width: 65ch;
  overflow-x: auto;
}

.counts-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-body);
}

/* The caption carries the scope of the numbers, so the claim above and the bounds below it
   are both readable without a legend somewhere else. */
.counts-caption {
  text-align: left;
  color: var(--text-muted);
  font-size: var(--text-caption);
  padding-bottom: var(--space-3);
}

/* One hairline under the head, which is the grouping whitespace cannot encode in a table:
   without it the header row reads as the first data row. */
.counts-table thead th {
  text-align: left;
  font-weight: 600;
  border-bottom: 1px solid var(--border);
  padding: var(--space-2) var(--space-3) var(--space-2) 0;
}

/* --space-4 rather than --space-3 down the sides: these rows are read across, one client at a
   time, and 12px of vertical padding packed four columns of digits into a block. Air is what
   makes a table scan as rows. */
.counts-table tbody th,
.counts-table tbody td {
  text-align: left;
  font-weight: 400;
  border-bottom: 1px solid var(--border-hairline);
  padding: var(--space-4) var(--space-3) var(--space-4) 0;
  min-height: var(--tap-target);
}

/* A client name is agency-authored and need contain no spaces at all — the same reasoning as
   .visibility-name, and the same fix. */
.counts-table tbody th { overflow-wrap: anywhere; }

/* Tabular figures so the columns line up digit-for-digit; right-aligned because these are
   quantities being compared down a column, not labels being read across. nowrap so a figure
   never breaks across two lines mid-number, which is the one way a count can be misread.
   tabular-nums stays even though the mono face below is fixed-width: it costs nothing and it is
   what keeps the fallback stack aligned when DM Mono has not loaded. */
.counts-number {
  text-align: right;
  font-variant-numeric: tabular-nums;
  padding-right: 0;
  white-space: nowrap;
}

/* Mono for the numbers, which is the epic's rule for counts and data — and scoped to the body
   cells on purpose. DM Mono ships 400 alone (fonts.css), and .counts-table thead th is 600: a
   600 request against a single-weight family gets synthetic bold rather than a real face. The
   header labels are words anyway, and words stay in the UI sans. */
.counts-table tbody .counts-number { font-family: var(--font-mono); }

/* A zero is printed in full and then quieted, so the eye lands on the numbers that are not
   zero. The digit still carries the information; the colour only ranks it. --text-muted is held
   to the 4.5:1 body-text floor by test/tokens.test.js, so this is a legible value rather than a
   decorative grey. */
.counts-number.is-zero { color: var(--text-muted); }

.counts .save-state { margin-top: var(--space-4); }

/* ── the bookings page (#69) ───────────────────────────────────────────────────────────── */

/* Six columns of recruiter-authored names and dates, so the same rule the counts table takes:
   wide content scrolls inside its own box rather than widening the page (CRAFT). Wider than
   .counts' 65ch because this table carries two name columns and a control column — 65ch would
   put it in a permanent horizontal scroll on a laptop, which is the failure the rule exists to
   avoid rather than the one it causes. */
.assignments {
  max-width: 88ch;
  overflow-x: auto;
}

/* The form is a zone above the list, and the gap between them has to read as a zone break —
   .page-head's argument at a smaller scale. */
.assignments-form { margin-bottom: var(--space-12); }

.assignments-form-head {
  font-size: var(--text-h4);
  margin: 0 0 var(--space-4);
}

/* Auto-fit rather than a fixed column count: five fields at a readable minimum width settle to
   three across on a laptop and one on a phone with no breakpoint of their own. `minmax(0, 1fr)`
   and not `1fr` — a grid track's default min-content floor is what makes a long client name in
   a <select> push the whole row wider than its container. */
.assignments-fields {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(18ch, 1fr));
  gap: var(--space-4);
  margin-bottom: var(--space-4);
}

.assignments-field { min-width: 0; }

.assignments-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-body);
}

.assignments-caption {
  text-align: left;
  color: var(--text-muted);
  font-size: var(--text-caption);
  padding-bottom: var(--space-3);
}

/* One hairline under the head — the grouping whitespace cannot encode in a table, .counts-table's
   rule unchanged. */
.assignments-table thead th {
  text-align: left;
  font-weight: 600;
  border-bottom: 1px solid var(--border);
  padding: var(--space-2) var(--space-3) var(--space-2) 0;
}

.assignments-table tbody th,
.assignments-table tbody td {
  text-align: left;
  font-weight: 400;
  vertical-align: middle;
  border-bottom: 1px solid var(--border-hairline);
  padding: var(--space-3) var(--space-3) var(--space-3) 0;
}

/* A candidate name and a client name are both typed by a person and need contain no spaces at
   all — .counts-table tbody th's reasoning, and the same fix. */
.assignments-table tbody th { overflow-wrap: anywhere; }

/* Dates are data, so they take the mono face and tabular figures: two dates in a column are read
   by comparing them, and that only works if the digits line up. nowrap because a date broken
   across two lines is a date that can be misread. */
.assignments-date-cell {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* The state chip. It always carries the WORD — the colour ranks it and never says it on its own,
   which is tokens.css's rule for every tint here and what keeps the screen readable to someone
   who cannot separate the two grounds. */
.assignments-chip {
  display: inline-block;
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-sm);
  font-size: var(--text-caption);
  white-space: nowrap;
}

/* Inside the radar's window. --unverified on --tint-warn is 5.59:1, measured in tokens.css:76 and
   gated by test/tokens.test.js — no new token, and an agency's branding swap reaches it. */
.assignments-chip.is-amber {
  background: var(--tint-warn);
  color: var(--unverified);
}

/* Resolved sinks visually as well as in the sort order. --text-muted holds the 4.5:1 body floor,
   so this quiets the row without dropping it below legibility. */
.assignments-chip.is-resolved { color: var(--text-muted); }

/* The per-row controls sit on one line and wrap as a group on a narrow viewport. The date input
   is sized to its content rather than the .input default of 100%, which in a table cell would
   take the whole column. */
.assignments-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}

.assignments-date { width: auto; }

/* --text-caption on a control, which is below the body size — allowed here because these are
   two words on a button whose tap target is set by the padding, not by the type. Both stay at or
   above the 12px floor. */
.assignments-action {
  font-size: var(--text-caption);
  padding: var(--space-2) var(--space-3);
}

.assignments .save-state { margin-top: var(--space-4); }

/* ── the compliance dashboard (#71) ────────────────────────────────────────────────────── */

/* A CARD LIST RATHER THAN A TABLE, which is the one structural difference from the bookings
   screen above. Eight items per candidate in a six-column table would put a whole page of
   repeated headers between one candidate's name and the next; the card makes the candidate the
   unit, which is how a recruiter reads this screen — "who do I chase" before "what about". The
   container still scrolls inside its own box rather than widening the page (CRAFT), same width
   as .assignments for the same reason: this row carries a label, a reference and a control
   group. */
.compliance {
  max-width: 88ch;
  overflow-x: auto;
}

/* The gap between cards has to read as a zone break, or two candidates' items merge into one
   list — .page-head's argument at a smaller scale. */
.compliance-card { margin-bottom: var(--space-8); }

/* A candidate name is typed by a person and may contain no spaces at all — .counts-table tbody
   th's reasoning, and the same fix. */
.compliance-name {
  font-size: var(--text-h4);
  margin: 0 0 var(--space-1);
  overflow-wrap: anywhere;
}

/* The completeness line. --text-muted holds the 4.5:1 body floor, so this quiets the sentence
   under the name without dropping it below legibility. */
.compliance-summary {
  color: var(--text-muted);
  font-size: var(--text-caption);
  margin: 0 0 var(--space-3);
}

.compliance-items {
  list-style: none;
  margin: 0;
  padding: 0;
}

/* One hairline per item, and wrap as a group: on a phone the chips and the controls drop below
   the label rather than forcing the card into a horizontal scroll. */
.compliance-item {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  border-bottom: 1px solid var(--border-hairline);
  padding: var(--space-3) 0;
  font-size: var(--text-body);
}

/* The label takes the slack so the chips sit at a consistent place across a card's eight rows.
   `min-width: 0` because a flex item's default min-content floor is what lets a long item name
   push the row wider than its container. */
.compliance-item-label {
  flex: 1 1 22ch;
  min-width: 0;
}

/* A reference number is a stranger's typing: it may be one unbroken run of characters. */
.compliance-ref {
  color: var(--text-muted);
  overflow-wrap: anywhere;
}

/* Dates are data, so they take the mono face and tabular figures: a column of dates is read by
   comparing them, and that only works if the digits line up. nowrap because a date broken across
   two lines is a date that can be misread. .assignments-date-cell's rule at a second root. */
.compliance-date {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: var(--text-muted);
}

/* Both chips. Each always carries the WORD — the colour ranks it and never says it on its own,
   which is tokens.css's rule for every tint here. That matters more on this screen than on the
   bookings one, because a row carries TWO chips and their difference (chase state versus risk)
   is carried by the words, not by the colours. */
.compliance-chip {
  display: inline-block;
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-sm);
  font-size: var(--text-caption);
  white-space: nowrap;
}

/* Running out. --unverified on --tint-warn is 5.59:1, measured in tokens.css:76 and gated by
   test/tokens.test.js — no new token, and an agency's branding swap reaches it. */
.compliance-chip.is-amber {
  background: var(--tint-warn);
  color: var(--unverified);
}

/* Already run out. The SAME ground with the darker ink rather than a second tint: --failed on
   --tint-warn is 7.08:1, measured on the same line, and one ground with two inks is what keeps
   red reading as "more of the same problem" instead of a different kind of thing. */
.compliance-chip.is-red {
  background: var(--tint-warn);
  color: var(--failed);
}

/* The per-item controls sit on one line and wrap as a group on a narrow viewport. */
.compliance-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}

/* Sized to the row rather than the .input default of 100%, which would push both buttons onto
   their own line on every item. */
.compliance-reason {
  width: auto;
  flex: 1 1 24ch;
  min-width: 0;
}

/* --text-caption on a control, which is below the body size — allowed here for the reason
   .assignments-action states: these are one or two words on a button whose tap target is set by
   the padding rather than by the type, and both stay at or above the 12px floor. */
.compliance-action {
  font-size: var(--text-caption);
  padding: var(--space-2) var(--space-3);
}

.compliance .save-state { margin-top: var(--space-4); }

/* ── mobile density ────────────────────────────────────────────────────────────────────── */

/* Under the one-column breakpoint the rail sits above the work, so every wasted pixel in it
   pushes the inputs further off the first screen. Measured before: the rail alone consumed
   most of a 390px viewport's height. */
@media (max-width: 859px) {
  .rail { padding: var(--space-3); }
  .client-row { margin-bottom: 0; }
  .topbar { margin-bottom: var(--space-6); }
  .page-head { margin-bottom: var(--space-8); }
  /* 64px between zones is right on a desktop, where an act is about one screenful. On a phone
     it is most of a viewport of nothing, and the argument above applies unchanged: every wasted
     pixel pushes the work further off the first screen. --space-12 is the step that still reads
     as a zone break at this width. */
  .act + .act { margin-top: var(--space-12); }
  .send-preview { margin-top: var(--space-12); }
  /* The ramp's top step is a display size: 35px over a 360px viewport takes three lines for
     "Client knowledge" and its subtitle before any work is visible. This is the shared element
     selector again, so /clients, /counts and 404.html step down with it — correct and
     consistent, and the first rule to look at if #61 reports a heading problem. */
  h1 { font-size: var(--text-h1); }
}

/* ── motion, defined once ──────────────────────────────────────────────────────────────── */

/* Every transition in this file lives here and nowhere else (#58).

   This used to be the other way round: six transition declarations scattered through the
   rules above, neutralised by a blanket `reduce` block with `!important`. That is opt-OUT — a
   transition added anywhere was live by default and the guard was a sledgehammer. Opt-in means
   a transition added to a rule above is a test failure (test/chrome.test.js) rather than a live
   animation nobody chose. prep.css already worked this way; this is that idiom, ported.

   The `no-preference` edge, stated precisely rather than as folklore: `no-preference` is NOT the
   negation of `reduce`. A user agent that reports no support for the preference at all matches
   neither, so it matches nothing here and gets no transition — every element renders at its
   final state immediately. That is the correct outcome IN THIS FILE specifically, because
   nothing in this repo waits on `transitionend` or `animationend` (app.js:410-426 reveals an act
   with a double requestAnimationFrame and removes .is-entering unconditionally). It is not a
   general licence: a file that ever gates visibility on a transition completing would strand an
   element at opacity 0 under exactly this condition. */

@media (prefers-reduced-motion: no-preference) {
  .topbar-nav a { transition: color var(--duration-1) var(--ease-out); }

  .btn {
    transition: border-color var(--duration-1) var(--ease-out),
                transform var(--duration-1) var(--ease-out),
                box-shadow var(--duration-1) var(--ease-out);
  }

  .client-row { transition: background-color var(--duration-1) var(--ease-out); }
  .file-input::file-selector-button { transition: border-color var(--duration-1) var(--ease-out); }

  /* The two authored moments, both at the slower step. The act's is opacity and transform —
     the two properties named explicitly rather than `all`, which CRAFT forbids, matching the
     three-property list .btn carries above. Both are compositor properties. */
  .save-state { transition: opacity var(--duration-2) var(--ease-out); }
  .act {
    transition: opacity var(--duration-2) var(--ease-out),
                transform var(--duration-2) var(--ease-out);
  }

  /* The expander's one authored moment: the evidence arrives rather than simply being there.
     Opacity and transform only, the same compositor pair .act takes above. The details element's
     own height is deliberately NOT animated — that needs ::details-content and interpolate-size,
     and a no-build static deployment has no browserslist and no way to state a browser floor, so
     a support gamble is traded for a motion that degrades to nothing rather than to half of
     itself. The @keyframes is nested INSIDE this guard on purpose: test/chrome.test.js strips
     this block and then forbids the word anywhere that survives, which is the whole file. */
  @keyframes evidence-in {
    from { opacity: 0; transform: translateY(calc(var(--space-1) * -1)); }
  }

  .claim-evidence[open] .claim-source {
    animation: evidence-in var(--duration-2) var(--ease-out);
  }
}
