/* Project Control — modern operator console.
   Neutral cool-grey world (as before), rebuilt as a modern UI: a layered elevation system,
   one accent used only for meaning, a proper component vocabulary with every interactive
   state, and a tight typographic rhythm. No build step — plain CSS by product constraint.

   Palette is the original neutral: cool archive greys with a single blue ink for the
   machine's voice and the current selection, red for fault, green for settled work.
   Light is primary; dark is the desk's stored option. */

:root {
  color-scheme: light;   /* the default: light, until the operator says otherwise */

  /* surfaces, layered: ground → panel → raised → sunken */
  --bg:        #eef0ef;
  --surface:   #fbfcfb;
  --raised:    #ffffff;
  --sunken:    #f2f4f3;
  --hover:     #f0f2f1;

  /* type */
  --ink:       #14171b;
  --ink-2:     #454a51;
  --ink-3:     #5f656d;
  /* #6e757d measured 4.07:1 on --bg and 4.22:1 on --sunken — below the AA floor for the
     small metadata sizes this token mostly sets. #656b73 holds ≥4.7:1 on all four light
     surfaces and keeps a visible step below --ink-3 (#5f656d, ≥5.1:1). */
  --ink-4:     #656b73;

  /* lines */
  --line:      #dfe3e2;
  --line-2:    #cbd1cf;
  --line-3:    #b0b7b4;

  /* one accent, used for meaning only */
  --accent:      #1b4d8f;
  --accent-hi:   #163f76;
  --accent-soft: #e8eff9;
  --accent-line: #b9cee9;

  --ok:        #1f6b45;
  --ok-soft:   #e6f2ea;
  --ok-line:   #a9cdb8;

  --warn:      #8a5a0b;
  --warn-soft: #fbf0dc;
  --warn-line: #e3c78f;

  --danger:      #a32a1f;
  --danger-hi:   #8a231a;
  --danger-soft: #fbeae7;
  --danger-line: #e8b7b0;

  --ui: "Public Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;

  --s1: 4px; --s2: 8px; --s3: 12px; --s4: 16px; --s5: 24px; --s6: 32px;
  /* the rail holds a case name, a state chip and a two-line destination — at the desk's 110%
     scale it needs the room, or the destination is ellipsised to nothing */
  --rail: 380px;

  --r-sm: 6px; --r-md: 8px; --r-lg: 12px; --r-xl: 16px;
  --r-pill: 999px;

  /* elevation: offset + soft blur, never a halo */
  --e1: 0 1px 2px rgba(20, 23, 27, 0.06);
  --e2: 0 2px 6px rgba(20, 23, 27, 0.08), 0 1px 2px rgba(20, 23, 27, 0.04);
  --e3: 0 8px 24px rgba(20, 23, 27, 0.10), 0 2px 6px rgba(20, 23, 27, 0.05);
  --e4: 0 24px 56px rgba(20, 23, 27, 0.20), 0 6px 16px rgba(20, 23, 27, 0.08);

  --ring: 0 0 0 3px rgba(27, 77, 143, 0.18);
  --dur: 150ms;
  --ease: cubic-bezier(0.2, 0.8, 0.2, 1);
}

/* The dark palette applies when the operator chose dark, OR when they are on "system" and the OS
   says dark. One palette, two triggers — the colours are never duplicated. */
[data-theme="dark"],
[data-theme="system"][data-prefers="dark"] {
  color-scheme: dark;

  --bg:        #16191c;
  --surface:   #1c2024;
  --raised:    #232830;
  --sunken:    #191d21;
  --hover:     #262b30;

  --ink:       #eef0f2;
  --ink-2:     #b7bdc4;
  --ink-3:     #9aa1a9;
  --ink-4:     #868d95;

  --line:      #2a3037;
  --line-2:    #363d45;
  --line-3:    #4a525b;

  --accent:      #7dabef;
  --accent-hi:   #93bbf4;
  --accent-soft: #1a2534;
  --accent-line: #2f4a6d;

  --ok:        #6fc094;
  --ok-soft:   #14271d;
  --ok-line:   #2c5140;

  --warn:      #e0b467;
  --warn-soft: #2a2113;
  --warn-line: #55492c;

  --danger:      #ef897a;
  --danger-hi:   #f5a396;
  --danger-soft: #2b1a17;
  --danger-line: #5d332c;

  --e1: 0 1px 2px rgba(0, 0, 0, 0.4);
  --e2: 0 2px 6px rgba(0, 0, 0, 0.45), 0 1px 2px rgba(0, 0, 0, 0.3);
  --e3: 0 8px 24px rgba(0, 0, 0, 0.5), 0 2px 6px rgba(0, 0, 0, 0.3);
  --e4: 0 24px 56px rgba(0, 0, 0, 0.6), 0 6px 16px rgba(0, 0, 0, 0.4);

  --ring: 0 0 0 3px rgba(125, 171, 239, 0.24);
}

* { box-sizing: border-box; }
[hidden] { display: none !important; }
.defs { position: absolute; width: 0; height: 0; overflow: hidden; }

/* visually hidden, still announced */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
/* keyboard-only skip link, revealed on focus */
.skip {
  position: absolute; left: var(--s3); top: -48px; z-index: 100;
  background: var(--accent); color: #fff; font-size: 13px; font-weight: 600;
  padding: 8px 14px; border-radius: var(--r-md); text-decoration: none;
  transition: top var(--dur) var(--ease);
}
.skip:focus { top: var(--s3); }

/* --------------------------------------------------------------------- sizing
 *
 * The desk reads at 110% of its authored size. Everything is authored in px, so the zoom is
 * a single root-level scale rather than a pass over 380 measurements: `zoom` on :root scales
 * the whole layout — text, controls, spacing, the grid — and the browser recomputes the
 * viewport in the same units, so nothing overflows and nothing gets clipped at the edges.
 *
 * 110% was asked for; --zoom is the one number to change if that changes.
 */
:root { zoom: var(--zoom, 1.1); }

html, body { height: 100%; }
/* An app shell: the desk owns every scrollbar (the rail and the sheet each scroll inside
   themselves), so the page itself never scrolls. Without this the browser's own scrollable
   area picks up the height of the long step card under `zoom` and the whole console can be
   dragged off-screen even though every panel is correctly contained. */
html, body { overflow: hidden; }
body {
  margin: 0;
  font-family: var(--ui);
  font-size: 14px;
  line-height: 1.5;
  color: var(--ink);
  background: var(--bg);
  display: grid;
  grid-template-rows: auto 1fr;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  touch-action: manipulation;
}
.mono, input, pre, textarea { font-family: var(--mono); font-feature-settings: "tnum" 1, "zero" 1; }

::selection { background: var(--accent-soft); color: var(--ink); }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--r-sm); }
a { color: var(--accent); text-underline-offset: 3px; }

h1, h2, h3 { text-wrap: balance; }

/* ------------------------------------------------------------- top bar */

.bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s5);
  padding: 0 var(--s4);
  height: 56px;
  background: var(--surface);
  border-bottom: 1px solid var(--line);
  position: relative;
  z-index: 10;
}
.mark { display: flex; align-items: center; gap: var(--s3); min-width: 0; cursor: pointer; border-radius: var(--r-md); padding: 6px 10px; margin: -6px 0 -6px -10px; transition: background var(--dur) var(--ease); }
.mark:hover { background: var(--hover); }
.mark-rose { width: 26px; height: 26px; flex: none; color: var(--accent); opacity: .85; }
.mark-text { display: flex; align-items: baseline; gap: var(--s3); min-width: 0; }
.mark-name { font-weight: 680; letter-spacing: -0.02em; font-size: 15px; }
.mark-sub {
  font-size: 13px; color: var(--ink-3);
  padding-left: var(--s3); border-left: 1px solid var(--line-2);
  white-space: nowrap;
}
.bar-meta { display: flex; align-items: center; gap: var(--s5); font-size: 13px; }
.kv { display: inline-flex; align-items: baseline; gap: 6px; }
.kv .k {
  color: var(--ink-3); text-transform: uppercase;
  letter-spacing: 0.07em; font-size: 11px; font-weight: 600;
}
.kv b { font-weight: 620; font-family: var(--mono); font-size: 13px; color: var(--ink); }
.bar-tools { display: inline-flex; align-items: center; gap: var(--s1); }

/* --------------------------------------------------------------- buttons */

.btn {
  font: inherit;
  font-size: 13px;
  font-weight: 560;
  letter-spacing: -0.005em;
  color: var(--ink);
  background: var(--raised);
  border: 1px solid var(--line-2);
  border-radius: var(--r-md);
  padding: 0 12px;
  height: 32px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  white-space: nowrap;
  box-shadow: var(--e1);
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease),
              color var(--dur) var(--ease), box-shadow var(--dur) var(--ease),
              transform var(--dur) var(--ease);
}
.btn:hover:not(:disabled) { background: var(--hover); border-color: var(--line-3); }
.btn:active:not(:disabled) { transform: translateY(1px); box-shadow: none; }
.btn:disabled {
  color: var(--ink-4); background: var(--sunken);
  border-color: var(--line); cursor: not-allowed; box-shadow: none;
}
.btn.primary {
  background: var(--accent); border-color: var(--accent); color: #fff;
  box-shadow: var(--e1), inset 0 1px 0 rgba(255,255,255,0.10);
}
.btn.primary:hover:not(:disabled) { background: var(--accent-hi); border-color: var(--accent-hi); }
.btn.primary:disabled { background: var(--sunken); border-color: var(--line); color: var(--ink-4); box-shadow: none; }
.btn.ghost {
  border-color: transparent; background: transparent; color: var(--ink-2);
  box-shadow: none; padding: 0 10px;
}
.btn.ghost:hover:not(:disabled) { background: var(--hover); border-color: transparent; color: var(--ink); }
.btn.danger { color: var(--danger); background: var(--raised); border-color: var(--line-2); }
.btn.danger:hover:not(:disabled) { background: var(--danger-soft); border-color: var(--danger-line); color: var(--danger); }
.btn.danger-solid { background: var(--danger); border-color: var(--danger); color: #fff; }
.btn.danger-solid:hover:not(:disabled) { background: var(--danger-hi); border-color: var(--danger-hi); }
.btn.icon { padding: 0; width: 32px; }
.btn.sm { height: 28px; font-size: 13px; padding: 0 10px; }
.glyph-svg { width: 15px; height: 15px; flex: none; }

/* ------------------------------------------------------------------ desk */

.desk {
  display: grid;
  grid-template-rows: auto 1fr;
  min-height: 0;
  overflow: hidden;
  background: var(--bg);
}
.col { min-height: 0; overflow: auto; }
/* the two columns' content sat flush on their own borders (the audit's "cramped padding" on
   `section.col`): the head already owns its bottom hairline, but the queue's rows began
   immediately on the right border and the sheet's card on the top-left corner. A small inner
   gutter on both gives every column's content its own air without moving either border. */
.queue { padding-left: 1px; }
.sheet { padding: 1px 0 0 1px; }

/* the rail sits on the sunken surface, separated by a hairline */
.queue {
  background: var(--surface);
  border-right: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.col-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  padding: 0 var(--s4) var(--s2) var(--s4);
  min-height: 44px;
  border-bottom: 1px solid var(--line);
  position: sticky;
  top: 0;
  background: var(--surface);
  z-index: 2;
  flex: none;
}
.col-head h2 {
  margin: 0;
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--ink-3);
  font-weight: 660;
}

.sheet {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--surface);
}
.sheet-scroll {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
}

/* -------------------------------------------------------------- cause list */
.rail-nav { display: flex; gap: 2px; padding: var(--s2) var(--s3) 0; }
.rail-tab {
  flex: 1; font: inherit; font-size: 13px; font-weight: 620;
  padding: 7px 10px; border-radius: var(--r-sm);
  color: var(--ink-2); background: transparent; border: 1px solid transparent;
  cursor: pointer; transition: background var(--dur) var(--ease), color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.rail-tab:hover { background: var(--hover); color: var(--ink); }
.rail-tab.on {
  color: var(--accent); background: var(--accent-soft); border-color: var(--accent-line);
}

.queue-list { list-style: none; margin: 0; padding: var(--s2); display: flex; flex-direction: column; gap: 2px; }
.entry {
  display: grid;
  grid-template-columns: 26px minmax(0, 1fr);
  gap: 0 var(--s2);
  align-items: start;
  width: 100%;
  text-align: left;
  font: inherit;
  color: var(--ink);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--r-md);
  padding: 8px 10px;
  cursor: pointer;
  position: relative;
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.entry:hover { background: var(--hover); }
.entry[aria-current="true"] {
  background: var(--accent-soft);
  border-color: var(--accent-line);
}
.entry-idx {
  font-family: var(--mono); font-size: 12px; color: var(--ink-4);
  text-align: right; padding-top: 3px;
  font-feature-settings: "tnum" 1; font-weight: 500;
  grid-row: 1 / span 2;
}
.entry[aria-current="true"] .entry-idx { color: var(--accent); font-weight: 700; }
.entry-main { min-width: 0; grid-column: 2; display: block; }
.entry-name {
  display: block; font-weight: 600; font-size: 13px; line-height: 1.35;
  overflow-wrap: anywhere;
  /* one line, ellipsised — the rail is a scannable index, not a document */
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.entry-meta {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: 6px 8px; margin-top: 4px; min-width: 0;
}
.entry-id {
  font-size: 11px; color: var(--ink-4); overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; min-width: 0;
}
.entry-sub {
  display: block; font-size: 12px; color: var(--ink-3);
  line-height: 1.4;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  min-width: 0;
  /* a long destination gets its own row rather than being ellipsised to nothing beside the
     state chip — one line of text is a better index than two words and an ellipsis */
  flex: 1 1 62%;
}

/* state = a small tinted chip, coloured by meaning */
.state {
  font-family: var(--mono);
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-3);
  background: var(--sunken);
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  padding: 2px 8px;
  white-space: nowrap;
  font-weight: 620;
  flex: none;
  line-height: 1.5;
}
.state[data-state="review"],
.state[data-state="running"]   { color: var(--accent); background: var(--accent-soft); border-color: var(--accent-line); }
.state[data-state="ready"],
.state[data-state="printed"],
.state[data-state="shipped"],
.state[data-state="confirmed"] { color: var(--ok); background: var(--ok-soft); border-color: var(--ok-line); }
.state[data-state="error"]     { color: var(--danger); background: var(--danger-soft); border-color: var(--danger-line); }
.state[data-state="confirmed"] { color: var(--warn); background: var(--warn-soft); border-color: var(--warn-line); }
.state[data-state="running"]   { border-style: dashed; }

/* AN INFORMAL JOB'S STAGE, coloured like Hague's states are.
 *
 * Kazim, 2026-09-26: *"in the informal tab the steps shown that you've added, its all grey, make it
 * grey for the draft but make it coloured for the other steps, like Hague."*
 *
 * `intake` is the draft rung and it is deliberately ABSENT here, so it keeps the bare `.state` grey
 * — a job with nothing on it yet should not look like progress. Every later rung is coloured, and
 * the palette is taken from the tokens the state rules above already use: progress is accent, work
 * waiting on a human is warn, and finished is ok.
 *
 * The keys are the STAGE keys the server sends (`informal_intake.STAGES`), not the words — the words
 * are the operator's and could be re-worded without breaking a colour. */
.state[data-state="details"]   { color: var(--accent); background: var(--accent-soft); border-color: var(--accent-line); }
.state[data-state="delivery"]  { color: var(--warn); background: var(--warn-soft); border-color: var(--warn-line); }
.state[data-state="affidavit"] { color: var(--accent); background: var(--accent-soft); border-color: var(--accent-line); }
.state[data-state="done"]      { color: var(--ok); background: var(--ok-soft); border-color: var(--ok-line); }

/* -------------------------------------------------------------- case head */

/* Phone-only: the drill-down's way back. On the desk the list is right there in the rail, so a
   "◀ Cases" control would be a second navigation for a journey that does not exist. Hidden at the
   BASE level, because a rule that only exists inside the phone media query leaves the element
   falling back to `inline-block` on the desk. */
.case-back { display: none; }

.case-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s4);
  /* a compact strip: it sits above a step rail, and the two together were eating the top of the
     case. The id is secondary to the name, so it goes underneath at a small size rather than
     costing a whole line of its own. */
  padding: 10px var(--s5) 8px;
  border-bottom: 1px solid var(--line);
  background: var(--surface);
  position: sticky;
  top: 0;
  z-index: 3;
  flex: none;
}
.case-title { min-width: 0; }
.case-id {
  margin: 0 0 1px;
  font-size: 11px;
  letter-spacing: 0.01em;
  color: var(--ink-4);
  font-family: var(--mono);
  font-weight: 500;
}
.case-head h1 {
  margin: 0;
  font-size: 20px;
  letter-spacing: -0.025em;
  line-height: 1.2;
  font-weight: 680;
  overflow-wrap: anywhere;
}
.case-actions { display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap; justify-content: flex-end; flex: none; }
/* Phone-only: the foot of the case carries "Delete this case" there. On the desk the header has
   the button, so this row must not render at all. */
.danger-foot { display: none; }

.banner {
  margin: 0;
  padding: 10px var(--s5);
  font-size: 13px;
  background: var(--danger-soft);
  color: var(--danger);
  border-bottom: 1px solid var(--danger-line);
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--s3);
  flex: none;
}
.banner[data-kind="info"] { background: var(--accent-soft); color: var(--accent); border-bottom-color: var(--accent-line); }
.banner-close {
  font: inherit; line-height: 1; color: inherit;
  background: transparent; border: 1px solid transparent;
  border-radius: var(--r-sm); padding: 2px; cursor: pointer; opacity: .75;
  display: inline-flex; align-items: center;
}
.banner-close:hover { opacity: 1; background: rgba(0,0,0,0.06); }

/* ------------------------------------------------------- stepper (segmented) */

.stepper {
  display: flex;
  align-items: center;
  gap: 2px;
  padding: var(--s3) var(--s5) var(--s2);
  background: var(--surface);
  flex: none;
  overflow-x: auto;
}
.step {
  font: inherit;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 7px 14px 7px 11px;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--r-md);
  color: var(--ink-3);
  cursor: pointer;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease),
              border-color var(--dur) var(--ease);
  white-space: nowrap;
}
.step:hover:not(.on) { background: var(--hover); color: var(--ink-2); }
.step-n {
  font-family: var(--mono);
  font-size: 12px;
  width: 20px; height: 20px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--line-3);
  border-radius: var(--r-pill);
  flex: none;
  font-weight: 620;
  color: var(--ink-4);
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease), color var(--dur) var(--ease);
}
.step-l { font-size: 13px; font-weight: 580; }
.step.on {
  color: var(--accent);
  background: var(--accent-soft);
  border-color: var(--accent-line);
}
.step.on .step-n {
  background: var(--accent); border-color: var(--accent); color: #fff;
}
/* completed steps are marked with a subdued check, never a loud fill — only the current
   step is highlighted (accent); future steps stay grey. */
.step.done { color: var(--ink-2); }
.step.done .step-n {
  background: var(--sunken); border-color: var(--line-3); color: var(--ink-3);
  position: relative; font-size: 0;
}
.step.done .step-n::after {
  content: ""; position: absolute; inset: 0;
  -webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'><path d='M2.4 7.6 5.5 10.7 11.6 3.4' fill='none' stroke='black' stroke-width='2.2'/></svg>") center / 11px 11px no-repeat;
  mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'><path d='M2.4 7.6 5.5 10.7 11.6 3.4' fill='none' stroke='black' stroke-width='2.2'/></svg>") center / 11px 11px no-repeat;
  background-color: var(--ink-3);
}
.step-arrow { display: inline-flex; align-items: center; color: var(--line-3); padding: 0 1px; flex: none; }
.step-arrow svg { width: 12px; height: 12px; }

.step-caption {
  margin: 0;
  padding: var(--s2) var(--s5) var(--s3);
  font-size: 13px;
  color: var(--ink-2);
  background: var(--surface);
  border-bottom: 1px solid var(--line);
  max-width: 84ch;
}

/* ------------------------------------------------------------ stage card */

.stage { padding: var(--s4) var(--s5) var(--s5); }
.step-card-head { margin-bottom: var(--s3); }
.step-card-head h3 {
  margin: 0 0 6px;
  font-size: 15px;
  font-weight: 680;
  letter-spacing: -0.015em;
}
.step-card-head p { margin: 0; font-size: 13px; color: var(--ink-2); max-width: 78ch; }
.stage-btnrow { display: flex; gap: var(--s2); flex-wrap: wrap; margin: var(--s3) 0; }

.instruct-label {
  display: block;
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--ink-3);
  font-weight: 660;
  margin-bottom: 6px;
}

/* intake: context + files, one message */
#intake-context, #chat-input {
  font: inherit;
  font-family: var(--mono);
  font-size: 13px;
  width: 100%;
  padding: 10px 12px;
  color: var(--ink);
  background: var(--raised);
  border: 1px solid var(--line-2);
  border-radius: var(--r-md);
  resize: vertical;
  min-height: 56px;
  line-height: 1.55;
  transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
#intake-context:focus-visible, #chat-input:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: var(--ring);
}
.intake-actions { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; margin: var(--s3) 0; }
.intake-picked { font-size: 12px; color: var(--ink-3); }
.instruct-hint { margin: 6px 0 var(--s3); font-size: 12px; color: var(--ink-3); }

.filelist {
  list-style: none; margin: 0; padding: 0;
  border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--raised); overflow: hidden;
}
.filerow {
  display: grid;
  /* kind | file name | what the operator calls the document | remove */
  grid-template-columns: 62px minmax(0, 1fr) minmax(0, 1fr) auto;
  gap: var(--s3);
  align-items: center;
  padding: 9px 12px;
  border-bottom: 1px solid var(--line);
}
.filerow:last-child { border-bottom: 0; }
.filerow.empty-row {
  display: block; color: var(--ink-3); font-size: 13px;
  border-bottom: 0; padding: var(--s3);
}
.fkind {
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--ink-3); font-weight: 640;
}
.fname { font-size: 13px; overflow-wrap: anywhere; color: var(--ink); }
.fdel {
  font: inherit; font-family: var(--ui); font-size: 12px; font-weight: 560;
  color: var(--danger); background: transparent;
  border: 1px solid transparent; border-radius: var(--r-sm);
  padding: 3px 8px; cursor: pointer;
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.fdel:hover { background: var(--danger-soft); border-color: var(--danger-line); }

/* --------------------------------------------------------- the field sheet */

.group {
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--raised);
  overflow: hidden;
  margin-bottom: var(--s4);
  box-shadow: var(--e1);
}
.group > h3 {
  margin: 0;
  padding: 10px var(--s4);
  font-size: 12px;
  font-weight: 680;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink-3);
  background: var(--sunken);
  border-bottom: 1px solid var(--line);
}
.row {
  display: grid;
  grid-template-columns: 200px minmax(190px, 1fr);
  gap: var(--s4);
  padding: var(--s3) var(--s4);
  border-bottom: 1px solid var(--line);
}
.row:last-child { border-bottom: 0; }
.row-label { font-size: 13px; font-weight: 600; color: var(--ink); line-height: 1.4; }
.row-label .page {
  display: block; font-family: var(--mono); font-size: 12px; color: var(--ink-4);
  font-weight: 400; margin-top: 3px;
}
.value { display: block; width: 100%; min-width: 0; }
.value input, .value textarea {
  font: inherit;
  font-family: var(--mono);
  font-size: 13px;
  width: 100%;
  padding: 8px 10px;
  color: var(--ink);
  background: var(--raised);
  border: 1px solid var(--line-2);
  border-radius: var(--r-md);
  resize: vertical;
  line-height: 1.55;
  transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease), background var(--dur) var(--ease);
}
.value textarea { min-height: 64px; }
.value input:hover:not([readonly]), .value textarea:hover:not([readonly]) { border-color: var(--line-3); }
.value input:focus-visible, .value textarea:focus-visible {
  outline: none; border-color: var(--accent); box-shadow: var(--ring);
}
.value input[readonly], .value textarea[readonly] {
  background: var(--sunken); color: var(--ink-2); border-color: var(--line);
}

/* confidence: a small tinted chip; medium carries a subtle hatch */
.provenance {
  display: flex; gap: var(--s2); align-items: center;
  flex-wrap: wrap; min-width: 0; margin-top: 8px;
}
.conf-pill {
  font: inherit;
  font-family: var(--mono);
  font-size: 12px;
  font-weight: 660;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ink-2);
  background: var(--sunken);
  border: 1px solid var(--line-2);
  border-radius: var(--r-pill);
  padding: 3px 10px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease),
              color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.conf-pill:hover { border-color: var(--line-3); box-shadow: var(--e1); }
.conf-pill.lookup { cursor: default; color: var(--ink-4); background: transparent; border-style: dashed; }
.conf-pill.lookup:hover { box-shadow: none; border-color: var(--line-2); }
.conf-caret { font-size: 9px; line-height: 1; opacity: .75; }
.conf-pill[data-c="high"]   { background: var(--ok-soft); border-color: var(--ok-line); color: var(--ok); }
.conf-pill[data-c="medium"] { background: var(--warn-soft); border-color: var(--warn-line); color: var(--warn); }
.conf-pill[data-c="low"]    { background: var(--danger-soft); border-color: var(--danger-line); color: var(--danger); }

.reason {
  margin: 8px 0 0;
  padding: 10px 12px;
  background: var(--sunken);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  font-family: var(--mono);
  font-size: 12px;
  line-height: 1.6;
  color: var(--ink-2);
  white-space: pre-wrap;
  word-break: break-word;
}
.provenance .src { color: var(--ink-4); font-size: 12px; font-family: var(--mono); }
.cand { margin: var(--s2) 0 0; padding: 0; list-style: none; }
.cand li { padding: 5px 0; border-bottom: 1px solid var(--line); font-size: 13px; }
.cand li:last-child { border-bottom: 0; }
.notes {
  margin: 0; padding: var(--s3) var(--s4) var(--s4);
  list-style: none; font-size: 13px; color: var(--ink-2);
}
.notes li { padding: 5px 0 5px 18px; position: relative; overflow-wrap: anywhere; }
.notes li::before {
  content: ""; position: absolute; left: 0; top: 13px;
  width: 6px; height: 4px; border-left: 1.5px solid var(--ink-4);
  border-bottom: 1.5px solid var(--ink-4);
  transform: rotate(-45deg);
}

/* --------------------------------------------------------- print assembly */

.assembly {
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--raised);
  margin-top: var(--s4);
  overflow: hidden;
  box-shadow: var(--e1);
}
.assembly h2 {
  margin: 0; padding: 10px var(--s4);
  font-size: 11px; text-transform: uppercase;
  letter-spacing: 0.1em; color: var(--ink-3); font-weight: 680;
  background: var(--sunken); border-bottom: 1px solid var(--line);
}
.manifest { margin: 0; padding: var(--s3) var(--s4); font-size: 12px; white-space: pre-wrap; color: var(--ink-2); }

/* ------------------------------------------------------- courier / waybill */

.verify-waybill {
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--raised);
  margin-bottom: var(--s4);
  overflow: hidden;
  box-shadow: var(--e1);
}
.verify-head {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s3);
  padding: 10px var(--s4); border-bottom: 1px solid var(--line);
  background: var(--sunken);
}
.verify-head .instruct-label { margin-bottom: 0; }
.waybill-actions { display: inline-flex; gap: var(--s1); }
.waybill {
  margin: 0; padding: var(--s3) var(--s4);
  max-height: 320px; overflow: auto;
  font-size: 12px; line-height: 1.65; white-space: pre-wrap; color: var(--ink);
  background: var(--raised);
}
.apply {
  display: flex; align-items: center; gap: var(--s2);
  padding: var(--s3) var(--s4); border-top: 1px solid var(--line);
  font-size: 13px; color: var(--ink-2); background: var(--sunken);
}
.ship-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s3); margin-top: var(--s3); }
.cf { display: block; min-width: 0; }
.cf.wide { grid-column: 1 / -1; }
.cf > span {
  display: block; font-size: 11px; text-transform: uppercase;
  letter-spacing: 0.08em; color: var(--ink-3); margin-bottom: 5px; font-weight: 640;
}
.cf input, .cf textarea {
  font: inherit; font-family: var(--mono); font-size: 13px; width: 100%;
  padding: 8px 10px; color: var(--ink); background: var(--raised);
  border: 1px solid var(--line-2); border-radius: var(--r-md); resize: vertical;
  transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.cf input:focus-visible, .cf textarea:focus-visible { outline: none; border-color: var(--accent); box-shadow: var(--ring); }
.courier-actions { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; margin-top: var(--s4); }
.courier-state { font-size: 12px; color: var(--ink-3); margin-left: auto; }
.courier-state[data-kind="ok"] { color: var(--ok); }
.courier-state[data-kind="notsent"] { color: var(--danger); font-weight: 640; }
.courier-note { font-size: 13px; color: var(--danger); margin: var(--s2) 0 0; }

/* ---------------------------------------------------------- activity feed */

/* `hidden` must win over the flex sizing below, or the feed leaves an empty band at the foot of
   the steps that do not carry it. `[hidden]` is only `display: none` at UA priority. */
.chat-wrap[hidden] { display: none !important; }

.chat-wrap {
  border-top: 1px solid var(--line);
  background: var(--surface);
  display: flex;
  flex-direction: column;
  margin-top: auto;
  flex: none;
}
body.chat-expanded .queue { display: none; }
/* the columns live on `.desk-body`, not `.desk` (which is a row grid) — targeting `.desk` here
   did nothing, so the feed stayed inside the rail's 380px track */
body.chat-expanded .desk-body { grid-template-columns: minmax(0, 1fr); }
body.chat-expanded .desk { grid-template-rows: auto minmax(0, 1fr); }
body.chat-expanded .sheet { min-width: 0; }
body.chat-expanded .sheet-scroll { overflow: hidden; }
body.chat-expanded .stage { display: none; }
body.chat-expanded #sheet-scroll { justify-content: flex-start; }
/* in expanded view the chat-wrap must take the full sheet height and let the feed scroll
   inside it — otherwise it grows to content height and the overflow:hidden clips it, and
   the whole panel is unscrollable (the "expanded chat is buggy / won't scroll" bug). */
body.chat-expanded .chat-wrap { flex: 1 1 auto; min-height: 0; }
body.chat-expanded .chat { min-height: 0; }

body.chat-running .stage { padding-bottom: 0; }
body.chat-running .running-head { padding-bottom: var(--s2); }
/* during a reading the chat becomes the scroller: the feed scrolls inside it and the
   composer stays pinned at the bottom — scrolling moves the agent's output, never the
   prompt row.
   THE HEIGHT MUST BE REAL. `flex: 1` here set flex-BASIS 0%, and a basis of 0% beats the
   `height` declaration in a flex column — the feed never got its 55vh; it got whatever was
   LEFT after the stage card, which is how a correction sent from the review card squeezed the
   feed to a 92px strip (measured) and the whole page "compressed down weirdly". `flex: none`
   + an explicit height gives the feed a stable, generous panel regardless of the card above
   it; the page scrolls to reach the rest. */
body.chat-running .chat-wrap { flex: none; height: calc(55dvh / var(--zoom, 1.1)); min-height: 300px; }
body.chat-running .chat { max-height: none; min-height: 0; flex: 1 1 auto; }

.chat-head {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s3);
  padding: 10px var(--s5);
  border-bottom: 1px solid var(--line);
  background: var(--surface);
}
.chat-head h2 {
  margin: 0; font-size: 11px; text-transform: uppercase;
  letter-spacing: 0.09em; color: var(--ink-3); font-weight: 660;
}
.chat-note { font-size: 12px; color: var(--ink-3); margin-left: auto; }
.chat {
  padding: var(--s3) var(--s5);
  max-height: 300px;
  overflow: auto;
  overscroll-behavior: contain;
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  flex: 1;
}
/* A flex column shrinks its children when the content is taller than the box. These entries
   are cards with `overflow: hidden`, so shrinking flattened each one to a 2px sliver and the
   feed read as a series of stray horizontal rules cutting across the conversation. The feed
   scrolls; its rows must keep their natural height. */
.chat > * { flex: 0 0 auto; }
body.chat-expanded .chat { max-height: none; flex: 1; }
body.chat-expanded .chat-wrap { flex: 1 1 auto; min-height: 0; }
/* Expanded on a viewport-height shell: the feed takes what is left between the feed's own head
   and the composer, and scrolls inside itself. It must NOT keep a floor height — `min-height:
   60vh` is 540px on a 900px screen, which beats the flex shrink, so the feed grew past the shell
   and pushed the composer (and its Send) 199px below the fold. The phone block sets
   `min-height: 0` for this reason; the desktop path needed it too. */
body.case-open.chat-expanded .chat { min-height: 0; overflow-y: auto; }
body.case-open.chat-expanded .chat-compose { flex: none; }
.chat-empty { color: var(--ink-3); font-size: 13px; }

/* one agent activity entry */
.act {
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--raised);
  overflow: hidden;
  box-shadow: var(--e1);
}
.act + .act { margin-top: 0; }
.act-head {
  display: flex; align-items: center; gap: var(--s2);
  padding: 7px 10px;
  font-size: 12px; color: var(--ink-2);
  background: var(--sunken);
}
.act-icon { font-size: 12px; line-height: 1; flex: none; opacity: .8; }
.act-k {
  font-family: var(--mono); font-size: 11px; text-transform: uppercase;
  letter-spacing: 0.07em; color: var(--ink-3); font-weight: 680; flex: none;
  background: var(--raised); border: 1px solid var(--line-2);
  border-radius: var(--r-sm); padding: 1px 6px;
}
.act-title { font-weight: 560; min-width: 0; overflow-wrap: anywhere; font-size: 12px; }
.act-meta { margin-left: auto; font-family: var(--mono); font-size: 12px; color: var(--ink-4); white-space: nowrap; flex: none; }
.act-toggle {
  flex: none;
  font: inherit; font-family: var(--ui);
  font-size: 12px; font-weight: 560;
  color: var(--accent);
  background: transparent;
  border: 1px solid var(--line-2);
  border-radius: var(--r-sm);
  padding: 2px 8px;
  cursor: pointer;
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.act-toggle:hover { background: var(--accent-soft); border-color: var(--accent-line); }
.act-body {
  padding: 10px 12px;
  font-family: var(--mono);
  font-size: 12px;
  line-height: 1.6;
  color: var(--ink);
  white-space: pre-wrap;
  word-break: break-word;
  max-height: 340px;
  overflow: auto;
  background: var(--raised);
  border-top: 1px solid var(--line);
}
.act.collapsed .act-body { display: none; }

.chat-msg.agent {
  font-size: 13px; color: var(--ink); line-height: 1.6;
  padding: 2px 0; white-space: pre-wrap; word-break: break-word;
}
.chat-msg.user {
  align-self: flex-end;
  background: var(--accent);
  color: #fff;
  font-family: var(--ui);
  font-size: 13px;
  line-height: 1.5;
  padding: 9px 13px;
  border-radius: var(--r-lg) var(--r-lg) var(--r-sm) var(--r-lg);
  max-width: 78%;
  white-space: pre-wrap;
  word-break: break-word;
  box-shadow: var(--e1);
}
.chat-msg.system {
  align-self: flex-start;
  background: transparent;
  color: var(--ink-4);
  font-size: 12px;
  font-family: var(--mono);
  letter-spacing: 0.03em;
  padding: 1px 0;
}
.chat-compose { border-top: 1px solid var(--line); padding: var(--s3) var(--s5); background: var(--surface); }
.chat-row { display: grid; grid-template-columns: 1fr auto; gap: var(--s2); align-items: end; }
.chat-row textarea {
  font: inherit; font-family: var(--mono); font-size: 13px; width: 100%;
  padding: 9px 12px; color: var(--ink); background: var(--raised);
  border: 1px solid var(--line-2); border-radius: var(--r-md);
  resize: none; overflow: hidden; min-height: 42px;
  transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.chat-row textarea:focus-visible { outline: none; border-color: var(--accent); box-shadow: var(--ring); }
.chat-hint { margin: 6px 0 0; font-size: 12px; color: var(--ink-4); }
body.chat-expanded .chat { padding-bottom: var(--s5); }
/* "showing the last N of M" — a truncation the operator can see and act on, not a silent one */
.chat-log-note {
  margin: 0 0 var(--s2); padding: 6px 10px;
  font-family: var(--mono); font-size: 11px; color: var(--ink-3);
  background: var(--sunken); border: 1px dashed var(--line-2); border-radius: var(--r-md);
  display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap;
}
.chat-log-note .btn { flex: none; }

/* ------------------------------------------------------------ inline form */

.inline-form {
  margin: var(--s4) 0;
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--raised);
  overflow: hidden;
  box-shadow: var(--e1);
}
.inline-form[data-open="false"] .pager,
.inline-form[data-open="false"] .inline-page { display: none; }
.inline-form-head {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s3);
  padding: 10px var(--s4); border-bottom: 1px solid var(--line);
  background: var(--sunken);
}
.inline-form-head h3 {
  margin: 0; font-size: 11px; text-transform: uppercase;
  letter-spacing: 0.09em; color: var(--ink-3); font-weight: 660;
}
.inline-form-actions { display: inline-flex; gap: var(--s1); }
.inline-page { display: block; max-width: 100%; margin: 0 auto; background: #fff; width: auto; height: auto; }

/* ------------------------------------------------------------ form overlay */

.form-overlay {
  position: fixed; inset: 0;
  background: rgba(20, 23, 27, 0.55);
  display: flex; flex-direction: column;
  z-index: 40;
  padding: var(--s5);
  overscroll-behavior: contain;
}
.form-overlay-head {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s4);
  background: var(--surface);
  border: 1px solid var(--line);
  border-bottom: 0;
  border-radius: var(--r-xl) var(--r-xl) 0 0;
  padding: var(--s4);              /* was --s3: the title row sat 12px from the panel's own border */
  flex: none;
}
/* THE TITLE ROW MUST NOT WRAP ITS COUNTS.
   Seen in a dark-mode capture: `.fo-title` was a baseline flex row whose summary could shrink below
   its content, so "71 notifications · 71 unseen · 1 cleared" broke and left "· 1 cleared" orphaned
   on a second line while the heading stayed on the first. The heading holds its size; the summary
   takes the rest and keeps each count whole. */
/* THE SHARED RULE STAYS SHARED — this is the BASE for every overlay's title row.
   An earlier edit REPLACED this with a `#notify-overlay`-scoped version, which silently removed
   `display:flex` from every OTHER overlay's title row and changed the My Servers header from 63px to
   75px. `verify_my_servers_header.py` caught it, because it asserts the desk header height as well
   as its own card's fix. Fix a shared component by ADDING a scoped override, never by replacing the
   base. */
.fo-title { display: flex; align-items: baseline; gap: var(--s3); }
/* ONLY THE NOTIFICATIONS PANEL IS OVERRIDDEN. Its summary holds three counts; without this the flex
   row shrank it below its content and "1 cleared" wrapped onto a second line. The My Servers overlay
   has its own scoped rules for the same row (and `verify_my_servers_header.py` proves they do not
   leak), so nothing generic is changed here — a shared base rule must not be tightened for one
   caller, or every other caller's geometry moves with it. */
#notify-overlay .fo-title { min-width: 0; }
#notify-overlay .fo-title h2 { flex: none; }
#notify-overlay .fo-title .mono { flex: 0 1 auto; min-width: 0; }
.fo-title h2 { margin: 0; font-size: 15px; font-weight: 680; }
.fo-title .mono { font-size: 12px; color: var(--ink-4); }
.fo-actions { display: flex; gap: var(--s2); }
.form-overlay-body {
  background: var(--raised);
  border: 1px solid var(--line);
  border-radius: 0 0 var(--r-xl) var(--r-xl);
  overflow: auto;
  flex: 1;
  padding: var(--s4);
  overscroll-behavior: contain;
}
.pager { display: flex; align-items: center; gap: 5px; margin-bottom: var(--s3); }
.pager-label {
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.09em;
  color: var(--ink-3); font-weight: 640;
}
.pg {
  font: inherit; font-family: var(--mono); font-size: 12px; font-weight: 560;
  min-width: 30px; height: 28px; padding: 0 8px;
  background: var(--raised); color: var(--ink-2);
  border: 1px solid var(--line-2); border-radius: var(--r-sm); cursor: pointer;
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease), color var(--dur) var(--ease);
}
.pg:hover { border-color: var(--line-3); color: var(--ink); }
.pg.on { background: var(--accent); border-color: var(--accent); color: #fff; }
.fo-page {
  display: block; max-width: 100%; margin: 0 auto;
  background: #fff; box-shadow: var(--e3); width: auto; height: auto;
}

/* ------------------------------------------------------------------ modal */

.modal-wrap {
  position: fixed; inset: 0;
  background: rgba(20, 23, 27, 0.5);
  display: flex; align-items: center; justify-content: center;
  padding: var(--s4);
  z-index: 60;
  overscroll-behavior: contain;
}
.modal {
  background: var(--surface);
  border: 1px solid var(--line-2);
  border-radius: var(--r-xl);
  max-width: 520px; width: 100%;
  padding: var(--s5);
  box-shadow: var(--e3);
}
.modal h3 { margin: 0 0 var(--s3); font-size: 17px; font-weight: 680; letter-spacing: -0.02em; }
.modal-body {
  margin: 0 0 var(--s4); padding: var(--s3);
  background: var(--sunken); border: 1px solid var(--line);
  border-radius: var(--r-md);
  font-size: 13px; white-space: pre-wrap; color: var(--ink-2);
  line-height: 1.6;
}
.modal-type { margin-bottom: var(--s4); }
.modal-type label {
  display: block; font-size: 11px; text-transform: uppercase;
  letter-spacing: 0.08em; color: var(--ink-3); margin-bottom: 6px; font-weight: 650;
}
.modal-type input {
  font: inherit; font-family: var(--mono); font-size: 13px; width: 100%;
  padding: 9px 11px; border: 1px solid var(--line-2); border-radius: var(--r-md);
  background: var(--raised); color: var(--ink);
  transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.modal-type input:focus-visible { outline: none; border-color: var(--accent); box-shadow: var(--ring); }
.modal-actions { display: flex; justify-content: flex-end; gap: var(--s2); }

/* ------------------------------------------------------------------- misc */

.empty { padding: var(--s6) var(--s5); max-width: 56ch; }
.empty-line { margin: 0 0 var(--s2); font-size: 18px; font-weight: 680; letter-spacing: -0.02em; }
.empty-sub { margin: 0; color: var(--ink-2); font-size: 14px; }

/* ------------------------------------------------------- the workflow tabs
   Three workflows across the top. Hague is the left default. Each tab owns its own cases,
   its own search and its own create action. */

.wfbar {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s4); flex-wrap: wrap;
  padding: var(--s3) var(--s4);
  background: var(--surface);
  border-bottom: 1px solid var(--line);
}
.wf-tabs { display: flex; gap: 2px; padding: 3px; background: var(--sunken);
  border: 1px solid var(--line); border-radius: var(--r-md); }
.wf-tab {
  font: inherit; font-size: 13px; font-weight: 640;
  padding: 7px 18px; border-radius: var(--r-sm);
  color: var(--ink-2); background: transparent; border: 1px solid transparent;
  cursor: pointer;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.wf-tab:hover { background: var(--hover); color: var(--ink); }
.wf-tab.on {
  color: var(--accent); background: var(--raised);
  border-color: var(--line-2); box-shadow: var(--e1);
}
.wf-tab .wf-n {
  font-family: var(--mono); font-size: 11px; color: var(--ink-4);
  margin-left: 7px; font-weight: 600;
}
.wf-tab.on .wf-n { color: var(--accent); }
/* THE GREYED TAB — THE BLANKET'S ONE VISIBLE MARK (the operator's 2026-09-24 word: a
   blanket-blocked workflow is greyed out for the people it is blocked for, everyone else sees it
   normal). Greyness is the WHOLE statement, not a disabled control: the tab stays clickable (the
   work that exists is still readable) and the routes behind its acts still refuse (R14). The
   reduced contrast, not a cursor change and not `disabled`, because the reason travels on the
   tab's own title tooltip. */
.wf-tab.wf-blocked {
  color: var(--ink-4); background: transparent; border-color: transparent;
  cursor: pointer; opacity: .55;
}
.wf-tab.wf-blocked:hover { background: var(--hover); color: var(--ink-3); opacity: .75; }
.wf-tab.wf-blocked .wf-n { color: var(--ink-4); }

.wf-tools { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; }
/* Phone-only controls. Hidden here and switched on inside the phone media query, so nothing
   phone-shaped can ever render on the desk. */
.wf-menu, .wf-search-toggle, .wf-sheet-wrap { display: none; }
.search-wrap { position: relative; display: flex; align-items: center; }
.search {
  font: inherit; font-family: var(--mono); font-size: 13px;
  width: 30ch; min-width: 18ch; padding: 8px 30px 8px 12px;
  background: var(--raised); color: var(--ink);
  border: 1px solid var(--line-2); border-radius: var(--r-md);
  transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.search::placeholder { color: var(--ink-4); font-size: 12px; }
.search:focus-visible { outline: none; border-color: var(--accent); box-shadow: var(--ring); }
.search-clear {
  position: absolute; right: 4px; width: 22px; height: 22px;
  font: inherit; font-size: 12px; line-height: 1;
  display: flex; align-items: center; justify-content: center;
  color: var(--ink-3); background: transparent; border: 0; border-radius: var(--r-pill);
  cursor: pointer;
}
.search-clear:hover { background: var(--hover); color: var(--ink); }

/* the desk body under the tab bar: rail + sheet */
.desk-body {
  display: grid; grid-template-columns: var(--rail) 1fr;
  min-height: 0; overflow: hidden;
}

/* the rail carries whatever the selected tab shows */
.rail-body { flex: 1; min-height: 0; overflow-y: auto; padding: var(--s2); }
.rail-count { font-size: 12px; color: var(--ink-4); }
.rail-note { padding: var(--s3) var(--s2); }
.rail-note .empty-line { font-size: 15px; }
.rail-note .empty-sub { font-size: 13px; }

/* --------------------------------------------------------- new-case modal */

.modal-wide { max-width: 760px; }
.nc-sub { margin: 0 0 var(--s4); color: var(--ink-2); font-size: 13px; }
.nc-hint { margin: 6px 0 var(--s3); color: var(--ink-3); font-size: 12px; line-height: 1.55; }
.nc-hint em { font-style: normal; color: var(--ink-2); }
.nc-docs {
  margin: var(--s4) 0;
  border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--sunken); padding: var(--s3) var(--s4);
}
.nc-docs-head { display: flex; align-items: center; justify-content: space-between; gap: var(--s3); }
.nc-actions { display: inline-flex; align-items: center; gap: var(--s2); }
.nc-count { font-size: 12px; color: var(--ink-4); }
.nc-list { list-style: none; margin: var(--s2) 0 0; padding: 0; display: flex; flex-direction: column; gap: var(--s2); }
.nc-row {
  display: grid; grid-template-columns: minmax(0, 15ch) minmax(0, 1fr) auto;
  gap: var(--s2); align-items: center;
  background: var(--raised); border: 1px solid var(--line);
  border-radius: var(--r-md); padding: var(--s2) var(--s3);
}
.nc-file { font-family: var(--mono); font-size: 12px; color: var(--ink-2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nc-label {
  font: inherit; font-size: 13px; width: 100%; padding: 7px 10px;
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--line-2); border-radius: var(--r-sm);
  transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.nc-label:focus-visible { outline: none; border-color: var(--accent); box-shadow: var(--ring); }
.nc-remove {
  font: inherit; font-size: 12px; color: var(--ink-3);
  background: transparent; border: 0; cursor: pointer; padding: 4px 6px;
  border-radius: var(--r-sm);
}
.nc-remove:hover { color: var(--danger); background: var(--danger-soft); }
/*
 * THE DROP ZONE IS DELIBERATELY BIG (Kazim, 2026-09-29): "make the drag and drop box bigger".
 * At `min-height: 64px` on a phone it was a strip narrower than a finger, and the operator kept
 * missing it — which was the real cause of the stray-drop navigation, not carelessness. Sized so the
 * target is obvious and hard to miss at both widths, and the hint is given its own line rather than
 * being squeezed beside the button.
 */
.nc-drop {
  margin-top: var(--s3); padding: var(--s5) var(--s4); min-height: 132px;
  display: flex; align-items: center; justify-content: center;
  border: 1px dashed var(--line-3); border-radius: var(--r-md);
  text-align: center; color: var(--ink-4); font-size: 12px;
  transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease);
}
.nc-drop.hot { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); }
.nc-empty { color: var(--ink-3); font-size: 13px; margin: var(--s2) 0 0; }

/* the selected tab's own header inside the sheet */
.wf-intro { margin-bottom: var(--s5); max-width: 68ch; }
.wf-intro h2 {
  margin: 0 0 6px; font-size: 20px; font-weight: 680; letter-spacing: -0.02em;
}
.wf-intro p { margin: 0; color: var(--ink-2); font-size: 13px; line-height: 1.6; }

/* a document's operator-given name, on the intake file list */
.filerow .flabel {
  font-size: 12px; color: var(--accent); font-family: var(--mono);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.filerow .flabel.missing { color: var(--warn); }
.filerow .flabel-edit {
  font: inherit; font-size: 12px; font-family: var(--mono);
  width: 100%; padding: 4px 7px;
  background: var(--raised); color: var(--ink);
  border: 1px solid var(--line-2); border-radius: var(--r-sm);
}
.filerow .flabel-edit:focus-visible { outline: none; border-color: var(--accent); box-shadow: var(--ring); }

/* the printer picker on the print step, and the office/printer pair it shows */
.printer-pick { display: flex; flex-direction: column; gap: var(--s2); margin: var(--s3) 0; }
.printer-row {
  display: flex; align-items: center; gap: var(--s3);
  border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--raised); padding: var(--s3) var(--s4);
  cursor: pointer;
}
.printer-row:hover { border-color: var(--line-3); }
.printer-row.on { border-color: var(--accent-line); background: var(--accent-soft); }
.printer-office { font-weight: 640; font-size: 13px; }
.printer-name { font-family: var(--mono); font-size: 12px; color: var(--ink-3); margin-left: auto; }
.printer-addr { font-family: var(--mono); font-size: 11px; color: var(--ink-4); }

/* ------------------------------------------------------- the print set
   What will print, as a thing the operator can hold and reorder. The form is pinned at the
   head of each copy and cannot be dragged; everything else is. */

.pset { margin: var(--s4) 0; }
.pset-head {
  display: flex; align-items: baseline; gap: var(--s3); flex-wrap: wrap;
  margin-bottom: var(--s2);
}
.pset-head h2 {
  margin: 0; font-size: 12px; text-transform: uppercase;
  letter-spacing: 0.1em; color: var(--ink-3); font-weight: 680;
}
.pset-counts { font-family: var(--mono); font-size: 12px; color: var(--ink-3); margin-left: auto; }
.pset-copy { margin-bottom: var(--s4); }
.pset-copy-head {
  display: flex; align-items: center; gap: var(--s3);
  font-family: var(--mono); font-size: 11px; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--ink-4); font-weight: 650;
  padding: 0 2px var(--s1);
}
.pset-copy-head .pset-copy-n {
  color: var(--ink-2); background: var(--sunken);
  border: 1px solid var(--line); border-radius: var(--r-sm); padding: 1px 7px;
}
.pset-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 3px; }
.pset-item {
  display: grid; grid-template-columns: 22px 26px minmax(0, 1fr) auto 24px;
  align-items: center; gap: var(--s2);
  background: var(--raised); border: 1px solid var(--line);
  border-radius: var(--r-md); padding: 8px 12px;
  transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease), opacity var(--dur) var(--ease);
}
.pset-item.pinned { background: var(--accent-soft); border-color: var(--accent-line); }
.pset-item[draggable="true"] { cursor: grab; }
.pset-item.dragging { opacity: 0.45; }
.pset-item.drop-before { border-top: 2px solid var(--accent); }
.pset-item.drop-after { border-bottom: 2px solid var(--accent); }
.pset-grip {
  font-size: 13px; color: var(--ink-4); text-align: center; line-height: 1;
  user-select: none;
}
.pset-item.pinned .pset-grip { color: var(--accent); opacity: 0.6; }
.pset-n {
  font-family: var(--mono); font-size: 12px; color: var(--ink-4);
  text-align: right; font-feature-settings: "tnum" 1;
}
.pset-body { min-width: 0; }
/* both lines must be block-level: as inline spans the title and its metadata run together on
   one line ("Klageschrift (DE)doc1.pdf · not yet read…"), and text-overflow needs a block
   container to ellipsise at all */
.pset-title {
  display: block;
  font-size: 13px; font-weight: 600; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pset-title.unnamed { color: var(--warn); font-weight: 620; }
.pset-sub {
  display: block;
  font-family: var(--mono); font-size: 11px; color: var(--ink-4);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pset-tags { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
.pset-tag {
  font-family: var(--mono); font-size: 11px; font-weight: 620;
  border-radius: var(--r-pill); padding: 1px 8px; white-space: nowrap;
}
.pset-tag.tr { color: var(--accent); background: var(--accent-soft); border: 1px solid var(--accent-line); }
.pset-tag.bundle { color: var(--ok); background: var(--ok-soft); border: 1px solid var(--ok-line); }
.pset-tag.machine { color: var(--warn); background: var(--warn-soft); border: 1px solid var(--warn-line); }
.pset-tag.unnamed { color: var(--warn); background: var(--warn-soft); border: 1px solid var(--warn-line); }
.pset-tag.meta { color: var(--ink-3); background: var(--sunken); border: 1px solid var(--line); }
.pset-actions { display: flex; gap: var(--s2); flex-wrap: wrap; margin-top: var(--s2); }
/* the remove control on each row, and the list of documents taken out of the set */
.pset-x {
  font: inherit; font-size: 12px; line-height: 1; color: var(--ink-4);
  background: transparent; border: 0; cursor: pointer;
  width: 22px; height: 22px; border-radius: var(--r-sm);
  display: flex; align-items: center; justify-content: center;
}
.pset-x:hover { color: var(--danger); background: var(--danger-soft); }
.pset-excluded {
  display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap;
  margin-top: var(--s3); padding: var(--s3) var(--s4);
  border: 1px dashed var(--line-3); border-radius: var(--r-md);
  background: var(--sunken);
}
.pset-excluded-head {
  font-size: 12px; color: var(--ink-3); font-weight: 620;
}
.pset-warn {
  margin: var(--s2) 0 0; padding: var(--s3) var(--s4);
  background: var(--warn-soft); border: 1px solid var(--warn-line);
  border-radius: var(--r-md); font-size: 12px; color: var(--ink-2); line-height: 1.6;
}
.pset-warn b { color: var(--warn); }
.pset-note { font-size: 12px; color: var(--ink-3); line-height: 1.6; margin: var(--s2) 0 0; }

/* the translate decision, on the review step */
.tr-box {
  margin: var(--s3) 0; padding: var(--s3) var(--s4);
  border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--raised);
}
.tr-box h4 { margin: 0 0 6px; font-size: 13px; font-weight: 660; }
.tr-row { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; margin-top: var(--s2); }
.tr-lang {
  font: inherit; font-family: var(--mono); font-size: 13px;
  padding: 7px 10px; min-width: 16ch;
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--line-2); border-radius: var(--r-sm);
}
.tr-lang:focus-visible { outline: none; border-color: var(--accent); box-shadow: var(--ring); }
.tr-state { font-family: var(--mono); font-size: 12px; color: var(--accent); }
.tr-state.off { color: var(--ink-3); }
.tr-state.busy { color: var(--warn, var(--accent)); }

/* ------------------------------------------------------------- STEP 3 · TRANSLATION
   The translation step is its own screen: the receiving authority's requirement with its HCCH
   source, the documents the requirement applies to, the translated set with each document's
   reviewed flag, and the controls the pipeline module offered. Blocks are cards on the raised
   surface, the same stack the rest of the console uses. */
.tr-block {
  margin: var(--s4) 0 0; padding: var(--s3) var(--s4);
  border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--raised);
}
.tr-block h4 {
  margin: 0 0 var(--s2); font-size: 11px; text-transform: uppercase;
  letter-spacing: 0.1em; color: var(--ink-3); font-weight: 680;
  display: flex; align-items: baseline; gap: var(--s2); flex-wrap: wrap;
}
.tr-count { font-family: var(--mono); font-size: 11px; letter-spacing: 0; text-transform: none; color: var(--ink-3); }
.tr-count::before { content: "·"; margin-right: var(--s2); color: var(--line-3); }
.tr-req-line { margin: 0 0 var(--s2); font-size: 13px; line-height: 1.65; color: var(--ink); white-space: pre-wrap; }
.tr-absent { margin: var(--s2) 0 0; font-size: 13px; line-height: 1.6; color: var(--ink-3); }
.tr-src { margin: var(--s2) 0 0; display: flex; gap: var(--s2); flex-wrap: wrap; align-items: baseline; }
.tr-src a { color: var(--accent); font-family: var(--mono); font-size: 12px; overflow-wrap: anywhere; }
.tr-src .mono { font-size: 12px; color: var(--ink-3); }
.tr-note { margin: var(--s2) 0 0; font-size: 12px; line-height: 1.6; color: var(--ink-3); }
.tr-note.ok { color: var(--ok, var(--accent)); }
.tr-flag {
  margin: var(--s2) 0 0; padding: 7px var(--s3); font-size: 12.5px; line-height: 1.6;
  /* `--warn-bg` is NOT a token. The themed warning SURFACE this file already defines for exactly
     this is `--warn-soft` (`#fbf0dc` light / `#2a2113` dark) — the flag must follow the theme, not
     borrow the plain surface. */
  color: var(--ink); background: var(--warn-soft, var(--surface));
  border-left: 2px solid var(--warn, var(--accent)); border-radius: var(--r-sm);
}
.tr-docs { list-style: none; margin: var(--s2) 0 0; padding: 0; display: grid; gap: 6px; }
.tr-doc, .tr-item {
  display: flex; align-items: baseline; gap: var(--s2); flex-wrap: wrap;
  padding: 6px 0; border-top: 1px solid var(--line);
}
.tr-doc:first-child, .tr-item:first-child { border-top: 0; }
.tr-doc-title { font-size: 13px; color: var(--ink); font-weight: 560; overflow-wrap: anywhere; }
.tr-doc-file { font-size: 12px; color: var(--ink-3); overflow-wrap: anywhere; }
.tr-doc-lang, .tr-doc-pages { font-size: 12px; color: var(--ink-3); }
.tr-tag {
  font-family: var(--mono); font-size: 11px; padding: 2px 7px; border-radius: 999px;
  border: 1px solid var(--line-2); color: var(--ink-2); white-space: nowrap;
}
.tr-tag.ok { color: var(--ok, var(--accent)); border-color: currentColor; }
.tr-tag.quiet { color: var(--ink-3); }
/* THE STEP'S OWN BUTTON ROW (Kazim, 2026-09-27: the approval "should look like how the approvals
   work in reading step"). It carries the SAME layout as Reading's `.stage-btnrow` — one flex row,
   the same gap, wrapping — so the approve control sits where an operator already expects to find it
   and behaves the same when the column is narrow.

   MEASURED before this: `.tr-controls` carried only `margin-top`, so the row did not lay out like
   Reading's at all — and on a phone Reading gives its buttons a 50%-width flex basis
   (`.stage-btnrow .btn` under the narrow breakpoint) which the Translation row did not inherit.
   Two rows that are meant to read as the same control have to be laid out by the same rules. */
.tr-controls { display: flex; gap: var(--s2); flex-wrap: wrap; margin: var(--s3) 0; }
.tr-settled { margin: 0 0 var(--s2); font-size: 13px; line-height: 1.6; }
.tr-settled.ok { color: var(--ok, var(--accent)); }
.tr-settled.open { color: var(--warn, var(--accent)); }
.tr-refusals { list-style: none; margin: var(--s3) 0 0; padding: 0; display: grid; gap: 4px; }
.tr-refusal { font-size: 12px; line-height: 1.6; color: var(--ink-3); }
.tr-refusal .mono { font-family: var(--mono); color: var(--ink-2); }
.tr-lang-wrap { display: inline-flex; }

/* THE TRANSLATED DOCUMENT'S OWN ACTIONS (Kazim, 2026-09-27) — "Read" and "Compare with the
   original" sit on the row they act on, pushed to the end so the row still reads
   title · file · language · state. They are real buttons rather than links because the reader is an
   overlay in this page, not a new document. */
.tr-doc-acts {
  margin-left: auto; display: inline-flex; gap: 6px; align-items: center; flex-wrap: wrap;
}
.tr-doc-acts .btn.sm { padding: 3px 9px; font-size: 12px; }

/* ------------------------------------------------------------ the document reader */
.docview-wrap { align-items: center; justify-content: center; padding: var(--s4); }
.modal.modal-doc {
  max-width: min(1500px, 96vw); width: min(1500px, 96vw);
  max-height: 94vh; display: flex; flex-direction: column; gap: var(--s2);
}
.dv-head h3 { margin: 0; font-size: 15px; }
.dv-sub { margin: 2px 0 0; font-size: 12px; color: var(--ink-3); }
.dv-bar {
  display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap;
  border-top: 1px solid var(--line); border-bottom: 1px solid var(--line);
  padding: 6px 0; font-size: 12px;
}
.dv-bar .dv-spacer { flex: 1; }
.dv-page, .dv-zoom { color: var(--ink-3); font-size: 12px; }
.dv-stage {
  flex: 1; min-height: 240px; overflow: auto; background: var(--surface-2, var(--surface));
  border: 1px solid var(--line); border-radius: var(--r-sm);
  display: flex; align-items: flex-start; justify-content: center; gap: var(--s3);
  padding: var(--s3);
}
/* THE TWO HALVES ARE RENDERED AS ONE IMAGE by the server, so the panes here carry the LABEL under
   each half and never scale the halves independently — the composition already placed them. */
.dv-pane { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.dv-pane-label { font-size: 11px; color: var(--ink-3); text-align: center; overflow-wrap: anywhere; }
.dv-img { display: block; height: auto; box-shadow: 0 1px 3px rgb(0 0 0 / 18%); background: #fff; }
.dv-msg { margin: auto; max-width: 60ch; font-size: 13px; line-height: 1.6; color: var(--warn, var(--accent)); }
.dv-foot { margin: 0; font-size: 12px; line-height: 1.5; color: var(--ink-3); }
body.docview-open { overflow: hidden; }

* { scrollbar-width: thin; scrollbar-color: var(--line-3) transparent; }
::-webkit-scrollbar { width: 11px; height: 11px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: var(--line-3); border: 3px solid transparent;
  background-clip: content-box; border-radius: var(--r-pill);
}
::-webkit-scrollbar-thumb:hover { background: var(--ink-4); background-clip: content-box; }

/* one authored moment: a state chip settles when a case's status changes */
@keyframes chip-in {
  from { opacity: 0; transform: translateY(-2px); }
  to   { opacity: 1; transform: none; }
}
.state.landed { animation: chip-in 200ms var(--ease); }

/* the mark's ring turns slowly, the console's only ambient motion */
@keyframes rose-turn { to { transform: rotate(360deg); } }
.mark-rose .rosette-weave { transform-origin: 20px 20px; animation: rose-turn 60s linear infinite; }

@media (prefers-reduced-motion: reduce) {
  .state.landed { animation: none; }
  .btn:active:not(:disabled) { transform: none; }
  .mark-rose .rosette-weave { animation: none; }
  * { transition-duration: 1ms !important; }
}

/* -------------------------------------------------------------- responsive */

@media (max-width: 1120px) {
  :root { --rail: 320px; }
  .row { grid-template-columns: 170px minmax(0, 1fr); }
}
@media (max-width: 980px) {
  .desk-body { grid-template-columns: 1fr; }
  .sheet { border-radius: 0; border-left: 0; }
  .queue { border-right: 0; border-bottom: 1px solid var(--line); }
  .rail-body { max-height: 232px; overflow: auto; }
  .wfbar { padding: var(--s3) var(--s4); }
  .search { width: 100%; min-width: 0; }
  .search-wrap { flex: 1 1 24ch; }
  .bar { height: auto; padding: var(--s3) var(--s4); flex-wrap: wrap; gap: var(--s3); }
  .bar-meta { flex-wrap: wrap; gap: var(--s3); }
  .case-head { padding: var(--s4) var(--s4) var(--s3); }
  .stage { padding: var(--s3) var(--s4) var(--s4); }
  .step-caption, .chat-head, .chat-compose { padding-left: var(--s4); padding-right: var(--s4); }
  .chat { padding-left: var(--s4); padding-right: var(--s4); }
  .stepper { padding-left: var(--s4); padding-right: var(--s4); }
}
@media (max-width: 860px) {
  .row { grid-template-columns: 1fr; gap: var(--s2); }
  .provenance { display: flex; }
  .ship-grid, .courier-grid { grid-template-columns: 1fr; }
  .step-l { display: none; }
  .step { padding: 7px 10px; }
  .step-arrow { display: none; }
  .form-overlay { padding: var(--s2); }
  /* the title needs the full width before the actions claim theirs, or it wraps per character */
  .case-head { flex-wrap: wrap; align-items: flex-start; gap: var(--s3); }
  .case-title { flex: 1 1 100%; min-width: 0; }
  .case-actions { width: 100%; justify-content: flex-start; }
}

/* ------------------------------------------------------------- candidate ladder */
/* A contradictory field shows the chosen value plus every other reading, ranked by */

/* confidence high→low. Each option carries its own source, opened in the highlight view. */
.cand-ladder { margin: 8px 0 0; border: 1px solid var(--line); border-radius: var(--r-md); overflow: hidden; }
.cand-ladder > summary {
  cursor: pointer; user-select: none;
  display: flex; align-items: center; gap: 8px;
  padding: 6px 12px; font-size: 12px; font-family: var(--mono);
  color: var(--warn); background: var(--warn-soft);
  border-radius: var(--r-md);
}
.cand-ladder > summary .caret { margin-left: auto; font-size: 10px; }
.cand-ladder[open] > summary { border-radius: var(--r-md) var(--r-md) 0 0; }
.cand-list { list-style: none; margin: 0; padding: 0; background: var(--raised); }
.cand-item {
  padding: 10px 12px; border-top: 1px solid var(--line);
  display: flex; flex-direction: column; gap: 6px; font-size: 13px;
}
.cand-item:first-child { border-top: 0; }
.cand-item .cand-val { color: var(--ink); white-space: pre-wrap; word-break: break-word; font-family: var(--mono); }
.cand-item .cand-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.cand-item .cand-ev { font-size: 12px; color: var(--ink-2); font-family: var(--mono); white-space: pre-wrap; word-break: break-word; }
.cand-src {
  margin-left: auto; flex: none;
  font: inherit; font-family: var(--ui); font-size: 12px; font-weight: 560;
  color: var(--accent); background: transparent; border: 1px solid var(--line-2);
  border-radius: var(--r-sm); padding: 2px 9px; cursor: pointer;
}
.cand-src:hover { background: var(--accent-soft); border-color: var(--accent-line); }
.cand-use {
  flex: none;
  font: inherit; font-family: var(--ui); font-size: 12px; font-weight: 620;
  color: #fff; background: var(--accent); border: 1px solid var(--accent);
  border-radius: var(--r-sm); padding: 3px 10px; cursor: pointer;
}
.cand-use:hover { filter: brightness(1.06); }

/* ----------------------------------------------------------- source highlight view */
.src-overlay {
  position: fixed; inset: 0; z-index: 60;
  background: var(--bg); padding: var(--s4);
  display: flex; flex-direction: column; gap: var(--s3);
}
.src-overlay-head {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s3);
}
.src-overlay-head h2 { margin: 0; font-size: 16px; color: var(--ink); }
.so-title { display: flex; align-items: baseline; gap: var(--s3); min-width: 0; }
.so-title .mono { font-size: 12px; color: var(--ink-4); }
.src-overlay-body {
  flex: 1; overflow: auto; background: var(--surface);
  border: 1px solid var(--line); border-radius: var(--r-lg);
  display: flex; justify-content: center; align-items: flex-start; padding: var(--s4);
}
.src-page-wrap { display: flex; flex-direction: column; align-items: center; gap: var(--s2);
  max-width: 100%; }
/* A source that opened on a page with nothing highlighted says so HERE. A banner rather than a
   caption: the note states that the view could not do its job, which is a different thing from
   the file name and page above it. */
.src-note {
  margin: 0; font-family: var(--ui); font-size: 13px; line-height: 1.45;
  color: var(--warn); background: var(--warn-soft);
  border: 1px solid var(--warn-line); border-radius: var(--r-sm);
  padding: 8px 12px; max-width: 60ch; text-align: left;
}
.src-note[hidden] { display: none !important; }
.src-page { max-width: 100%; box-shadow: var(--e1); border-radius: 2px; }

/* ------------------------------------------------------- case-name rename / trash */
.case-name-wrap { display: flex; align-items: center; gap: var(--s2); min-width: 0; }
.case-name-wrap h1 { margin: 0; overflow-wrap: anywhere; }
.btn-icon {
  flex: none; width: 28px; height: 28px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  font: inherit; color: var(--ink-3);
  background: transparent; border: 1px solid transparent; border-radius: var(--r-sm);
  cursor: pointer; transition: color var(--dur) var(--ease), background var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.btn-icon:hover { color: var(--accent); background: var(--accent-soft); border-color: var(--accent-line); }
.btn-icon.danger:hover { color: var(--danger); background: var(--danger-soft); border-color: var(--danger-line); }
.btn-icon .glyph { width: 15px; height: 15px; }

.entry-row { position: relative; }
.entry-row .entry { width: 100%; text-align: left; }
.entry-actions {
  position: absolute; top: 8px; right: 6px;
  display: flex; gap: 2px; opacity: 0; transition: opacity var(--dur) var(--ease);
}
.entry-row:hover .entry-actions, .entry-row:focus-within .entry-actions { opacity: 1; }
.entry-row .entry-actions .btn-icon { width: 24px; height: 24px; background: var(--surface); border-color: var(--line); }
.entry-row .entry-actions .btn-icon:hover { background: var(--accent-soft); }
.entry-row .entry-actions .btn-icon.danger:hover { background: var(--danger-soft); }
.need-dest { font-style: normal; color: var(--warn); font-weight: 620; }

/* -------------------------------------- agent activity: message boxes + timestamps */
.chat-msg { display: flex; flex-direction: column; gap: 3px; max-width: 85%; }
.chat-msg.agent {
  align-self: flex-start;
  background: var(--raised); border: 1px solid var(--line);
  border-radius: var(--r-lg) var(--r-lg) var(--r-sm) var(--r-lg);
  color: var(--ink); font-size: 13px; line-height: 1.6;
  padding: 9px 13px; box-shadow: var(--e1);
  white-space: pre-wrap; word-break: break-word;
}
.chat-msg.user { align-self: flex-end; max-width: 78%; }
.chat-msg.system { align-self: flex-start; max-width: 100%; }
.msg-meta { display: flex; align-items: center; gap: 6px; font-family: var(--mono); font-size: 11px; color: var(--ink-4); }
.chat-msg.user .msg-meta { align-self: flex-end; }
.chat-msg.agent .msg-meta { order: -1; }
.chat-msg .msg-lbl { font-weight: 680; color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.06em; }
.chat-msg.user .msg-lbl { color: #fff; opacity: .9; }
.chat-msg.system .msg-meta { align-self: flex-start; }

/* translations / requirements panel */
.req-panel { margin: var(--s4) 0 0; }
.req-panel h3 { margin: 0 0 var(--s2); font-size: 11px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3); font-weight: 680; }
.req-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s3); }
.req-card { border: 1px solid var(--line); border-radius: var(--r-md); background: var(--raised); padding: var(--s3) var(--s4); }
.req-card h4 { margin: 0 0 4px; font-size: 12px; text-transform: uppercase; letter-spacing: 0.07em; color: var(--ink-3); }
.req-card p { margin: 0; font-size: 13px; color: var(--ink); white-space: pre-wrap; }
.req-card .mono { font-size: 12px; color: var(--ink-2); display: block; margin-top: 4px; }
.req-card a { color: var(--accent); font-family: var(--mono); font-size: 12px; }
/* A requirements card holds a long URL and a source line. At phone width an unbroken token set a
   min-content floor and the card measured 457px in a 390px viewport — the whole page scrolled
   sideways. `minmax(0,1fr)` releases the track; `overflow-wrap` lets the URL break. */
.req-grid { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
.req-card { min-width: 0; overflow-wrap: anywhere; }
.req-card a, .req-card .mono, .req-card p { overflow-wrap: anywhere; word-break: break-word; }
@media (max-width: 620px) { .req-grid { grid-template-columns: minmax(0, 1fr); } }

/* ---------------------------------------------------------------- home view */
/* The default page: every case grouped. A tab bar of categories (All, folders, statuses,
   countries). The case chrome (id chip, title, stepper, caption, intake/agent feed) is
   hidden on home so it never reads as "no case selected". */

body.on-home .case-chrome,
body.on-home .case-head,
body.on-home .stepper,
body.on-home .step-caption,
body.on-home .chat-wrap { display: none; }
body.on-home .sheet-scroll { overflow-y: auto; }

.home-tabs {
  display: flex; flex-wrap: wrap; gap: 6px;
  margin-bottom: var(--s4);
}
.home-tab {
  font: inherit; font-family: var(--mono); font-size: 12px; font-weight: 620;
  padding: 6px 12px; border-radius: var(--r-pill);
  color: var(--ink-2); background: var(--raised);
  border: 1px solid var(--line-2); cursor: pointer;
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease), color var(--dur) var(--ease);
}
.home-tab:hover { border-color: var(--line-3); color: var(--ink); }
.home-tab.on { color: #fff; background: var(--accent); border-color: var(--accent); }
.home-empty { padding: var(--s5) 0; }
.home-new-folder { margin-top: var(--s2); }
.home-section { margin-bottom: var(--s5); }
.home-section-head {
  display: flex; align-items: center; gap: var(--s2);
  margin-bottom: var(--s2);
}
/* says whether a heading is one of the operator's folders or a group computed from the
   destination — without it the two are indistinguishable and a case under "France" appears to
   contradict its own "No folder" picker */
.home-kind {
  /* 11px, not 10px: the floor. At 10px with this letter-spacing the FOLDER/YOURS tag read as
     texture, not text, and the audit measured it below the console's own legibility floor. */
  font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase;
  padding: 1px 6px; border-radius: var(--r-pill);
  color: var(--ink-3); background: var(--sunken); border: 1px solid var(--line);
}
.home-kind[data-kind="folder"],
.home-kind[data-kind="own"] { color: var(--accent); background: var(--accent-soft); border-color: var(--accent-line); }
.home-section-head .home-count { margin-left: auto; }
.home-section-head h2 {
  margin: 0; font-size: 12px; text-transform: uppercase;
  letter-spacing: 0.1em; color: var(--ink-3); font-weight: 680;
}
.home-count { font-size: 12px; color: var(--ink-4); }
/* A category the operator made can be renamed and deleted from its own heading. Kept faint until
   the heading is hovered or the button is focused, so a list of headings stays calm — but always
   reachable by keyboard, and never `display: none`. */
.sec-acts { display: flex; align-items: center; gap: 2px; }
.sec-acts .btn-icon { width: 24px; height: 24px; opacity: 0; transition: opacity var(--dur) var(--ease); }
.home-section-head:hover .sec-acts .btn-icon,
.sec-acts .btn-icon:focus-visible { opacity: 1; }
@media (hover: none) { .sec-acts .btn-icon { opacity: 1; } }
.home-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: var(--s3); }
.home-card {
  border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--raised); box-shadow: var(--e1);
  padding: var(--s3) var(--s4); display: flex; flex-direction: column; gap: var(--s2);
  position: relative;
}
.home-card-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: start; }
/* The desk keeps the desk's behaviour: the rename/delete pair is out of the way until the card
   is pointed at. The phone block overrides this — there is no hover on touch, so there they are
   always shown. Nothing here differs from the rail's own row actions. */
.home-card-acts { display: flex; gap: 2px; opacity: 0; transition: opacity var(--dur) var(--ease); }
.home-card:hover .home-card-acts, .home-card:focus-within .home-card-acts { opacity: 1; }
.home-card .btn-icon { width: 24px; height: 24px; }
.home-card-main {
  font: inherit; text-align: left; cursor: pointer;
  background: transparent; border: 0; padding: 0;
  display: flex; flex-direction: column; gap: 6px; min-width: 0;
}
.home-card-main .entry-name { font-weight: 620; color: var(--ink); }
.home-card-meta { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; }
.home-card-meta .entry-sub { font-family: var(--mono); font-size: 12px; color: var(--ink-2); }
.home-cat-sel {
  font: inherit; font-family: var(--mono); font-size: 12px;
  color: var(--ink); background: var(--raised);
  border: 1px solid var(--line-2); border-radius: var(--r-sm); padding: 4px 8px;
  cursor: pointer;
}
.home-folder { border: 1px solid var(--line); border-radius: var(--r-md); background: var(--surface); margin-bottom: var(--s2); }
.home-folder summary {
  list-style: none; cursor: pointer; user-select: none;
  display: flex; align-items: center; gap: var(--s3);
  padding: var(--s3) var(--s4);
}
.home-folder summary::-webkit-details-marker { display: none; }
.home-folder summary::before {
  content: "▸"; color: var(--ink-3); font-size: 11px; transition: transform var(--dur) var(--ease);
}
.home-folder[open] summary::before { transform: rotate(90deg); }
.folder-title { font-weight: 620; color: var(--ink); }
.folder-count { font-size: 12px; color: var(--ink-4); margin-left: auto; }
.folder-actions { display: flex; gap: 2px; }
.home-folder > div { padding: 0 var(--s4) var(--s4); }
.folder-empty { color: var(--ink-3); font-size: 13px; margin: var(--s2) 0 var(--s3); }
.home-section .folder-empty { margin-left: 0; }

/* ============================================================ phones
 *
 * A phone does not get a squeezed desk. The desk is two panes side by side — a case list and
 * the sheet — and at 390px there is no room for both, so the console becomes what a phone
 * already expects for a list of things: a **drill-down**. The list is the page; picking a case
 * slides the case in; a back control at the top of the case returns to the list.
 *
 * Three things make that real rather than cosmetic:
 *   1. `body.case-open/on-home` drives which pane exists on screen, so nothing is laid out
 *      off-screen to be scrolled into by accident.
 *   2. The workflow tabs, search and create actions live in a bar that stays put at the top of
 *      the list view — the operator's first question is "which of these needs me", and that bar
 *      is how they narrow it.
 *   3. Everything tappable is at least 44px on its short side. The desktop build is dense by
 *      design; a thumb is not a mouse.
 *
 * Notch and home bar: the shell pads with the safe-area insets, so the last row of a long case
 * is never under the home indicator and the tabs are never under the notch.
 */

@media (max-width: 720px) {

  /* The desk is a grid, and a grid sizes its columns to their content. A nowrap row of buttons
     therefore set a min-content floor wider than the phone itself (measured: 455px inside a
     390px body), and the parent's overflow:hidden hid the symptom while the content sat
     off-screen. Every level down to the scroll container is pinned to the viewport width here,
     and min-width:0 releases the automatic minimum that creates the floor. */
  .bar { padding-left: max(var(--s3), env(safe-area-inset-left));
         padding-right: max(var(--s3), env(safe-area-inset-right)); }
  /* `body` is itself a grid with an implicit auto column, so an auto track let the desk's
     min-content (413px) set the column width inside a 354px body. An auto track is allowed to
     overflow its grid container, which is why max-width on the children could not help: the
     floor was set one level up. Pinning the track with minmax(0,1fr) is the structural fix —
     nothing inside can widen the page again. */
  body { grid-template-columns: minmax(0, 1fr); }
  .desk, .desk-body, .queue, .sheet, .stage, .wfbar {
    min-width: 0; max-width: 100%;
  }
  .desk { min-height: 0; width: 100%; }
  .desk-body { width: 100%; }

  /* --- the list is the page, and the page scrolls as one --- */
  .desk-body { display: block; }
  .queue { border-right: 0; border-bottom: 0; height: auto; }
  /* on the list view the sheet does not exist at all */
  body.on-home .sheet { display: none; }
  /* inside a case the list is gone, not scrolled past */
  body:not(.on-home) .queue { display: none; }
  /* The list view is ONE scroll region. Previously the rail kept its own scrollbar, so the
     cases scrolled inside a box while the bar and the page sat still — on a phone that reads
     as a trapped pane. Here the page itself scrolls and the rail stops being a scroll
     container, so the whole list moves together under the thumb. The bar stays pinned because
     it is the list's own control (workflow + search), not page furniture. */
  /* `html`/`body` are pinned to `height: 100%` for the desktop app shell, and the desk is a grid
     whose second row is `1fr` — so the desk never grows: the rail's content was clipped at 767px
     with 442px of cases hidden below, and `body` could not scroll because it had nothing to
     scroll. On a phone the list must be as tall as its content, so the height chain is released
     from `body` down and the document becomes the scroller. */
  /* `html` carries `overflow: hidden` (the desktop app shell) and `height: 100%`, so a scroller
     on `body` becomes a 767px-tall box INSIDE a fixed 844px html: body's own scrollable height
     equals its client height and nothing moves. On a phone the document itself must scroll, so
     html is released to `overflow-y: auto` with its height free, and body stops being a scroller
     and simply grows to its content. */
  /* both phone views scroll the document: the list and the case */
  /* X is pinned shut and Y is the scroller. `overflow-y: auto` on its own leaves the X axis
     `visible`, and `zoom: 1.1` rounds the layout up a fraction of a pixel — enough for the phone
     page to be dragged a few px sideways. Pin the axis we never want scrolled. */
  html:has(body.on-home),
  html:has(body.case-open) {
    overflow-x: clip; overflow-y: auto;
    height: auto; overscroll-behavior-y: contain;
  }
  /* `position: sticky` needs a scrolling ancestor whose content is taller than itself. With the
     document now scrolling, the sticky bar's containing block (`body` -> `.desk` -> `.wfbar`)
     must not be its own overflow context, or the sticky resolves against a box that never
     scrolls and the bar rides away with the content. `overflow: visible` on the chain (set
     below) is what makes the bar hold. */
  body.on-home .desk { overflow: visible; }
  /* `body` is a grid with a `1fr` second row: perfect for a fixed-height app shell, useless
     when the page must grow. An auto-height grid gives a `1fr` row no free space to claim, so
     the desk was sized to a fraction of the viewport and everything below it simply overflowed —
     body stayed 391px tall with 1333px of content inside it. On the phone list, normal block
     flow sizes each box to its content and nothing needs a height at all. */
  body.on-home { display: block; overflow: visible; height: auto; min-height: 100%; }
  /* Every box from the desk down becomes plain block flow on the phone list. The desk chain is a
     grid (desk) inside a grid (desk-body) whose columns are flex columns — built so each pane
     can own its own scrollbar in a fixed-height shell. When the page must grow instead, that
     machinery sizes boxes from free space that does not exist and quietly compresses them: the
     rail measured 232px tall with 1209px of cards inside, all of it clipped by `overflow:
     hidden`. A vertical stack of blocks has no such failure mode — each box is as tall as its
     content, and `html` scrolls. */
  /* NOTE: the `.col` selector is deliberately absent. `.sheet` is a `.col`, so including it here
     set `display: block` on the case workspace and the phone list drew the sheet a second time
     under the rail (the sheet's own `display: none` lost on source order). Only the rail's own
     boxes are released. */
  body.on-home .desk,
  body.on-home .desk-body,
  body.on-home .queue,
  body.on-home .rail-body {
    display: block; height: auto; min-height: 0; max-height: none;
    overflow: visible; grid-template-rows: none; grid-template-columns: none;
  }
  /* release the sheet's own clipping *only* while it is the visible pane */
  body:not(.on-home) .sheet,
  body:not(.on-home) .sheet-scroll { height: auto; min-height: 0; }
  body.on-home .queue,
  body.on-home .rail-body { overflow: visible; flex: none; height: auto; }

  /* --- the case is the page, and it has a way out --- */
  body:not(.on-home) .sheet { display: flex; }
  body:not(.on-home) .case-back { display: inline-flex; }   /* the base rule hides it elsewhere */
  /* the workflow bar belongs to the list. Inside a case it is a second navigation competing
     with the case header, and it pushed the header off the first screen. */
  body:not(.on-home) .wfbar { display: none; }
  .case-back { display: none; }

  /* The case view scrolls as ONE region, exactly like the list.
     The desk is a fixed-height app shell where each pane owns its own scrollbar; on a phone that
     left the case as a 10,593px sheet inside a 767px clipped body with no working scroller, so
     the operator could not get past the first screen. Releasing the chain is the fix: every box
     from `html` down becomes as tall as its content, `html` is the scroller, and the sheet's own
     internal scroller stops being one (a nested scroller inside a page scroller is what makes a
     phone page feel stuck). */
  /* `visible` here means "the document scrolls, this box does not clip", which is what the sticky
     bar needs — but it must not extend to X, or a sub-pixel rounding under zoom becomes a
     sideways drag. */
  body.case-open { overflow-x: clip; overflow-y: visible; height: auto; min-height: 100%; display: block; }
  body.case-open .desk,
  body.case-open .desk-body,
  body.case-open .sheet {
    display: block; height: auto; min-height: 0; max-height: none;
    overflow: visible; grid-template-rows: none; grid-template-columns: none;
  }
  /* the sheet's internal scroller is neutralised: the page is the scroller here, and two nested
     scrollers on a touch device fight each other (the inner one swallows the gesture) */
  body.case-open .sheet-scroll {
    display: block; height: auto; min-height: 0; max-height: none; overflow: visible;
  }
  /* the header, the step rail and the actions stay put while the case scrolls under them */
  /* Only what the operator NAVIGATES with stays put: the header (which holds the way back) and
     the step rail (which says where the case is). The step caption is one line of explanation
     about the step — it scrolls away with the content it describes, or it sits on top of the
     agent feed and reads as a rendering fault. */
  body.case-open .case-head,
  body.case-open .stepper {
    position: sticky; z-index: 5; background: var(--surface);
  }
  body.case-open .case-head { top: 0; }
  body.case-open .stepper {
    top: var(--phone-head);
    box-shadow: 0 1px 0 var(--line);   /* the rail's own hairline, so nothing shows through */
  }
  body.case-open .step-caption { position: static; }
  /* Collapsed is a PANEL; expanded is the page.
     Growing the feed with the page made a long reading 30,885px tall, so the case was almost
     entirely agent log and the step card was buried under it. Collapsed is now about half the
     screen with the feed scrolling inside — a panel beside the work, not a wall in front of it.
     The nested scroller is deliberate here: it is bounded and small, and the operator scrolls it
     to read the feed, then scrolls the page to get to the step. The expanded view is where it
     takes the whole screen. */
  body.case-open .chat-wrap { flex: none; }
  body.case-open .chat {
    /* ~half the viewport: enough to read a few exchanges, not enough to bury the step. Divided by
       `--zoom` for the same reason as the expanded height — viewport units scale with it. */
    max-height: calc(44dvh / var(--zoom, 1.1));
    overflow-y: auto;
    overscroll-behavior: contain;
    min-height: 0;
  }
  /* the composer is the input the operator types in: keep it reachable at the foot of the feed */
  body.case-open .chat-compose { position: sticky; bottom: 0; z-index: 4;
    background: var(--surface); border-top: 1px solid var(--line); }
  /* Expanded is the exception: there the feed fills the screen and scrolls within itself, which
     is the point of the control. That needs a bounded height, so the shell goes back to a
     viewport-height column and the feed takes what is left of it. */
  /* `zoom` on :root scales viewport units as well, so a plain `100dvh` here computes to 928px on
     an 844px screen and the feed never fills it. Divide the factor back out — `--zoom` is the one
     number, so this follows a change to it. */
  body.case-open.chat-expanded {
    height: calc(100dvh / var(--zoom, 1.1));
    overflow: hidden;
    display: flex; flex-direction: column;
  }
  body.case-open.chat-expanded .desk,
  body.case-open.chat-expanded .desk-body { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; overflow: hidden; }
  body.case-open.chat-expanded .sheet { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; overflow: hidden; }
  body.case-open.chat-expanded .sheet-scroll { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; overflow: hidden; }
  /* the header and the step rail are part of the expanded screen: they are how the operator
     leaves and how they know where the case is. They keep their natural height. */
  body.case-open.chat-expanded .case-head,
  body.case-open.chat-expanded .stepper { flex: none; }

  body.case-open.chat-expanded .chat-wrap { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
  body.case-open.chat-expanded .chat {
    flex: 1 1 auto; min-height: 0; overflow-y: auto;
    max-height: none;                 /* expanded: the whole screen, no cap */
  }
  body.case-open.chat-expanded .stage { display: none; }
  /* Expanded is the activity ALONE, filling the screen. The step rail is navigation for a view
     the operator has just left, and the explanatory caption goes with it — together they were
     457px of chrome on an 844px screen, which is why "expanded" came out barely bigger than the
     collapsed panel. What stays is the minimum to get out: the way back and the case title. */
  /* The app bar ("Project Control" + the theme toggle) is the console's own furniture, not the
     case's. In expanded view it is 67px of chrome the activity needs: hide it, and the only
     things above the feed are the way back and the title. */
  body.case-open.chat-expanded .bar { display: none; }
  body.case-open.chat-expanded .stepper,
  body.case-open.chat-expanded .step-caption,
  body.case-open.chat-expanded .banner { display: none; }
  body.case-open.chat-expanded .case-head {
    padding: 4px var(--s3);
    border-bottom: 1px solid var(--line);
  }
  body.case-open.chat-expanded .case-head .case-id,
  body.case-open.chat-expanded .case-head .case-actions,
  body.case-open.chat-expanded .case-head .btn-icon { display: none; }
  body.case-open.chat-expanded #case-name { font-size: 17px; }
  /* the feed's own head: a label and the control that returns, nothing else */
  body.case-open.chat-expanded .chat-head { padding: 6px var(--s3); }
  body.case-open.chat-expanded .chat-head .chat-note { display: none; }
  /* and the composer is the last thing at the foot of the screen */
  body.case-open.chat-expanded .chat-compose {
    position: static; border-top: 1px solid var(--line);
    padding-bottom: max(var(--s2), env(safe-area-inset-bottom));
  }
  html:has(body.case-open.chat-expanded) { overflow: hidden; height: 100%; }

  /* --- the workflow bar on a phone ---
     Three workflows and a search used to sit in the bar as tabs plus a full-width field, which
     cost three rows before any case. They collapse to two controls: a three-line button that
     names the current workflow and opens all three, and a search toggle that reveals the field
     only when wanted. Both are phone-only; the desktop keeps its inline tabs. */
  .wfbar {
    flex-direction: row; align-items: center; justify-content: space-between;
    flex-wrap: nowrap; gap: var(--s2);
    padding: var(--s2) max(var(--s3), env(safe-area-inset-left)) var(--s2)
             max(var(--s3), env(safe-area-inset-right));
    position: sticky; top: 0; z-index: 6;
  }
  /* the three-line control carries the current workflow's name, so the bar still answers
     "which tab am I in" without the tabs being visible */
  .wf-menu {
    display: inline-flex; align-items: center; gap: 8px;
    flex: 1 1 auto; min-width: 0; min-height: 46px; padding: 0 14px;
    font: inherit; font-size: 15px; font-weight: 660; color: var(--ink);
    background: var(--raised); border: 1px solid var(--line-2); border-radius: var(--r-md);
    cursor: pointer; text-align: left;
  }
  .wf-menu:active { background: var(--hover); }
  .wf-menu .glyph-svg { flex: none; color: var(--ink-2); }
  .wf-menu-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .wf-menu-n {
    margin-left: auto; flex: none; font-size: 12px; color: var(--ink-3);
    background: var(--sunken); border-radius: var(--r-pill); padding: 1px 8px;
  }
  .wf-search-toggle {
    display: inline-flex; align-items: center; justify-content: center; position: relative;
    flex: none; width: 46px; height: 46px;
    color: var(--ink-2); background: var(--raised);
    border: 1px solid var(--line-2); border-radius: var(--r-md); cursor: pointer;
  }
  .wf-search-toggle[aria-expanded="true"] {
    color: var(--accent); background: var(--accent-soft); border-color: var(--accent-line);
  }
  /* a dot on the toggle when a search is live and the field is folded away, so a filtered list
     can never look like a short list */
  .wf-st-dot {
    position: absolute; top: 7px; right: 7px; width: 8px; height: 8px;
    border-radius: 50%; background: var(--accent); border: 2px solid var(--raised);
  }
  /* the tabs themselves leave the bar — they are offered by the three-line sheet instead */
  .wfbar .wf-tabs { display: none; }
  .wf-tools {
    display: none; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: var(--s2); width: 100%;
  }
  /* the search toggle unfolds the tool row; the creates come with it, because they are the same
     question ("this tab: find one, or start one") */
  .wfbar.tools-open { flex-wrap: wrap; }
  .wfbar.tools-open .wf-tools { display: grid; }
  .search-wrap { grid-column: 1 / -1; }
  .search { width: 100%; min-height: 46px; font-size: 16px; }
  #create-hague, #create-informal, #create-translate {
    width: 100%; min-width: 0; min-height: 46px; padding: 0 10px;
    font-size: 13.5px; white-space: nowrap;
  }
  #create-translate { grid-column: 1 / -1; }

  /* the three-line menu's sheet. `display` must be restored here: the default rule hides it on
     the desk, and the phone block otherwise only positioned it — so the sheet stayed invisible
     while the JS believed it was open. */
  .wf-sheet-wrap { display: block; position: fixed; inset: 0; z-index: 40; }
  .wf-sheet-wrap[hidden] { display: none; }
  .wf-sheet-scrim {
    position: absolute; inset: 0; background: rgba(12, 14, 16, 0.42);
    backdrop-filter: blur(1.5px);
  }
  .wf-sheet {
    position: absolute; left: 0; right: 0; bottom: 0;
    background: var(--surface); border-top: 1px solid var(--line-2);
    border-radius: var(--r-xl) var(--r-xl) 0 0;
    box-shadow: var(--e3);
    padding: var(--s2) var(--s3) max(var(--s4), env(safe-area-inset-bottom));
    max-height: 78vh; overflow-y: auto;
  }
  .wf-sheet-grab {
    width: 40px; height: 4px; margin: 6px auto var(--s3);
    border-radius: var(--r-pill); background: var(--line-2);
  }
  .wf-sheet-head {
    margin: var(--s2) 0 6px; padding: 0 var(--s2);
    font-size: 11px; font-weight: 660; letter-spacing: 0.08em; text-transform: uppercase;
    color: var(--ink-3);
  }
  .wf-sheet-list { list-style: none; margin: 0 0 var(--s3); padding: 0; }
  .wf-sheet-list li { margin: 0; }
  .wf-sheet-btn {
    display: flex; align-items: center; gap: var(--s3); width: 100%;
    min-height: 54px; padding: 0 var(--s2);
    font: inherit; font-size: 16px; font-weight: 600; color: var(--ink); text-align: left;
    background: transparent; border: 0; border-radius: var(--r-md); cursor: pointer;
  }
  .wf-sheet-btn:active { background: var(--hover); }
  /* THE DRAWER'S OWN GREY (same rule as the tab strip): a blanket-blocked workflow's entry. The
     reason is on the button's title. */
  .wf-sheet-btn.wf-sheet-blocked { color: var(--ink-4); opacity: .55; }
  .wf-sheet-btn.wf-sheet-blocked .wf-sheet-tick { color: var(--ink-4); }
  .wf-sheet-blocked-note {
    display: flex; align-items: center; width: 100%; min-height: 54px; padding: 0 var(--s2);
    font: inherit; font-size: 15px; color: var(--ink-3); text-align: left;
  }
  .wf-sheet-btn .wf-sheet-tick { flex: none; width: 18px; color: var(--accent); }
  .wf-sheet-btn .wf-sheet-n {
    margin-left: auto; flex: none; font-family: var(--mono); font-size: 13px; color: var(--ink-3);
  }
  .wf-sheet-btn[aria-current="true"] { color: var(--accent); background: var(--accent-soft); }
  .wf-sheet-btn[aria-current="true"] .wf-sheet-n { color: var(--accent); }

  /* --- the list: the desk's categorised cards, made for a phone ---
     The desk's centre view groups a tab into the operator's folders, then by destination
     country, and draws each case as a card. On a phone that grouping is the whole point — it is
     how 20 cases stay scannable — so the phone shows the same cards in one column instead of
     the bare numbered rail list. `renderRail` builds the same markup here; the grouping comes
     from `caseSections`, so the two views cannot drift. */
  /* the three-line control already names the current workflow, so the rail's own heading would
     say it a second time one row below. The count is useful; the word is not. */
  .queue .col-head { display: none; }
  .rail-body { padding: 0 max(var(--s3), env(safe-area-inset-left)); }
  .queue-list { list-style: none; padding: 0; gap: 0; }

  .home-section { margin: 0 0 var(--s4); }
  .home-section-head {
    /* Not sticky. Sticky looked helpful (the group name stays visible) but a long destination
       name wraps to two lines at this width and the pinned box then covered the first card,
       hiding the case name. The bar above is already pinned; a second pinned thing that can
       change height is a defect waiting to happen. */
    position: static;
    margin: var(--s3) 0 var(--s2); padding: 0;
    background: none;
  }
  .home-section-head h2 { font-size: 12px; letter-spacing: 0.07em; }
  .home-count { font-size: 12px; }

  .home-cards { grid-template-columns: minmax(0, 1fr); gap: var(--s2); }
  .home-card {
    padding: var(--s3); gap: 10px;
    border-radius: var(--r-lg);
  }
  /* the whole card is the tap target, so the actions sit in their own row beside it rather than
     overlapping the name or the chip */
  .home-card-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 4px; }
  .home-card-main {
    display: flex; flex-direction: column; gap: 8px; align-items: stretch;
    min-width: 0; min-height: 56px; padding: 4px; text-align: left;
    background: transparent; border: 0; border-radius: var(--r-md); cursor: pointer;
  }
  .home-card-main:active { background: var(--hover); }
  .home-card .entry-name { font-size: 16px; font-weight: 660; }
  .home-card-meta { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 8px; }
  .home-card-meta .entry-sub {
    font-size: 13px; color: var(--ink-3); min-width: 0;
    white-space: normal; overflow-wrap: anywhere;
  }
  .home-card .state { font-size: 11px; padding: 3px 8px; flex: 0 1 auto; min-width: 0; max-width: 100%; }
  .home-card .entry-idx { font-size: 12px; }
  /* no hover on a phone, so the pair is always visible (the desk keeps it on hover only).
     Side by side, not stacked: a vertical pair was 101px tall and stole the card's width. */
  .home-card-acts { display: flex; flex-direction: row; gap: 2px; opacity: 1; align-items: flex-start; }
  .home-card-acts .btn-icon { width: 44px; height: 44px; }
  .home-card-acts .btn-icon .glyph { width: 18px; height: 18px; }
  /* the folder select is a real control, so it gets a real row rather than being squeezed
     beside the title */
  .home-card .home-cat-sel { width: 100%; min-height: 44px; }
  .home-empty { padding: var(--s4) var(--s3); }

  /* --- the case sheet --- */
  .case-head {
    padding: var(--s2) max(var(--s4), env(safe-area-inset-left)) var(--s3);
    gap: 2px var(--s2);
    align-items: center;
  }
  /* the title takes the width the back control does not use, on the same line */
  .case-title { flex: 1 1 auto; min-width: 0; }
  .case-id { font-size: 11px; }
  /* The state chip and Delete case were a second full-width row under the title, and the stepper
     sticks at the height of the FIRST row — so those two stayed on screen and peeked out from
     under the step rail while the page scrolled. On a phone the header is ONE row: the way back,
     the title, and the chip. Delete case moves to the foot of the case, where a destructive action
     belongs anyway (out of the way, not beside the title you are reading). */
  .case-head { flex-wrap: nowrap; }
  .case-actions {
    width: auto; flex: 0 0 auto; gap: var(--s2); margin-top: 0;
    align-items: center;
  }
  .case-actions .state { padding: 4px 9px; font-size: 11px; }
  .case-actions #delete-case { display: none; }
  /* The phone header is ONE row of a known height, so the step rail can be offset by exactly
     that and never overlap it. Without a fixed height the header and the rail overlap and the
     title and status chip show through the rail as the page scrolls. `--phone-head` is that
     height: the back control is 44px, plus the header's own vertical padding. */
  :root { --phone-head: 56px; }
  body.case-open .case-head {
    height: var(--phone-head);
    min-height: var(--phone-head);
    max-height: var(--phone-head);
    padding-top: 0; padding-bottom: 0;
    align-items: center;
    overflow: hidden;                /* a fixed header must clip, not grow */
  }
  /* one row means the name gets one line: a two-line title made the header 126px and no fixed
     rail offset could match it */
  body.case-open .case-head .case-id { display: none; }
  body.case-open .case-title { overflow: hidden; }
  body.case-open #case-name {
    font-size: 17px; line-height: 1.2;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  /* The title and the chip cannot share one 390px row: the chip needs ~133px for a readable
     status, which left the name "New matt…". The chip moves to its own strip directly under the
     header, so the title has the full row and the status keeps all its words. */
  body.case-open .case-actions { min-width: 0; flex: 0 1 auto; }
  /* One row is what makes the rail's offset exact, so the header stays one row and a very long
     case name ellipsises rather than pushing the status off. The chip is capped and the title
     takes the remainder; both are readable, neither pushes the other out. */
  body.case-open .case-head { flex-wrap: nowrap; }
  body.case-open #case-name { min-width: 0; }
  /* "AWAITING APPROVAL" is 17 characters of uppercase with wide letter-spacing: 157px of a 390px
     row, which left the case title "Ne…". Tighten the chip rather than truncate either of them —
     the status still reads, and the title keeps its room. */
  body.case-open .case-actions .state {
    max-width: 46vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    font-size: 10px; letter-spacing: 0.02em; padding: 4px 7px;
  }
  body.case-open .case-title { flex: 1 1 auto; min-width: 0; }
  body.case-open .case-name-wrap { min-width: 0; }
  /* the destructive action gets its own place at the end of the case, on every step. This block is
     inside the phone media query, so it cannot reach the desk — where the header keeps the button. */
  body.case-open .danger-foot { display: flex; }
  .danger-foot {
    display: none;                    /* the desk keeps the button in the header */
    margin-top: var(--s4); padding-top: var(--s3);
    border-top: 1px solid var(--line);
    justify-content: flex-end;
  }
  .case-back {
    order: -1; flex: 0 0 auto;
    align-items: center; gap: 4px;
    min-height: 44px; padding: 0 12px 0 8px; margin-left: -8px;
    font: inherit; font-size: 14px; font-weight: 620;
    color: var(--accent); background: transparent; border: 0; border-radius: var(--r-md);
    cursor: pointer;
  }
  .case-back:hover, .case-back:active { background: var(--accent-soft); }
  .case-back .glyph-svg { width: 16px; height: 16px; }
  /* THE WORD "CASES" COSTS THE TITLE ITS ROW. Measured at 390px: back control (44px) + chip cap
     (46vw) + paddings left the name "New m…" — the label is 45px of a row the title is starving
     for, and on a phone the arrow alone says "back": this button sits top-left, which is where
     back lives, and its aria-label already says where the press goes. The label span stays in
     the DOM (no markup change, wider phones keep it via the reverse rule below). */
  .case-back-label { display: none; }
  .case-back { padding: 0 10px 0 6px; margin-left: -6px; }
  #case-name { font-size: 19px; }
  .case-actions { gap: var(--s2); }
  .case-actions .btn { min-height: 44px; }

  /* the step rail: four equal tappable cells. The label stays — a bare numeral reads as a
     mystery tick, and the operator mid-task needs "Print", not "3". */
  .stepper {
    padding: 0 max(var(--s4), env(safe-area-inset-left)) 0;
    gap: 4px; overflow-x: auto; scrollbar-width: none;
  }
  .stepper::-webkit-scrollbar { display: none; }
  .step {
    flex: 1 1 0; min-width: 0; min-height: 48px;
    flex-direction: column; align-items: center; justify-content: center; gap: 1px;
    padding: 4px 2px;
  }
  .step-n { font-size: 11px; }
  .step-l { display: block; font-size: 11px; }
  .step-caption { font-size: 13px; padding: 0 max(var(--s4), env(safe-area-inset-left)); }

  .stage { padding: var(--s3) max(var(--s4), env(safe-area-inset-left)) var(--s5); }
  .step-card-head h3 { font-size: 17px; }
  .step-card-head p { font-size: 14px; }

  /* buttons: thumb-sized everywhere, without letting a long label wrap to two lines */
  .btn { min-height: 44px; padding-top: 0; padding-bottom: 0; }
  .btn.sm, .btn.ghost.sm { min-height: 40px; }
  /* action rows read as a small grid: each button is at least half the width, so a row of
     four is two comfortable columns rather than four ragged ones */
  .stage-btnrow { gap: var(--s2); }
  .stage-btnrow .btn { flex: 1 1 calc(50% - var(--s2)); min-width: 0; }
  .stage-btnrow .btn.primary:only-child { flex: 1 1 100%; }
  /* THE TRANSLATION STEP'S ROW GETS THE SAME TREATMENT. It is the same kind of action row — the
     approval for the step — so on a phone it must not be the one row that stays ragged while
     Reading's goes to two comfortable columns. */
  .tr-controls .btn { flex: 1 1 calc(50% - var(--s2)); min-width: 0; }
  .tr-controls .btn.primary:only-child { flex: 1 1 100%; }

  /* the field sheet: a value is read, not scanned in columns */
  .row { grid-template-columns: 1fr; gap: 4px; }
  .row .label { font-size: 12px; }
  .row textarea, .row input[type=text] { font-size: 16px; }   /* 16px stops iOS zooming on focus */
  .group-head { padding-left: max(var(--s4), env(safe-area-inset-left)); }

  /* the intake file list: name on its own line, controls on the next */
  .filerow {
    grid-template-columns: 1fr auto; gap: 6px 8px;
    padding: 12px max(var(--s4), env(safe-area-inset-left));
  }
  .filerow .fkind { grid-column: 1; }
  .filerow .fname { grid-column: 1 / -1; font-size: 14px; }
  .filerow .flabel-cell { grid-column: 1 / -1; }
  .filerow .fdel { grid-column: 2; grid-row: 1; min-height: 40px; padding: 0 12px; }
  .filerow .flabel-edit, .filerow .flabel { font-size: 14px; }
  .filerow .flabel-save { min-height: 40px; }

  /* the chat feed: full width, and its composer stays above the keyboard */
  .chat-head { padding: 0 max(var(--s4), env(safe-area-inset-left)) var(--s2); }
  .chat { padding: 0 max(var(--s4), env(safe-area-inset-left)); }
  .chat-compose {
    padding: var(--s2) max(var(--s4), env(safe-area-inset-left))
             max(var(--s3), env(safe-area-inset-bottom));
  }
  .chat-row { gap: var(--s2); }
  .chat-row textarea { font-size: 16px; min-height: 44px; }
  .chat-row .btn { min-height: 44px; }
  .bubble { max-width: 88%; }

  /* the print set: one copy after the other, each row thumb-reachable */
  .pset-item {
    grid-template-columns: 18px 20px minmax(0, 1fr) auto 40px;
    gap: 6px; padding: 10px;
  }
  .pset-grip { font-size: 16px; }
  .pset-title { font-size: 14px; }
  .pset-sub { font-size: 12px; }
  .pset-x { width: 40px; height: 40px; }
  .pset-excluded .btn { min-height: 40px; }
  .pset-actions .btn { flex: 1 1 100%; }

  /* the printer picker and the translate row stack */
  .printer-row { flex-wrap: wrap; gap: 6px; padding: 12px; min-height: 56px; }
  .printer-name { margin-left: 0; flex: 1 1 100%; }
  .printer-addr { flex: 1 1 100%; }
  .tr-row .btn { flex: 1 1 100%; }
  .tr-lang { flex: 1 1 100%; min-height: 44px; font-size: 16px; }

  /* cards in the list view become full-width rows */
  .home-cards { grid-template-columns: 1fr; }
  .home-card { padding: 12px; }
  .home-card-main .entry-name { font-size: 15px; }
  .home-cat-sel { min-height: 40px; }
  .wf-intro { margin-bottom: var(--s4); }
  .wf-intro h2 { font-size: 18px; }

  /* overlays: full-bleed, and the sheet's own scroll is the only one */
  .form-overlay, .src-overlay { padding: 0; }
  .form-overlay-head, .src-overlay-head {
    padding: var(--s2) max(var(--s3), env(safe-area-inset-left));
    gap: var(--s2);
  }
  .form-overlay-head h2, .src-overlay-head h2 { font-size: 15px; }
  .fo-actions .btn, .so-title + .fo-actions .btn { min-height: 40px; }
  .pager { padding: var(--s2) max(var(--s3), env(safe-area-inset-left)); gap: 4px; }
  .pg { min-width: 44px; min-height: 44px; }
  .form-overlay-body, .src-overlay-body {
    padding: 0 max(var(--s2), env(safe-area-inset-left))
             max(var(--s3), env(safe-area-inset-bottom));
  }

  /* dialogs: bottom sheet behaviour, so they land under the thumb */
  .modal-wrap { align-items: flex-end; padding: 0; }
  .modal {
    max-width: none; width: 100%;
    border-radius: var(--r-xl) var(--r-xl) 0 0;
    padding: var(--s4) var(--s4) max(var(--s4), env(safe-area-inset-bottom));
    max-height: 88vh; overflow-y: auto;
    overscroll-behavior: contain;
  }
  .modal h3 { font-size: 18px; }
  .modal-actions { gap: var(--s2); }
  .modal-actions .btn { flex: 1 1 0; min-height: 46px; }
  #nc-create { flex: 2 1 0; }
  .nc-row { grid-template-columns: 1fr auto; gap: 6px 8px; padding: 10px; }
  .nc-row .nc-file { grid-column: 1 / -1; }
  .nc-row .nc-label { grid-column: 1; min-height: 44px; font-size: 16px; }
  .nc-row .nc-remove { grid-column: 2; min-height: 44px; padding: 0 12px; }
  .nc-docs-head { flex-wrap: wrap; gap: var(--s2); }
  .nc-drop { min-height: 64px; display: flex; align-items: center; justify-content: center; }

  /* The top bar was authored as a desktop bar: mark, queue, engine, profile, theme. At 390px
     that is three lines of chrome before any work happens. On a phone the queue count is the
     fact the operator needs ("which of these needs me"), the theme toggle is reachable, and
     "engine hermes · agent" is a constant nobody reads mid-task. */
  .bar {
    height: auto; flex-wrap: nowrap; align-items: center;
    gap: var(--s2); padding: var(--s2) max(var(--s3), env(safe-area-inset-left));
  }
  .mark { flex: 0 1 auto; min-width: 0; }
  .mark-text { min-width: 0; }
  .mark-name {
    font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .mark-rose { width: 22px; height: 22px; }
  .bar-meta { flex: 0 1 auto; gap: var(--s2); align-items: center; }
  .bar-meta .kv { display: none; }
  .bar-meta { min-width: 0; overflow: hidden; }
  .bar-meta .kv:has(#counts) { display: inline-flex; min-width: 0; max-width: 100%; }
  .bar-meta .kv:has(#counts) .k { display: none; }   /* "queue" is implied by a count */
  #counts { font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  /* at this width the long queue sentence is clipped, so swap in the short form */
  #counts-short { display: inline; font-size: 12px; white-space: nowrap; }
  .bar-meta .kv:has(#counts) > #counts { display: none; }
  .mark-sub { display: none; }      /* the tabs say it; the mark keeps its name */
  .bar-tools { flex: none; }
  .btn.ghost.icon { width: 44px; height: 44px; }
}

/* Very narrow phones (iPhone SE and similar): three tab labels cannot hold 13px plus a count
   badge in 320px, and "Translations" was clipping mid-word. Tighten the tab row rather than
   truncate a label — a tab whose name is cut off is a defect, not a design. */
@media (max-width: 380px) {
  /* the tab row gets the whole width minus the bar padding, and may scroll as a last resort:
     a tab that cannot fit must still be reachable rather than clipped */
  .wf-tabs { overflow-x: auto; scrollbar-width: none; }
  .wf-tabs::-webkit-scrollbar { display: none; }
  .wf-tab { font-size: 11.5px; padding: 0 2px; gap: 2px; letter-spacing: -0.01em; }
  .wf-tab .wf-n { font-size: 11px; }  /* the count is data, not decoration: the 11px floor holds */
  /* In a 320px row the index (26px) and two thumb-sized actions (110px) leave ~117px for the
     content, and a nowrap "Awaiting approval" pill is 168px — it was being cut off by the
     icons. The pill may wrap to two short lines here; keeping the 44px targets matters more
     than keeping the label on one line. */
  .entry-meta .state, .home-card-meta .state {
    font-size: 11px; padding: 3px 8px;
    white-space: normal; overflow-wrap: anywhere;
    /* the desktop rule is `flex: none`, which pins the pill at its content width no matter how
       narrow the column is; a flex item must be allowed to shrink before it can wrap */
    flex: 0 1 auto; min-width: 0; max-width: 100%;
  }
  .entry-main { min-width: 0; }
  #create-hague, #create-informal, #create-translate { font-size: 12.5px; padding: 0 8px; }
  .case-actions .state { font-size: 11px; padding: 4px 8px; }
  .case-actions .btn { padding: 0 10px; font-size: 13px; }
  /* at 380px even the icon-only back control's row is short: give the name every recovered
     pixel — the chip cap (46vw) and the title now share the row with ~34px of chrome */
  body.case-open .case-actions .state { max-width: 44vw; }
}

/* a phone held sideways has height to spare and width to spend: back to two panes is wrong
   (there is still not room for a rail), so keep the drill-down and just use the width */
@media (max-width: 720px) and (orientation: landscape) and (min-width: 560px) {
  .wfbar { flex-direction: row; align-items: center; flex-wrap: wrap; }
  .wf-tabs { flex: 1 1 100%; }
  .search-wrap { flex: 1 1 18ch; }
  #create-hague, #create-informal, #create-translate { flex: 0 1 auto; padding: 0 14px; }
  .stage, .sheet-scroll { max-width: 92ch; margin: 0 auto; width: 100%; }
}

/* --------------------------------------------------- the agent feed on a phone
 *
 * The row head is icon + kind + title + meta + time + toggle on ONE line. At 390px that leaves the
 * title about 90px, so a file path breaks mid-word ("/tmp/nm3/scan_ch" / "eck.py") and the row
 * reads as a rendering fault. On a phone the head becomes two lines: the kind and the title above,
 * the meta, time and toggle below. Nothing is cut, and nothing wraps inside a word.
 */
@media (max-width: 720px) {
  .act-head {
    flex-wrap: wrap; align-items: center;
    gap: 4px var(--s2);
    padding: 8px 10px;
    row-gap: 2px;
  }
  .act-icon { font-size: 13px; }
  .act-title {
    /* own line, full width, and only break a long token if it truly cannot fit */
    flex: 1 1 100%; order: 3;
    font-size: 13px; line-height: 1.45;
    overflow-wrap: break-word; word-break: normal;
    white-space: normal;
  }
  .act-k { order: 1; }
  .act-meta { order: 2; margin-left: auto; font-size: 11px; }
  .act-time { order: 4; font-size: 11px; }
  .act-toggle { order: 5; margin-left: auto; min-height: 34px; padding: 0 12px; }

  .act-body { font-size: 12px; padding: 9px 11px; max-height: 260px; }

  /* A bubble at 85% of a 390px screen leaves a cramped column with dead space beside it, and a
     long path still breaks. Nearly full width, with the operator's own messages kept distinct by
     their colour and alignment rather than by being narrow. */
  .chat-msg { max-width: 100%; }
  .chat-msg.agent { font-size: 13px; padding: 10px 12px; max-width: 96%; }
  .chat-msg.user { max-width: 88%; }
  .chat-msg .msg-text { overflow-wrap: break-word; word-break: normal; }
  /* an activity row's body is monospace output: keep it from forcing a horizontal scroll */
  .act-body { overflow-x: auto; }
  .chat { padding: var(--s3) var(--s3); }
  /* the feed's own header: the heading and the button share a line, the note goes under them.
     Otherwise "Agent activity" breaks to "AGENT" / "ACTIVITY" and looks broken. */
  .chat-head {
    padding: 10px var(--s3);
    flex-wrap: wrap; align-items: center;
    gap: 2px var(--s2);
  }
  .chat-head h2 { flex: 1 1 auto; font-size: 14px; white-space: nowrap; order: 1; }
  .chat-head .btn { order: 2; flex: none; min-height: 36px; padding: 0 12px; }
  .chat-head .chat-note { order: 3; flex: 1 1 100%; font-size: 12px; }
  .chat-compose { padding: var(--s2) var(--s3) max(var(--s2), env(safe-area-inset-bottom)); }
}

/* =========================================================================== touch sizes
 *
 * Gated on phone GEOMETRY, never on `hover`/`pointer`. This desk reports `(hover: none)` too, so
 * a pointer-based query pulled phone target sizes onto the desktop — the two must not share a
 * condition. A width, or the short-and-wide shape of a landscape phone, cannot match a desktop
 * window, so nothing in here can reach the desk.
 */
@media (max-width: 720px), (max-width: 900px) and (max-height: 500px) {
  /* every control a thumb lands on is at least 44px on its short side */
  .btn { min-height: 44px; }
  .btn.sm { min-height: 40px; }
  .btn.ghost.icon, .btn.icon { width: 44px; height: 44px; }
  .btn-icon { width: 44px; height: 44px; opacity: 1; }
  .wf-tab { min-height: 44px; }
  .wf-menu, .wf-search-toggle { min-height: 44px; }
  .step { min-height: 44px; }
  .pg { min-width: 44px; min-height: 44px; }
  select, .cat-sel, .home-cat-sel { min-height: 44px; }
  /* 16px stops iOS zooming the page when a field takes focus */
  input[type=text], input[type=email], input[type=search], select, textarea { font-size: 16px; }
  /* there is no hover to lean on, so give controls a press state instead */
  .btn:active:not(:disabled) { transform: translateY(1px); }
  .entry:active, .wf-tab:active, .printer-row:active, .home-card-main:active { background: var(--hover); }
}

/* A landscape phone is 844px wide: too wide for the phone rules above, so it keeps the desk's
 * two-pane layout (correctly — there is room). It still needs the drill-down, because the rail
 * and the sheet side by side on a 390px-tall screen leaves neither pane usable. */
@media (orientation: landscape) and (max-height: 500px) and (max-width: 900px) {
  body.on-home .sheet { display: none; }
  body:not(.on-home) .queue { display: none; }
  body:not(.on-home) .case-back { display: inline-flex; }   /* the base rule hides it elsewhere */
  body:not(.on-home) .wfbar { display: none; }
  .case-back { display: none; }
  .queue { border-right: 0; }
  /* the rail is the page here too, so it draws the categorised cards, not the index */
  .rail-body { padding: 0 var(--s3); }
  .queue .col-head { display: none; }
  .home-cards { grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); }
  .home-section-head { position: static; margin: var(--s3) 0 var(--s2); padding: 0; }
  .home-card-acts { opacity: 1; }
  .home-card-acts .btn-icon { width: 40px; height: 40px; }
  .home-card-meta .entry-sub {
    font-size: 13px; white-space: normal; overflow-wrap: anywhere; line-clamp: 2;
    display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  }
  /* the create row is desk-width controls; wrap it so nothing runs past the right edge */
  .wfbar { flex-wrap: wrap; }
  .wf-tools { width: 100%; }
}

/* --------------------------------------------------- the print set's drag affordance
 *
 * Reordering is the operator's own gesture on their own set, so it must read as picking up a row:
 * a grip that invites the drag, a lifted ghost while it moves, and a clear line where it will land.
 */
.pset-item[draggable="true"] { cursor: grab; }
.pset-item[draggable="true"]:active { cursor: grabbing; }
.pset-item.dragging { opacity: .45; }
.pset-item.drop-before,
.pset-item.drop-after { position: relative; }
.pset-item.drop-before::before,
.pset-item.drop-after::after {
  content: ""; position: absolute; left: 0; right: 0; height: 2px;
  background: var(--accent); border-radius: 2px;
}
.pset-item.drop-before::before { top: -1px; }
.pset-item.drop-after::after { bottom: -1px; }
/* the row itself must not flicker while the browser draws the drag image */
.pset-item.dragging .pset-x { pointer-events: none; }

/* ---------------------------------------------------------------- the theme button
 *
 * Three states, one button. The mark shows the state you are IN (not the one you would go to), so
 * the operator can see at a glance whether the console is following their system or overridden:
 *   system — a monitor, "whatever your machine is doing"
 *   light  — a sun
 *   dark   — a half-filled disc
 */
.theme-btn { position: relative; }
.theme-btn .theme-i { display: none; }
.theme-btn[data-theme-state="system"] .theme-i[data-for="system"],
.theme-btn[data-theme-state="light"]  .theme-i[data-for="light"],
.theme-btn[data-theme-state="dark"]   .theme-i[data-for="dark"] { display: block; }
/* on system the button reads as "automatic" rather than as a fixed choice */
.theme-btn[data-theme-state="system"] { color: var(--ink-3); }

/* ------------------------------------------------------------- the court format (E4.1)
 *
 * PROJECT.md §3.2: the formats are the CLIENT'S OWN, held as editable data. This panel shows
 * which one a job is on and, when the system has a view, WHY it recommends one — so the operator
 * can audit the recommendation instead of accepting it (R3). A recommendation is drawn as a
 * warning, never as a settled fact: until the operator confirms it, the job is not on a format.
 */
.fmt-box {
  margin: var(--s3) 0; padding: var(--s3) var(--s4);
  border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--raised);
}
.fmt-box h4 { margin: 0 0 6px; font-size: 13px; font-weight: 660; }
.fmt-box .pset-note b { color: var(--ink); }
.fmt-quiet { color: var(--ink-4); }
.fmt-warn b { color: var(--warn); }
.fmt-warn { border-left: 3px solid var(--warn-line); padding-left: var(--s3); }
.fmt-bad b { color: var(--danger); }
.fmt-bad { border-left: 3px solid var(--danger-line); padding-left: var(--s3); }
.fmt-row { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; margin-top: var(--s3); }

/* the recommendation: a summary the operator opens, because the reasons are the point */
.fmt-match { margin-top: var(--s3); border-top: 1px solid var(--line); padding-top: var(--s2); }
.fmt-match > summary {
  cursor: pointer; list-style: none; display: flex; align-items: center;
  gap: var(--s2); font-size: 13px; color: var(--ink-2);
}
.fmt-match > summary::-webkit-details-marker { display: none; }
.fmt-match > summary .caret { color: var(--ink-4); transition: transform var(--dur) var(--ease); }
.fmt-match[open] > summary .caret { transform: rotate(90deg); }
.fmt-match-title b { color: var(--accent); }
.fmt-terms {
  margin: var(--s2) 0 4px; font-size: 11px; text-transform: uppercase;
  letter-spacing: 0.08em; color: var(--ink-3); font-weight: 650;
}
.fmt-terms.mono { text-transform: none; letter-spacing: 0; font-size: 12px; }
.fmt-why { margin: 0 0 var(--s2); padding-left: 18px; font-size: 12px; color: var(--ink-2); line-height: 1.6; }
.fmt-why li { margin: 2px 0; }

/* every format the system considered, with the terms that hit and the terms that did not */
.fmt-cand-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--s2); }
.fmt-cand {
  border: 1px solid var(--line); border-radius: var(--r-md);
  padding: var(--s2) var(--s3); background: var(--sunken);
}
.fmt-cand.has { border-color: var(--accent-line); background: var(--accent-soft); }
.fmt-cand-name { font-size: 13px; font-weight: 640; color: var(--ink); }
.fmt-cand-score {
  font-family: var(--mono); font-size: 11px; margin-left: var(--s2);
  color: var(--accent); text-transform: uppercase; letter-spacing: 0.06em;
}
.fmt-cand-score.off { color: var(--ink-4); }
.fmt-cand-att { font-size: 11px; color: var(--ink-3); margin-left: var(--s2); }
.fmt-notes { margin: 6px 0 0; padding-left: 16px; font-size: 12px; color: var(--ink-2); line-height: 1.55; }
.fmt-notes li { margin: 1px 0; }

/* the picker: formats by NAME and what they apply to, never by id */
.fmt-picker { margin-top: var(--s3); }
.fmt-pick-list { list-style: none; margin: var(--s2) 0 0; padding: 0; display: flex; flex-direction: column; gap: var(--s2); }
.fmt-pick-btn {
  width: 100%; text-align: left; font: inherit; cursor: pointer;
  display: flex; flex-direction: column; gap: 3px;
  padding: var(--s2) var(--s3);
  background: var(--sunken); color: var(--ink);
  border: 1px solid var(--line); border-radius: var(--r-md);
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.fmt-pick-btn:hover { background: var(--hover); border-color: var(--line-2); }
.fmt-pick-btn[aria-current="true"] { border-color: var(--accent); background: var(--accent-soft); }
.fmt-pick-name { font-size: 13px; font-weight: 640; }
.fmt-pick-meta { font-size: 11px; color: var(--ink-3); overflow-wrap: anywhere; }

/* the editor: the pipeline's own fields, none of them pre-filled (R1) */
.fmt-editor {
  margin-top: var(--s3); padding: var(--s3);
  border: 1px solid var(--line-2); border-radius: var(--r-md);
  background: var(--sunken);
}
.fmt-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: var(--s2); }
.fmt-editor .cf { display: flex; flex-direction: column; gap: 4px; margin: 6px 0; }
.fmt-editor .cf span { font-size: 11px; text-transform: uppercase; letter-spacing: 0.07em; color: var(--ink-3); font-weight: 650; }
.fmt-editor input, .fmt-editor textarea, .fmt-editor select {
  font: inherit; font-size: 13px; padding: 7px 10px;
  background: var(--raised); color: var(--ink);
  border: 1px solid var(--line-2); border-radius: var(--r-sm);
}
.fmt-editor textarea { font-family: var(--mono); font-size: 12px; resize: vertical; }
/* The one CHECKBOX in the format editor: a fact about the court, not a field with a value. The
   label text and the box sit on one line, and the note under them is ordinary prose rather than
   the upper-case field label the other rows carry. */
.fmt-editor .cf-check > span:first-child { text-transform: none; letter-spacing: 0;
                                           font-size: 12px; }
.fmt-editor .cf-check input[type="checkbox"] { width: 16px; height: 16px; padding: 0;
                                               align-self: flex-start; accent-color: var(--accent); }
.fmt-editor .cf-check .pset-note { text-transform: none; letter-spacing: 0; font-weight: 400;
                                   margin: 0; }
.fmt-editor input:focus-visible, .fmt-editor textarea:focus-visible, .fmt-editor select:focus-visible {
  outline: none; border-color: var(--accent); box-shadow: var(--ring);
}

/* ---------------------------------------------- the court-format panel on a phone
 *
 * Phone rules are gated on GEOMETRY only — a width or shape no desktop window has. Never on
 * `hover`/`pointer`: this desk reports `hover: none`, so a pointer query leaks onto the desktop.
 *
 * The panel carries court and county names, which are long, and a candidate list whose rows are
 * a name plus a note per term considered. Left alone, a long `mono` line sets a min-content floor
 * and widens the whole phone page sideways — the trap that has bitten this console before. So the
 * grid tracks get `minmax(0, 1fr)` and the text gets `overflow-wrap: anywhere`, rather than a
 * `max-width` on the children.
 */
@media (max-width: 720px) {
  .fmt-box { padding: var(--s3); }
  .fmt-grid { grid-template-columns: minmax(0, 1fr); }
  .fmt-row { flex-wrap: wrap; }
  .fmt-row .btn { flex: 0 1 auto; min-height: 44px; }
  .fmt-match > summary { align-items: flex-start; }
  .fmt-match-title { min-width: 0; overflow-wrap: anywhere; }
  .fmt-terms.mono { overflow-wrap: anywhere; }
  .fmt-cand, .fmt-notes li, .fmt-pick-btn { min-width: 0; overflow-wrap: anywhere; }
  .fmt-editor .cf { min-width: 0; }
  .fmt-editor input, .fmt-editor textarea, .fmt-editor select { min-height: 44px; }
}

/* a landscape phone has height to spare: two columns is right there, not one */
@media (max-width: 720px) and (orientation: landscape) and (min-width: 560px) {
  .fmt-grid { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); }
}

/* ============================================================== the attempt log (informal)

   Every attempt is its own card; every READING of that attempt is a row inside it, with the one
   taken marked. A candidate reading must never look like the chosen one, so the effective row
   carries the accent and the others are quiet — the operator is looking for "which of these is
   the one that stands", and colour-blind-quiet is not enough on its own, so each row also says
   so in words.

   Grid tracks are `minmax(0,1fr)` and the long values carry `overflow-wrap`: a file path, a
   record id and an ISO instant are all unbroken tokens, and an unbroken token inside an auto
   track sets a min-content floor that scrolls the whole phone page sideways. */
.att-panel { margin: var(--s4) 0 0; }
.att-panel h3 { margin: 0 0 var(--s2); font-size: 11px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3); font-weight: 680; }
.att-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--s3); }
.att-card { min-width: 0; overflow-wrap: anywhere; }
.att-card h4 { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.att-quiet { color: var(--ink-3); font-weight: 400; }
.att-k { display: inline-block; min-width: 62px; font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-3); font-weight: 680; }
.att-read { border: 1px solid var(--line); border-left: 3px solid transparent; border-radius: var(--r-sm); padding: var(--s2) var(--s3); margin: var(--s2) 0 0; background: var(--raised); }
.att-read.on { border-left-color: var(--accent); }
.att-read-head { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-bottom: 4px; }
.att-rec { font-size: 11px; color: var(--ink-2); }
.att-tag { font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em; font-weight: 680; padding: 1px 6px; border-radius: 999px; border: 1px solid var(--accent); color: var(--accent); white-space: nowrap; }
.att-tag.quiet { border-color: var(--line); color: var(--ink-3); }
.att-fields > div { font-size: 13px; color: var(--ink); margin-top: 2px; }
.att-fields .mono { margin-left: 6px; font-size: 11px; }
.att-null { color: var(--ink-3); font-style: italic; }
.att-why, .att-edit { margin: var(--s2) 0 0; font-size: 12px; color: var(--ink-2); border-top: 1px solid var(--line); padding-top: 6px; }
.att-edit { border-top: 0; padding-top: 2px; }
.att-edit .mono { display: block; margin-top: 2px; font-size: 11px; }
.att-flags { margin: var(--s2) 0 0; }
.att-flags li { font-size: 12px; }
.att-gaps { margin-top: var(--s3); }
.att-summary { margin: 0; font-size: 13px; white-space: pre-wrap; color: var(--ink); }

@media (max-width: 720px) {
  .att-k { min-width: 0; display: block; }
  .att-fields .mono { display: block; margin-left: 0; }
  .att-read, .att-card { min-width: 0; }
}
.att-sum-card { margin-top: var(--s3); }

/* ------------------------------------------------------------------ My servers

   The client's own process-server roster, drawn inside the same overlay shell the filled form
   uses. A surface over /api/servers: nothing here decides what a caller may see or change, and
   a refusal is rendered in the server's own words. */

/* Scoped to this overlay: the row controls are `btn ghost`, which in the desk's shell renders as
   near-naked text. Inside a card the actions are the row's primary affordance, so they get a real
   edge here — without touching `.btn` itself, which every other screen depends on. */
#servers-overlay .btn.ghost {
  border-color: var(--line-2); background: var(--raised); color: var(--ink-2);
  box-shadow: var(--e1);
}
#servers-overlay .btn.ghost:hover:not(:disabled) {
  border-color: var(--line-3); background: var(--hover); color: var(--ink);
}
#servers-overlay .btn.ghost:disabled { border-color: var(--line); background: var(--sunken); }
#servers-overlay .btn.sm { height: 30px; }

.srv-caller {
  display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap;
  padding: var(--s3) var(--s4); margin-bottom: var(--s3);
  background: var(--sunken); border: 1px solid var(--line); border-radius: var(--r-md);
}
.srv-caller-label {
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.09em;
  color: var(--ink-3); font-weight: 640;
}
.srv-caller select, .srv-caller input {
  font: inherit; font-size: 13px; height: 30px; padding: 0 var(--s2);
  background: var(--raised); color: var(--ink);
  border: 1px solid var(--line-2); border-radius: var(--r-sm);
}
.srv-caller input { min-width: 190px; }
.srv-caller input:disabled { color: var(--ink-4); background: var(--sunken); }
.srv-caller-note { font-size: 12px; color: var(--ink-4); }

.srv-hint { font-size: 13px; color: var(--ink-3); margin: 0 0 var(--s3); }
.srv-hint.sm { font-size: 12px; }

.srv-banner {
  padding: var(--s3) var(--s4); margin-bottom: var(--s3);
  border: 1px solid var(--accent-line); background: var(--accent-soft); color: var(--ink);
  border-radius: var(--r-md); font-size: 13px; line-height: 1.5;
  overflow-wrap: anywhere;
}
.srv-banner[data-kind="error"] {
  border-color: var(--danger-line); background: var(--danger-soft); color: var(--danger);
}
.srv-banner[data-kind="info"] {
  border-color: var(--ok-line); background: var(--ok-soft); color: var(--ok);
}
/* A write that LANDED but whose list re-read did not answer, and a roster read that came back
   without an answer: neither is an error (nothing was refused) and neither is good news. The
   amber the console already uses for "not settled / confirm this" is the honest one, so the
   operator can tell it apart from a green acknowledgement and a red refusal at a glance. */
.srv-banner[data-kind="warn"] {
  border-color: var(--warn-line); background: var(--warn-soft); color: var(--warn);
}

.srv-add {
  padding: var(--s4); margin-bottom: var(--s4);
  background: var(--raised); border: 1px solid var(--line-2); border-radius: var(--r-md);
  box-shadow: var(--e1);
}
.srv-add h3 { margin: 0 0 var(--s3); font-size: 15px; font-weight: 680; }
.srv-fields { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--s3); }
.srv-field { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
.srv-field.wide { grid-column: 1 / -1; }
.srv-field span {
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.09em;
  color: var(--ink-3); font-weight: 640;
}
.srv-field input, .srv-field select {
  font: inherit; font-size: 13px; height: 32px; padding: 0 var(--s2);
  background: var(--raised); color: var(--ink);
  border: 1px solid var(--line-2); border-radius: var(--r-sm); min-width: 0;
}
.srv-field input:focus, .srv-field select:focus { outline: none; border-color: var(--accent); box-shadow: var(--ring); }
.srv-add .modal-actions { margin-top: var(--s4); }

.srv-pool { margin-bottom: var(--s5); }
.srv-pool h3 {
  display: flex; align-items: center; gap: var(--s2);
  margin: 0 0 var(--s2); font-size: 13px; font-weight: 680;
  text-transform: uppercase; letter-spacing: 0.07em; color: var(--ink-2);
}
.srv-count { font-size: 12px; color: var(--ink-4); }
.srv-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s3); }

.srv-item {
  padding: var(--s3) var(--s4); background: var(--surface);
  border: 1px solid var(--line); border-radius: var(--r-md);
}
.srv-item-main { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; }
.srv-name { font-size: 15px; font-weight: 640; color: var(--ink); overflow-wrap: anywhere; }

.srv-pill {
  font-size: 11px; font-weight: 640; letter-spacing: 0.06em; text-transform: uppercase;
  padding: 3px 8px; border-radius: var(--r-pill);
  border: 1px solid var(--line-2); background: var(--sunken); color: var(--ink-3);
}
.srv-pill.on   { border-color: var(--ok-line); background: var(--ok-soft); color: var(--ok); }
.srv-pill.off  { border-color: var(--danger-line); background: var(--danger-soft); color: var(--danger); }
.srv-pill.unset{ border-color: var(--warn-line); background: var(--warn-soft); color: var(--warn); }

.srv-tag { font-size: 12px; color: var(--ink-4); }
.srv-meta { margin: 4px 0 0; font-size: 11px; color: var(--ink-4); overflow-wrap: anywhere; }
.srv-notes { margin: var(--s2) 0 0; font-size: 13px; color: var(--ink-2); overflow-wrap: anywhere; }
.srv-held {
  margin: var(--s2) 0 0; font-size: 12px; line-height: 1.5;
  color: var(--warn); overflow-wrap: anywhere;
}
.srv-held.free { color: var(--ink-4); }
/* The holding could not be READ — a third state, and it must not look like the free row's
   quiet grey: an unknown holding is a reason the removal will be refused (503), so it wears
   the same amber as a live holding, in italics to say the holding is not known. */
.srv-held.unknown { color: var(--warn); font-style: italic; }
.srv-why { margin: 4px 0 0; font-size: 12px; color: var(--ink-3); line-height: 1.5; }

.srv-acts { display: flex; gap: var(--s2); flex-wrap: wrap; margin-top: var(--s3); }
.srv-acts .btn { flex: 0 0 auto; }
.srv-empty { font-size: 13px; color: var(--ink-3); }

/* ===== candidate pools (E1.2): a job's two pools, and which pool each server came from =====

   The rows are deliberately NOT colour-coded by pool alone. A tag reading "platform" or
   "client:acme" is the fact; the accent marks the row that is the CLIENT's own, because that is
   the one whose ownership matters to the operator reading the screen. Colour is never the only
   carrier of a meaning here — every state is also written out, so the panel survives a
   monochrome screen and a screenshot. */

.cand-panel { margin: var(--s4) 0 0; }
.cand-panel .cand-grid { align-items: start; }
.cand-card { min-width: 0; }
.cand-card > .pset-note { margin: 0 0 var(--s2); }
.cand-list { list-style: none; margin: var(--s2) 0 0; padding: 0; }
.cand-row {
  display: flex; align-items: baseline; gap: var(--s2); flex-wrap: wrap;
  padding: 5px 0; border-top: 1px solid var(--line);
}
.cand-row:first-child { border-top: 0; }
.cand-name { font-size: 13px; color: var(--ink); flex: 1 1 8em; min-width: 0; overflow-wrap: anywhere; }
.cand-pool {
  font-size: 11px; padding: 1px 6px; border-radius: 999px;
  border: 1px solid var(--line); color: var(--ink-2); white-space: nowrap;
}
.cand-pool[data-pool="client"] { border-color: var(--accent); color: var(--accent); }
.cand-av { font-size: 11px; color: var(--ink-3); white-space: nowrap; }
.cand-av[data-av="false"] { color: var(--ink-2); text-decoration: line-through; }
.cand-unresolved { color: var(--ink-2); font-weight: 620; }
.cand-panel .att-flags { margin: var(--s3) 0 0; }

/* ============ dispatch — assign this job to a named server (E2.1) ============
   A MANAGED model: the operator names one server from the two pools the job may draw on. There is
   no bid board and nothing is offered to anybody, so there is no control here for posting, bidding
   or claiming — and the panel says so in words, because an operator who cannot find one needs to
   know it never existed rather than to conclude the screen is broken. */

.disp-panel { margin: var(--s4) 0 0; }
.disp-state { display: block; margin: var(--s2) 0 var(--s3); padding: var(--s2) var(--s3);
              background: var(--sunken); border: 1px solid var(--line-3);
              border-radius: var(--r-sm); font-size: 12px; overflow-wrap: anywhere; }
.disp-state[data-state="assigned"] { border-color: var(--ok-line); background: var(--ok-soft); }
.disp-state[data-state="assigned_missing"] { border-color: var(--danger); }
.disp-state[data-state="unknown"] { border-color: var(--danger); }
.disp-who { min-width: 0; }
.disp-who.disp-none { /* no bar: "nobody assigned" is a state, not a severity */ }
/* `.req-card .mono` is a BLOCK (label-value pairs need it), and this one sentence carries two mono
   values INLINE. Left as blocks, each becomes its own line and the sentence breaks into orphaned
   fragments with a lone "." between them — measured on a rendered shot. Scoped to this one
   paragraph so the block behaviour the rest of the card relies on is untouched. */
.disp-who p.disp-then .mono { display: inline; margin-top: 0; }
.disp-k { display: inline-block; min-width: 0; color: var(--ink-3);
          text-transform: uppercase; letter-spacing: .04em; font-size: 10px;
          margin-right: var(--s2); }
.disp-act { margin: var(--s3) 0 0; min-width: 0; }
.disp-label { display: block; margin: var(--s2) 0 3px; font-size: 11px; color: var(--ink-3);
              text-transform: uppercase; letter-spacing: .04em; }
.disp-pick, .disp-instruction { width: 100%; max-width: 100%; min-width: 0;
                                font-family: var(--mono); font-size: 13px; }
.disp-act .btn { margin-top: var(--s3); }
/* `.disp-unassign` has no bar: unassigning is an action's outcome, not a severity. */
.disp-unassign { padding-left: var(--s2); }
.disp-blocked .notes { margin: var(--s2) 0 0; }
.disp-blocked li { overflow-wrap: anywhere; }
.disp-hist { margin: var(--s3) 0 0; }
.disp-hist > summary { cursor: pointer; font-size: 12px; color: var(--ink-2); overflow-wrap: anywhere; }
.disp-hist-list { list-style: none; margin: var(--s2) 0 0; padding: 0;
                  display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--s2); }
.disp-hist-row { display: flex; flex-wrap: wrap; gap: var(--s2); align-items: baseline;
                 border-left: 2px solid transparent; padding-left: var(--s2); }
.disp-hist-row.in-force { border-left-color: var(--accent); }  /* only the in-force row carries the bar */
.disp-hist-act { text-transform: uppercase; font-size: 10px; letter-spacing: .04em;
                 color: var(--ink-3); }
.disp-hist-name { color: var(--ink); overflow-wrap: anywhere; }
.disp-hist-instr { overflow-wrap: anywhere; }
.disp-chip { font-size: 10px; letter-spacing: .04em; text-transform: uppercase;
             padding: 1px 6px; border-radius: var(--r-pill);
             background: var(--accent-soft); color: var(--accent);
             border: 1px solid var(--accent-line); }

/* THE JOB'S STATUS (E2.2). The chip a step carries is the whole point of the panel: "reached",
   "not reached" and "not established" are three DIFFERENT facts about a job and are drawn as
   three different things. A step the records do not settle is neither ticked nor blanked, because
   those read as "done" and "not done" and neither is what the record says. */
.st-panel { margin: var(--s4) 0 0; }
.st-where { overflow-wrap: anywhere; }
.st-now { overflow-wrap: anywhere; }
.st-steps { list-style: none; margin: var(--s3) 0 0; padding: 0;
            display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--s2); }
.st-step { display: flex; flex-wrap: wrap; gap: var(--s2); align-items: baseline;
           border-left: 2px solid transparent; padding-left: var(--s2); min-width: 0; }
/* the bar appears only when a step's state says something: reached (ok), unknown (amber),
   current (accent). An unreached step is quiet — grey bars on every row drowned the timeline. */
.st-step[data-reached="true"] { border-left-color: var(--ok-line); }
.st-step[data-reached="unknown"] { border-left-color: var(--amber, #c98a00); }
.st-step.current { border-left-color: var(--accent); background: var(--sunken); }
.st-step-n { font-size: 10px; color: var(--ink-3); min-width: 12px; }
.st-step-label { font-weight: 640; overflow-wrap: anywhere; }
.st-step-when { overflow-wrap: anywhere; }
/* The REASON a step is not established is the text that must be readable, so it is a full-width
   line under the row rather than a tooltip or a truncation — an operator who cannot see why a
   step is unsettled would read the chip as a fault in the job. */
.st-step-why { flex: 1 0 100%; color: var(--ink-3); font-size: 11px; overflow-wrap: anywhere; }
.st-chip { font-size: 10px; letter-spacing: .04em; text-transform: uppercase;
           padding: 1px 6px; border-radius: var(--r-pill);
           border: 1px solid var(--line-3); color: var(--ink-3); background: var(--sunken); }
.st-chip-done { border-color: var(--ok-line); background: var(--ok-soft); color: var(--ink-2); }
.st-chip-current { border-color: var(--accent-line); background: var(--accent-soft); color: var(--accent); }
.st-chip-unknown { border-color: var(--amber, #c98a00); }
.st-moves { margin: var(--s3) 0 0; }
.st-move { display: flex; flex-wrap: wrap; gap: var(--s2); align-items: baseline;
           border-left: 2px solid transparent; padding-left: var(--s2); }
.st-move-act { text-transform: uppercase; font-size: 10px; letter-spacing: .04em; color: var(--ink-3); }
.st-move-to { overflow-wrap: anywhere; }
.st-move-note { overflow-wrap: anywhere; }
.st-gaps li { overflow-wrap: anywhere; }

@media (max-width: 720px) {
  .st-step, .st-move { display: block; }
  .st-step > *, .st-move > * { display: block; }
}

@media (max-width: 720px) {
  /* A label, a value and the action row cannot share a 390px line. The value goes on its own
     line under its label, and the two buttons take the full width they need. */
  .disp-k { display: block; min-width: 0; }
  .disp-act .btn { width: 100%; }
  .disp-hist-row { display: block; }
  .disp-hist-row > * { display: block; }
}

@media (max-width: 720px) {
  /* A row's three parts cannot share a 390px line: the name would be squeezed to nothing and a
     server name would wrap mid-word. Name on its own line, the two tags beneath it. */
  .cand-row { display: block; }
  .cand-name { display: block; margin-bottom: 3px; }
  .cand-pool, .cand-av { display: inline-block; margin-right: var(--s2); }
}

/* ============ informal workflow step card (E8.1) ============
   The card is drawn from the machine the server sends; these are only its clothes. Nothing here
   decides anything about the sequence. */

.informal-status { display: flex; flex-wrap: wrap; gap: var(--s2); align-items: center;
                   margin: var(--s2) 0 var(--s3); }
#informal-status-key { font-size: 12px; color: var(--ink-2); background: var(--sunken);
                       border: 1px solid var(--line-3); border-radius: var(--r-sm);
                       padding: 2px 7px; }
.informal-flag { font-size: 11px; font-weight: 640; letter-spacing: 0.06em; text-transform: uppercase;
                 color: var(--accent); background: var(--accent-soft);
                 border: 1px solid var(--accent-line); border-radius: var(--r-pill);
                 padding: 2px 8px; }

.informal-acts { list-style: none; margin: var(--s3) 0 0; padding: 0; display: flex;
                 flex-direction: column; gap: var(--s2); }
.informal-act { display: flex; gap: var(--s3); align-items: baseline; flex-wrap: wrap; }
/* the note beside a control explains what it will do — it must be readable at phone width, so it
   takes its own line rather than being clipped beside the button */
.informal-note { font-size: 12px; color: var(--ink-3); flex: 1 1 18ch; min-width: 0;
                 overflow-wrap: anywhere; }

.informal-blocked, .informal-absent-box { margin-top: var(--s4); padding: var(--s3) var(--s4);
  border: 1px solid var(--line-3); border-left: 3px solid var(--warn-line);
  border-radius: var(--r-md); background: var(--surface-2, var(--sunken)); }
/* the AMBER hex was a literal outside the palette; `--warn-line` is the same "confirm this"
   language on the token the theme owns. `informal-absent-box` overrides to grey: absence of a
   document is a fact, not a warning. */
.informal-absent-box { border-left-color: var(--line-3); }
.informal-blocked h4, .informal-absent-box h4 {
  margin: 0 0 var(--s2); font-size: 11px; text-transform: uppercase; letter-spacing: 0.09em;
  color: var(--ink-3); font-weight: 660;
}
.informal-blocked ul, .informal-absent-box ul { list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: var(--s3); }
.informal-stop { display: flex; flex-direction: column; gap: 3px; }
.informal-stop-label { font-size: 13px; font-weight: 600; color: var(--ink-2); }
/* the server's own reason, verbatim: a long sentence must wrap rather than widen the page */
.informal-stop-why { font-size: 12px; color: var(--ink-3); max-width: 70ch;
                     overflow-wrap: anywhere; }
.informal-absent { font-size: 12.5px; color: var(--ink-2); overflow-wrap: anywhere; }

@media (max-width: 720px) {
  /* the action row's note cannot share a phone line with its button */
  .informal-act { display: block; }
  .informal-act .btn { width: 100%; margin-bottom: 4px; }
  .informal-note { display: block; }
}

/* ------------------------------------------- what the client has been told (E6.1)

   The composed message is shown as it would arrive — wrapped, never clipped. It is prose a
   client reads, so it keeps its own line breaks (`pre-wrap`) and is capped at a readable measure
   rather than stretched across the panel. */
.notif-panel { margin-top: var(--s5, var(--s4)); }
.notif-panel h3 { margin: 0 0 var(--s2); font-size: 11px; text-transform: uppercase;
                  letter-spacing: 0.1em; color: var(--ink-3); font-weight: 680; }
.notif-head { margin: var(--s4) 0 var(--s2); font-size: 11px; text-transform: uppercase;
              letter-spacing: 0.09em; color: var(--ink-3); font-weight: 660; }
.notif-channel { margin-top: var(--s2); }
.notif-when { margin-top: 2px; }
.notif-text { margin: var(--s2) 0 0; font-family: var(--sans, inherit); font-size: 13px;
              line-height: 1.55; color: var(--ink); white-space: pre-wrap;
              overflow-wrap: anywhere; max-width: 68ch; }
.notif-card.quiet { background: var(--sunken); }
.notif-hold { border-left: 3px solid var(--warn-line); padding-left: var(--s3);
              color: var(--ink-2); }  /* token, not a literal amber outside the palette */
.notif-acts { margin-top: var(--s3); display: flex; flex-wrap: wrap; gap: var(--s2); }

@media (max-width: 720px) {
  /* one column, and the send control full width: it is the operator's action, not a decoration */
  .notif-acts .btn { width: 100%; }
  .notif-text { font-size: 13px; }
}

/* ---------------------------------------- which attempts print (§3.2, E3.4) — appended at the end
 * Kept at the END of the stylesheet on purpose: a block placed above a later rule that touches the
 * same element loses silently, and this one deliberately overrides nothing. It is drawn INSIDE
 * `.fmt-box` because the rule belongs to the format that owns it.
 *
 * The panel carries NO CONTROL — a toggle here is the per-job setting §3.2 forbids — so there is
 * no button rule and no interactive state to style. What it does carry is a warning when the rule
 * is not settled, which must not read as a settled decision.
 */
.ap-box { margin-top: var(--s3); border-top: 1px solid var(--line); padding-top: var(--s2); }
.ap-head { margin: 0 0 6px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.1em;
           color: var(--ink-3); font-weight: 680; }
.ap-quiet { color: var(--ink-4); }
.ap-warn { border-left: 3px solid var(--warn-line); padding-left: var(--s3); }
.ap-warn b { color: var(--warn); }
.ap-bad { border-left: 3px solid var(--danger-line); padding-left: var(--s3); }
.ap-bad b { color: var(--danger); }
.ap-list { margin: var(--s1) 0 0; }
.ap-rows { margin: var(--s2) 0 0; padding-left: 0; list-style: none; counter-reset: ap; }
.ap-row { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s2);
          padding: var(--s2) 0; border-top: 1px solid var(--line); }
.ap-row:first-child { border-top: 0; }
.ap-line { flex: 1 1 22ch; min-width: 0; font-size: 13px; line-height: 1.5; color: var(--ink);
           overflow-wrap: anywhere; }
/* the record the line was built from: R2, the value's provenance one step from the value */
.ap-trace { flex: 0 1 auto; font-size: 11px; color: var(--ink-4); overflow-wrap: anywhere; }

@media (max-width: 720px) {
  /* one line per attempt, the trace under it: a long method text and an id on one row at 390px
     leaves the line ~90px and breaks a word in half */
  .ap-row { display: block; }
  .ap-trace { display: block; margin-top: 2px; }
}

/* ====================================================== photos on an attempt (E3.3)

 * A photo documents ONE attempt, so everything here renders INSIDE that attempt's reading row
 * (`.att-read`), never at job level. Three things have to stay readable together:
 *
 *   * the image itself, openable in one click from the record it hangs off (R2);
 *   * the photo's OWN time and the file's own EXIF time, drawn as two separate readings — they
 *     are different facts and the page must not merge them into one;
 *   * the conflicts, which are flags and not decisions: when the record and the file disagree,
 *     both values stay on screen.
 *
 * `min-width: 0` and `overflow-wrap: anywhere` on the row are not decoration: a case-relative
 * path inside a grid track sets a min-content floor that widens the whole page on a phone (the
 * trap `.req-card` hit at 457px inside a 390px viewport).
 */
.att-photos { margin: var(--s1) 0 0; padding-left: 0; list-style: none;
              display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--s2); }
.att-photo { min-width: 0; border: 1px solid var(--line); border-radius: var(--r-sm);
             padding: var(--s2); background: var(--raised); }
.att-photo-head { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; min-width: 0; }
.att-photo-head .mono { min-width: 0; overflow-wrap: anywhere; }
.att-photo-img { display: block; margin-top: 6px; }
.att-photo-img img { display: block; max-width: 100%; height: auto; border-radius: var(--r-sm);
                     border: 1px solid var(--line-2); }
.att-photo-cap { margin-top: 4px; font-size: 12px; color: var(--ink-2); }
.att-photo-missing { margin-top: 6px; font-size: 12px; color: var(--warn);
                     border-left: 3px solid var(--warn-line); padding-left: var(--s2); }
.att-photo .att-quiet { font-size: 11px; overflow-wrap: anywhere; }

/* A disagreement between the record and the file: a FLAG, and it says which two values disagree.
   It is not styled as an error the operator must clear — it is a reading the page will not
   resolve on its own, exactly like a contradictory source on a field. */
.att-conflict { margin-top: 4px; font-size: 12px; color: var(--danger);
                border-left: 3px solid var(--danger-line); padding-left: var(--s2); }
.att-conflict .mono { display: block; color: var(--ink-3); font-size: 11px;
                      overflow-wrap: anywhere; }

/* The exhibit page (E4.6): the photo, and the ATTEMPT's own date and time beside it. The grid is
   minmax(0, 1fr) for the same reason as above. */
.att-exhibit { margin-top: var(--s3); }
.att-exhibit-grid { display: grid; grid-template-columns: minmax(0, 160px) minmax(0, 1fr);
                    gap: var(--s3); align-items: start; }
.att-exhibit-grid img { display: block; max-width: 100%; height: auto; border-radius: var(--r-sm);
                        border: 1px solid var(--line-2); }
.att-not-settled { border-left: 3px solid var(--warn-line); }
.att-not-settled .att-why { border-top: 0; }

@media (max-width: 720px) {
  /* one column: a 160px thumbnail beside a label at 390px leaves the label ~180px and breaks the
     attempt id mid-word */
  .att-exhibit-grid { grid-template-columns: minmax(0, 1fr); }
  .att-photo-head { display: block; }
  .att-photo-head .mono { display: block; }
}

/* The append-only verdict (E7.2). The panel only appears when a record store is not what it was
   appended as, or is not covered yet, so it reads as an alarm rather than a fixture. `minmax(0,
   1fr)` on the track and `overflow-wrap: anywhere` on the problem text, because these lines carry
   file names and hashes and a long one must not set a min-content floor that widens a phone page. */
.ao-panel { border-left: 3px solid var(--warn-line); }
.ao-panel h3.ao-broken { color: var(--danger); }
.ao-panel .ao-store { border-left: 3px solid var(--warn-line); }
.ao-panel .ao-store-broken { border-left-color: var(--danger); }
.ao-panel .ao-problems { margin: var(--s2) 0 0; padding-left: 1.1em;
                         list-style: disc; }
.ao-panel .ao-problems li { font-size: 12px; color: var(--ink-3);
                            overflow-wrap: anywhere; }
.ao-panel .ao-store h4 .mono { display: block; }

/* ====================================================== merge-field mapping (E4.3)
   The panel draws the SERVER's map: which of the job's values goes on which blank of the office's
   own supplied PDF, WHY that blank was chosen, and every blank it could have been with its own
   reasons. Nothing here is computed in CSS — the classes only say what state a row is in.

   `minmax(0, 1fr)` on the tracks and `overflow-wrap: anywhere` on the value and the reasons,
   because these lines carry addresses, quotes and file names: a long one must not set a
   min-content floor that widens the page at phone width. */
.map-box { border-left: 3px solid var(--accent-line); }
.map-box .map-quiet { color: var(--ink-3); }
.map-box .map-warn { color: var(--warn); }
.map-box .map-null { color: var(--ink-3); font-style: italic; }
.map-sec { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--s2); }
.map-unmapped > summary { cursor: pointer; color: var(--ink-2); font-size: 13px;
                          padding: var(--s2) 0; }
.map-withheld > summary { cursor: pointer; color: var(--ink-2); font-size: 13px;
                          padding: var(--s2) 0; }
.map-card { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--s1);
            margin: 0 0 var(--s2); }
.map-card.off { /* off is a state, not a severity: no bar */ }
.map-card.on { border-left: 3px solid var(--ok-line); }
.map-head { display: flex; flex-wrap: wrap; gap: var(--s2); align-items: baseline; }
.map-label { font-weight: 600; color: var(--ink); }
.map-key { color: var(--ink-3); font-size: 11px; }
.map-chip { font-size: 10px; letter-spacing: .04em; text-transform: uppercase;
            padding: 1px 6px; border-radius: var(--r-pill);
            background: var(--ok-soft); color: var(--ok); border: 1px solid var(--ok-line); }
.map-chip.pinned { background: var(--accent-soft); color: var(--accent);
                   border-color: var(--accent-line); }
.map-chip.off { background: transparent; color: var(--ink-3); border-color: var(--line-2); }
.map-line { display: block; font-size: 12px; color: var(--ink-2); }
.map-k { display: inline-block; min-width: 3.6em; color: var(--ink-3);
         text-transform: uppercase; letter-spacing: .04em; font-size: 10px; }
.map-val, .map-blank, .map-src, .map-via { overflow-wrap: anywhere; }
.map-val { color: var(--ink); }
.map-blank { color: var(--ink-2); }
.map-via, .map-src { color: var(--ink-3); }
.map-why { margin: var(--s1) 0 0; padding-left: 1.1em; list-style: disc; }
.map-why li { font-size: 12px; color: var(--ink-2); overflow-wrap: anywhere; }
.map-reason { margin: var(--s1) 0 0; font-size: 12px; color: var(--ink-2);
              overflow-wrap: anywhere; }
.map-alts > summary { cursor: pointer; font-size: 11px; color: var(--ink-3);
                      padding: 2px 0; }
.map-alt-list { list-style: none; margin: var(--s1) 0 0; padding: 0;
                display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--s2); }
.map-alt { padding-left: var(--s2); font-size: 12px; }  /* a secondary reading, no bar needed */
.map-alt-blank { display: block; color: var(--ink-2); overflow-wrap: anywhere; }
.map-alt-score { display: inline-block; color: var(--ink-3); font-size: 11px; }
.map-alt-text { display: block; color: var(--ink-2); overflow-wrap: anywhere; }
.map-alt-why { margin: 2px 0 var(--s1); padding-left: 1.1em; list-style: disc;
               color: var(--ink-3); font-size: 11px; }
.map-alt-why li { overflow-wrap: anywhere; }
.map-conflicts li { overflow-wrap: anywhere; }
.map-stored { font-size: 11px; }
.map-files { list-style: none; margin: var(--s2) 0; padding: 0;
             display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--s2); }
.map-file { display: flex; flex-wrap: wrap; gap: var(--s2); align-items: baseline; }
.map-fname { color: var(--ink); overflow-wrap: anywhere; }
.map-fmeta { color: var(--ink-3); font-size: 11px; }
.map-fbad { color: var(--danger); font-size: 11px; overflow-wrap: anywhere; }
.map-fpicks { display: flex; flex-wrap: wrap; gap: var(--s2); margin: var(--s2) 0; }
.map-fpick { font-size: 11px; padding: 2px 8px; border: 1px solid var(--line-2);
             border-radius: var(--r-pill); background: transparent; color: var(--ink-2);
             cursor: pointer; }
.map-fpick.on { background: var(--accent-soft); border-color: var(--accent-line);
                color: var(--accent); }
.map-row { display: flex; flex-wrap: wrap; gap: var(--s2); margin-top: var(--s2); }

@media (max-width: 720px) {
  /* The heading line carries a label, the field's own key and a state chip; at 390px they stack
     rather than squeeze. The action row wraps for the same reason — three buttons on one line at
     phone width is a row of 60px targets. */
  .map-head { display: block; }
  .map-head .map-key { display: block; }
  .map-row .btn { flex: 1 1 auto; }
  .map-line { display: block; }
  .map-k { display: block; min-width: 0; }
}

/* ============================================ the affidavit of service (E4.2)

   The panel is a document-shaped surface: a grid of fields, each with its value, its source, the
   machine's own reading beside a correction, and a stated reason when it is blank. The long value
   is a paragraph, so every box here carries `overflow-wrap: anywhere` — a field row whose value is
   a 200-character address sets a min-content floor and widens the whole phone page, which has
   happened twice on this console (the destination-requirements card at 457px inside a 390px
   viewport). The grid track is `minmax(0, 1fr)` for the same reason: `1fr` alone cannot shrink
   below its content's min-content width. */

.aff-panel { margin: var(--s4) 0 0; }
.aff-panel h3 { margin: 0 0 var(--s2); font-size: 11px; text-transform: uppercase;
                letter-spacing: 0.1em; color: var(--ink-3); font-weight: 680; }
.aff-head { margin: var(--s5) 0 var(--s2); font-size: 12px; text-transform: uppercase;
            letter-spacing: 0.07em; color: var(--ink-3); font-weight: 680; }
.aff-quiet { color: var(--ink-3); }
.aff-bad { color: var(--danger); }
.aff-bad-card { border-color: var(--danger, var(--line)); }

.aff-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
            gap: var(--s3); }
.aff-field { border: 1px solid var(--line); border-radius: var(--r-md); background: var(--raised);
             padding: var(--s3) var(--s4); min-width: 0; overflow-wrap: anywhere; }
.aff-field.aff-blank { border-style: dashed; }
.aff-field.aff-edited { border-color: var(--accent-line); }
.aff-field-head { display: flex; flex-wrap: wrap; gap: var(--s2); align-items: baseline; }
.aff-field-label { font-size: 12px; text-transform: uppercase; letter-spacing: 0.07em;
                   color: var(--ink-3); font-weight: 680; }
.aff-flag { font-size: 10px; padding: 1px 7px; border-radius: var(--r-pill);
            background: var(--accent-soft); border: 1px solid var(--accent-line);
            color: var(--accent); white-space: nowrap; }
.aff-flag-quiet { background: transparent; border-color: var(--line-2); color: var(--ink-3); }
.aff-value { margin: 6px 0 0; font-size: 13px; color: var(--ink); white-space: pre-wrap;
             overflow-wrap: anywhere; }
.aff-system { color: var(--ink-3); font-size: 11px; margin: 4px 0 0; overflow-wrap: anywhere; }
.aff-src { display: flex; flex-wrap: wrap; gap: var(--s2); align-items: baseline;
           margin: var(--s2) 0 0; }
.aff-src .pset-note { margin: 0; overflow-wrap: anywhere; }
.aff-switch { margin: var(--s2) 0 0; overflow-wrap: anywhere; }
.aff-switch[data-notary="off"] { color: var(--ink-3); }
.aff-switch[data-notary="on"] { color: var(--ink); }
.aff-edit { display: flex; flex-wrap: wrap; gap: var(--s2); margin: var(--s2) 0 0; }
.aff-edit .aff-input, .aff-edit .aff-instruct { flex: 1 1 100%; min-width: 0; }
.aff-actions { margin: var(--s2) 0 0; }
.aff-row { display: flex; flex-wrap: wrap; gap: var(--s3); align-items: baseline;
           margin: var(--s3) 0 0; }

/* The composed page, exactly as the module wrote it — a monospace block so the office's own line
   breaks are the ones the document has. */
.aff-document { margin: var(--s2) 0 0; padding: var(--s4); border: 1px solid var(--line);
                border-radius: var(--r-md); background: var(--raised); color: var(--ink);
                font-family: var(--mono); font-size: 12px; line-height: 1.7;
                white-space: pre-wrap; overflow-wrap: anywhere; }

.aff-gaps, .aff-blanks .notes { margin: var(--s2) 0 0; padding-left: 1.1em; list-style: disc; }
.aff-gap { font-size: 12px; color: var(--ink-2); margin: 0 0 6px; overflow-wrap: anywhere; }
.aff-gap b { color: var(--ink); }
.aff-gap-quiet { color: var(--ink-3); }
.aff-gap-closed { color: var(--ink-3); }
.aff-closed { margin: var(--s3) 0 0; }
.aff-closed summary { cursor: pointer; font-size: 12px; color: var(--ink-2);
                      list-style: none; }
.aff-closed summary::-webkit-details-marker { display: none; }
.aff-closed summary .caret { display: inline-block; margin-left: 6px; color: var(--ink-3);
                             transition: transform 0.15s; }
.aff-closed[open] summary .caret { transform: rotate(90deg); }

/* A FIELD THE DOCUMENTS DISAGREE ABOUT (R3, E4.4). The flag is the point: without it a contested
   field renders exactly like one the documents agree about, and the review screen is the one place
   a person is meant to settle it. The other readings are OPEN by default — a <details> that starts
   closed hides exactly the text the operator has to read, which is the trap the mapping panel
   already carries a note about. */
/* `--warn-bg` is NOT a token either: the literal was a DARK-tinted translucent brown, so the
   disagreed-field flag was tinted the same way in the light theme. `--warn-soft` is the themed
   warning surface (light `#fbf0dc`, dark `#2a2113`) and `--warn-line` is already used beside it. */
.aff-flag-flag { background: var(--warn-soft, rgba(180, 120, 20, 0.14));
                 border-color: var(--warn-line, var(--accent-line)); color: var(--ink); }
.aff-field.aff-contested { border-color: var(--accent-line); }
.aff-contest { margin: var(--s2) 0 0; }
.aff-contest summary { cursor: pointer; font-size: 11px; color: var(--ink-2); list-style: none; }
.aff-contest summary::-webkit-details-marker { display: none; }
.aff-contest summary .caret { display: inline-block; margin-left: 6px; color: var(--ink-3);
                              transition: transform 0.15s; }
.aff-contest[open] summary .caret { transform: rotate(90deg); }
.aff-others { margin: var(--s2) 0 0; padding-left: 0; list-style: none; }
.aff-other { font-size: 12px; color: var(--ink-2); margin: 0 0 var(--s2);
             padding-left: var(--s3); border-left: 2px solid var(--line-2);
             overflow-wrap: anywhere; }
.aff-other .cand-val { color: var(--ink); }
.aff-other-src { display: flex; flex-wrap: wrap; gap: var(--s2); align-items: baseline;
                 margin-top: 3px; }
/* AN EDIT THE FORMAT NO LONGER OPENS (R4/R8). It is drawn, not swallowed: the value above is the
   reading, and this says why the operator's correction is not the one in force. */
.aff-stale { margin: var(--s2) 0 0; color: var(--danger, var(--ink-2));
             overflow-wrap: anywhere; }

/* EVERY CHANGE TO THE AFFIDAVIT (E4.10). The log is drawn open, one card per record, in creation
   order — the order IS the story, so the cards are listed rather than gridded, and each one wraps
   its own value and time (a long instruction or a long value must not widen the panel; measured on
   the mapping panel, where a long address stretched the page). */
.aff-log-panel { display: flex; flex-direction: column; }
.aff-log-panel .aff-changes { display: flex; flex-direction: column; gap: var(--s2); }
.aff-log-panel .aff-change { padding: var(--s2) var(--s3); }
.aff-log-panel .aff-change h4 { margin: 0 0 2px; font-size: 11px; }
.aff-log-panel .aff-change h4 .mono { display: inline; margin-left: var(--s2); }
.aff-log-panel .aff-change p { margin: 0; overflow-wrap: anywhere; }
/* `.mono` is `display: block` GLOBALLY (a value's source line gets its own row), so a timestamp
   wrapped in it inside a sentence is torn onto a line of its own. That is the defect measured on
   the desk capture while every field-level assertion passed. A date is a value inside this
   sentence, not a source line: unblock it and let it wrap like the words around it.
   (NOTE: `.pset-note` is NOT `white-space: pre-wrap` — it computes `normal` on the rendered panel,
   so a template literal broken across source lines COLLAPSES and does not reach the screen. The
   classes that DO preserve a source wrap are `.req-card p`, `.reason`, `.act-body`, `.aff-value`.
   Check `getComputedStyle` rather than a class's own rule: `white-space` is inherited.) */
.aff-log-panel .aff-change .aff-when { display: inline; font-family: var(--mono);
                                       font-size: 11px; }
.aff-log-panel .aff-change .aff-instruction,
.aff-log-panel .aff-change .aff-supersedes { margin-top: 2px; }
/* A RECORD THAT PREDATES THE LOG is marked, not hidden: it is a real gap in the attribution, and a
   reader who took the assembly's own `generated_by` for the change's would be told the wrong
   person made it (R1). */
.aff-log-panel .aff-change-unstated { border-left: 3px solid var(--warn-line); }
.aff-log-panel .aff-change .aff-unstated { margin-top: 2px; }

@media (max-width: 720px) {
  /* One column: a 390px phone cannot hold two value boxes side by side, and the value is the
     thing being read. The heading line stacks rather than squeezing the label against its chip. */
  .aff-grid { grid-template-columns: minmax(0, 1fr); }
  .aff-field-head { display: block; }
  .aff-field-head .aff-flag { display: inline-block; margin-top: 4px; }
  .aff-row .btn { flex: 1 1 auto; }
  .aff-document { padding: var(--s3); font-size: 11px; }
}
.map-noteclear { margin: var(--s1) 0 0; font-size: 11px; color: var(--ink-3);
                 overflow-wrap: anywhere; }
/* An action that belongs to the blank line, grouped in its own element so the source template's
   indentation cannot bleed into the rendered line — `.mono` is `display: block` (a global rule for
   a value's source line), so the whitespace between two interpolations rendered as a stray indent
   in front of the button. */
.map-actions { display: inline-block; margin-left: var(--s2); }
@media (max-width: 720px) {
  .map-actions { display: block; margin: var(--s2) 0 0; }
}

/* THE EXHIBIT PAGE ON THE AFFIDAVIT (E4.6). Appended at the END, after every phone override, for
   the reason the sibling `.att-exhibit` block learned the hard way: a rule added earlier is
   silently overridden by a later media query, and a phone layout that "did not take" reads as a
   broken grid rather than a stylesheet order problem.

   The grid is `minmax(0, 1fr)` on the text column, not `1fr`: a `1fr` track has an automatic
   minimum (its content) and a long attempt id or file path then widens the page instead of
   wrapping inside its column. */
.aff-exhibit { margin-top: var(--s3); }
.aff-exhibit-card { min-width: 0; }
.aff-exhibit-grid { display: grid; grid-template-columns: minmax(0, 170px) minmax(0, 1fr);
                    gap: var(--s3); align-items: start; min-width: 0; }
.aff-exhibit-grid img { display: block; max-width: 100%; height: auto; border-radius: var(--r-sm);
                        border: 1px solid var(--line-2); }
/* The photo's bytes are read at read time, so a photo whose file is gone is drawn as its own
   state rather than as a broken image glyph. */
.aff-exhibit-missing { padding: var(--s3); border: 1px dashed var(--warn-line, var(--line-2));
                       border-radius: var(--r-sm); font-size: 12px; color: var(--warn, var(--ink-2)); }
/* The label is the point of the block: the ATTEMPT's own date and time. It is given the accent so
   it cannot be mistaken for the assembled-document date printed elsewhere on the record. */
.aff-exhibit-label { font-size: 13px; }
.aff-exhibit-grid .att-quiet { font-size: 11px; overflow-wrap: anywhere; }
.aff-exhibit-cap { margin: var(--s2) 0 0; font-size: 12px; color: var(--ink-2);
                   overflow-wrap: anywhere; }
.aff-exhibit-why { margin: var(--s1) 0 0; font-size: 12px; }
/* NOT SETTLED (two served attempts, or several unmarked photos): a flag, not an error to clear —
   the same treatment `.att-not-settled` gets on the attempt panel. */
.aff-exhibit-settled-no { border-left: 3px solid var(--warn-line, var(--accent-line)); }

@media (max-width: 720px) {
  /* one column: a 170px thumbnail beside a label at 390px leaves the label ~180px, and the
     attempt id and the file path both break mid-word. */
  .aff-exhibit-grid { grid-template-columns: minmax(0, 1fr); }
}

/* A not-settled candidate row (E4.6). `.mono` is `display: block` globally — that is right for a
   value's source line under a field and wrong inside a sentence, where it breaks the row and hangs
   the em-dash at the start of the next line. The row is one line by construction; this only says so. */
.aff-exhibit-cand { overflow-wrap: anywhere; }
.aff-exhibit-cand .mono { display: inline; }

/* APPROVE AND SAVE, OR REJECT (E4.11). The panel is where the decision is taken, and the task a
   rejection leaves open has to be readable at 390px: every row wraps inside its own column, and no
   row relies on a `1fr` track (whose automatic minimum is its content, and a long field label then
   widens the page rather than wrapping — the trap the exhibit grid above already records). */
.rev-panel { margin: var(--s4) 0 0; }
.rev-actions { display: flex; flex-wrap: wrap; gap: var(--s2); margin: var(--s3) 0 0; }
.rev-reject { margin: var(--s3) 0 0; padding: var(--s3); border: 1px solid var(--line-2);
              border-radius: var(--r-sm); }
.rev-reject .rev-row { display: flex; flex-wrap: wrap; gap: var(--s2); align-items: center;
                       margin: 0 0 var(--s2); }
.rev-reject .rev-row label { font-size: 11px; color: var(--ink-3); }
.rev-reject .rev-row select,
.rev-reject .rev-row input { min-width: 0; flex: 1 1 200px; }
.rev-pending { margin: var(--s2) 0; padding-left: 0; list-style: none; }
.rev-pending-item { font-size: 12px; color: var(--ink-2); margin: 0 0 4px;
                    overflow-wrap: anywhere; }
/* `.notes` IS A PROSE LIST AND CARRIES A MARKER. `.notes li::before` draws an absolutely-positioned
   chevron at `left: 0, top: 13px`, and it is more specific than any single-class rule here — so
   borrowing the class (`class="notes rev-task"`) drew the app's chevron AND this list's own disc on
   every row, and `content: none` could not turn it off. These two lists therefore do NOT carry the
   class: they are their own list, with no marker at all, the same as `.rev-pending` above them.
   Measured on the running page with `getComputedStyle(li, '::before')`, not inferred — the probe in
   `tools/verify_review_decide.py` reads exactly this and its negative control adds a marker back. */
.rev-items, .rev-task { margin: var(--s2) 0 0; padding: 0; list-style: none; }
.rev-item, .rev-task-item { margin: 0 0 8px; padding: 0; font-size: 12px;
                            color: var(--ink-2); overflow-wrap: anywhere; }
.rev-item b, .rev-task-item b { color: var(--ink); }
/* `.mono` is `display: block` globally (a value's source line gets its own row), so a field name
   wrapped in it INSIDE a row is torn onto a line of its own — the same defect the mapping and
   affidavit panels each carry a note about. */
.rev-item .mono, .rev-task-how .mono { display: inline; }
.rev-task-how { display: block; margin-top: 2px; font-size: 11px; color: var(--ink-3);
                overflow-wrap: anywhere; }
/* The "on the affidavit now" value gets its own line under "what to do", so a long value cannot
   read as part of the instruction. */
.rev-task-how span { display: block; }
.rev-run-card, .rev-open { border-left: 3px solid var(--warn-line, var(--accent-line)); }
.rev-open h4 { margin: 0 0 var(--s2); }
/* The open task is the thing the operator came back to the job to read: it is not a quiet aside. */
.rev-open .rev-task-item { color: var(--ink-2); }
.rev-stale { margin: var(--s2) 0 0; color: var(--danger, var(--ink-2));
             overflow-wrap: anywhere; }
/* A recorded decision reads as a stamp, the same shape the approval note uses — one sentence in
   ONE string, because `.pset-note` is `pre-wrap` and a template literal broken across source lines
   renders the source's own wrap and hanging indent as content. */
.rev-panel #rev-decision, .rev-panel [id$="-decision"] { overflow-wrap: anywhere; }

/* THE INVOICE (E5). An invoice raised against a completed job, derived from it, approved then sent.

   Two layout facts drive every rule here, and both are this console's own measured traps:

     * `.mono` is `display: block` GLOBALLY, so a value's source line gets its own row under a field.
       Inside an invoice LINE — "1 × Service of process — AUD 385.00" — that would tear the line into
       four stacked fragments with the money floating away from what it is for. Every `.mono` inside
       `.inv-line` and `.inv-money` rows is therefore put back to `display: inline`.
     * a `1fr` track's automatic minimum is its CONTENT, so a long document path or a long
       description widens the panel (and the page) rather than wrapping. Rows are flex-wrap with
       `min-width: 0`, and every cell wraps anywhere. */
.inv-panel { margin: var(--s4) 0 0; }
.inv-panel .inv-meaning { overflow-wrap: anywhere; }
.inv-lines { margin: var(--s2) 0 0; padding: 0; list-style: none; }
.inv-line { display: flex; flex-wrap: wrap; gap: var(--s2); align-items: baseline;
            margin: 0 0 6px; padding: 0; font-size: 12px; color: var(--ink-2);
            overflow-wrap: anywhere; }
.inv-line > * { min-width: 0; }
/* the money is the point of the row: it is the ink, not a quiet aside */
.inv-line .inv-what { flex: 1 1 200px; color: var(--ink); }
.inv-line .inv-money { color: var(--ink); font-weight: 600; white-space: nowrap; }
.inv-line .inv-qty { color: var(--ink-3); }
.inv-line .inv-src { flex: 1 1 100%; font-size: 11px; color: var(--ink-3); }
/* `.mono` is block by default — inside these rows it must not be. */
.inv-line .mono, .inv-panel .inv-approved .mono, .inv-panel .inv-sending .mono { display: inline; }
.inv-total { margin: var(--s2) 0 0; font-size: 13px; overflow-wrap: anywhere; }
.inv-total b { color: var(--ink); }
.inv-approved, .inv-sending, .inv-void, .inv-drift li { overflow-wrap: anywhere; }
/* A delivery on an EARLIER version is the fact a person has to read before pressing anything: it
   gets the same raised edge as a voided approval for the same reason — it is not a quiet aside. */
.inv-prev, .inv-redel { border-left: 3px solid var(--warn-line, var(--accent-line)); padding-left: var(--s2); }
/* A voided approval is the R6 consequence and it is NOT a quiet aside: it is the reason the invoice
   is in front of a person again. */
.inv-void { border-left: 3px solid var(--warn-line, var(--accent-line)); padding-left: var(--s2); }
.inv-sent-chip { color: var(--ink); }
.inv-acts { display: flex; flex-wrap: wrap; gap: var(--s2); margin: var(--s3) 0 0; }
/* The author, the price and the instruction are FIELDS beside the controls, not dialogs: an
   operator has to be able to see what the act will carry while they fill it in, and a modal blocks
   the page (measured — a headless run hung on it). The author field is required by the module and
   is given the width to say so. */
.inv-acts .inv-by, .inv-acts .inv-amount, .inv-acts .inv-instruction { min-width: 0; font: inherit; }
.inv-acts .inv-by { flex: 1 1 220px; }
.inv-acts .inv-price { flex: 1 1 240px; min-width: 0; }
.inv-acts .inv-amount { flex: 1 1 150px; }
.inv-acts .inv-instruction { flex: 2 1 260px; }
.inv-panel .st-gaps li, .inv-panel .inv-gaps li { overflow-wrap: anywhere; }

@media (max-width: 720px) {
  /* At 390px the row wraps. The FIRST version of this stacked every cell and the quantity was left
     alone on its own line — "1 ×" with the description under it — which reads as an orphan. MEASURED
     by reading the capture. On a phone the line is its OWN GRID instead: the description takes the
     free width, the quantity stops wrapping away from it, and the money sits on the right of the
     first row where it can be read against what it is for. `minmax(0, …)` because a grid track's
     automatic minimum is its CONTENT and a long description would otherwise widen the page. */
  .inv-line { display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
              align-items: baseline; column-gap: var(--s2); row-gap: 2px; }
  .inv-line .inv-qty { grid-column: 1; }
  .inv-line .inv-what { grid-column: 2; min-width: 0; }
  .inv-line .inv-money { grid-column: 3; white-space: nowrap; }
  .inv-line .inv-src { grid-column: 1 / -1; }
  .inv-acts { display: block; }
  .inv-acts .btn { display: block; width: 100%; margin: 0 0 var(--s2); }
  /* A `<select>`'s automatic minimum is its CONTENT, so the price picker's longest option
     ("SERVICE_INTL — Service of process — international — AUD 620.50") widened the page to 585px on
     a 390px phone and the panel scrolled sideways. MEASURED, not inferred: the width audit above
     reported `.inv-price` at right=585. `width: 100%` plus `max-width: 100%` is what bounds it —
     `min-width: 0` alone does not, because the select's own intrinsic width wins against a flex
     basis it is no longer participating in under `display: block`. */
  .inv-acts .inv-by, .inv-acts .inv-price, .inv-acts .inv-amount,
  .inv-acts .inv-instruction { display: block; width: 100%; max-width: 100%; min-width: 0;
                               box-sizing: border-box; margin: 0 0 var(--s2); }
}

/* THE HELD SIGNATURE (E4.5). "Held with the process server's permission, applied on the operator's
   instruction, or the affidavit is sent for signature instead."

   The same two measured traps as the invoice panel govern every rule here:

     * `.mono` is `display: block` GLOBALLY, so a `<span class="mono">` inside a `set-line` — the
       record's file name, a when, the appearance — would be torn onto its own row and read as a
       fragment rather than as part of the sentence it belongs to. Every `.mono` inside a `.sig-*`
       line is put back to `display: inline`.
     * a `1fr` track's automatic minimum is its CONTENT, so a long permission line or a long
       instruction widens the panel (and the page) rather than wrapping. The two-field row is
       flex-wrap with `min-width: 0`, and every line wraps anywhere. */
.sig-panel { margin: var(--s4) 0 0; }
.sig-panel .sig-state { font-weight: 600; letter-spacing: .04em; }
.sig-held, .sig-applied, .sig-none, .sig-acts, .sig-act, .sig-withdraw, .sig-apply-act {
  overflow-wrap: anywhere;
}
.sig-held { border-left: 3px solid var(--accent-line); }
/* An application that no longer stands over this document is the R6 consequence, and it is NOT a
   quiet aside: it is the reason the affidavit in front of the operator is not the signed one. */
.sig-stale { border-left: 3px solid var(--warn-line, var(--accent-line)); }
.sig-stale-why { color: var(--danger, var(--ink-2)); }
.sig-held .set-line, .sig-applied .set-line { margin: 2px 0; font-size: 12px; color: var(--ink-2); }
.sig-k { color: var(--ink-3); font-size: 11px; }
.sig-words { font-style: italic; }
.sig-label { display: block; font-size: 11px; color: var(--ink-3); margin: var(--s2) 0 2px; }
.sig-server, .sig-panel input[type="text"], .sig-panel select {
  display: block; width: 100%; min-width: 0; max-width: 100%;
}
/* Two fields side by side on the desk; stacked at the phone width below, where a 200px basis would
   overflow a 390px screen once the labels and the panel padding are counted. */
.sig-row { display: flex; flex-wrap: wrap; gap: var(--s2); }
.sig-row > div { flex: 1 1 200px; min-width: 0; }
.sig-act .btn, .sig-withdraw .btn { margin: var(--s3) 0 0; }
/* `.mono` is block by default — inside these lines it must not be. Measured the same way the
   invoice and mapping panels were: a `set-line` whose value was torn onto its own row reads as a
   fragment and the sentence loses its meaning. */
.sig-held .mono, .sig-applied .mono, .sig-panel .set-line .mono, .sig-panel .pset-note .mono {
  display: inline;
}
/* The trail's own list: a record's file name is one token, and it gets its own row. */
.sig-trail { margin: var(--s3) 0 0; }
.sig-trail summary { cursor: pointer; font-size: 12px; color: var(--ink-2); }
.sig-bad li { color: var(--danger, var(--ink-2)); }

@media (max-width: 720px) {
  .sig-row { display: block; }
  .sig-row > div { margin: 0 0 var(--s2); }
  .sig-act .btn, .sig-withdraw .btn { display: block; width: 100%; }
}

/* MY SERVERS' OWN HEADER, at the widths where its own labelled text tips a rule every overlay
   shares. `#servers-overlay` puts a title, `#srv-scope` (the caller label, as long as a client
   id) and TWO controls into `.form-overlay-head`, which is `display:flex; flex-wrap:nowrap` —
   so once the content is wider than the bar it goes through `overflow: visible`, the document
   reports NO overflow at all, and the Close control is simply pushed past the right edge. That
   is the one control that always has to be reachable: measured at 360px with a realistic client
   id, Close was 0/66px visible and a real click at its visible centre did not close the sheet.
   Scoped to `#servers-overlay`: the sibling overlays that share `.form-overlay-head` (the USM94
   form, Notifications) lay out correctly and this card's text is what tips the shared rule, so
   this card carries its own fix rather than re-laying out every overlay. */
@media (max-width: 720px) {
  #servers-overlay .form-overlay-head { flex-wrap: wrap; }
  /* the title takes the first row and may shrink; `.fo-title` is `display:flex`, so the label
     inside needs `min-width: 0` before it can truncate at all */
  #servers-overlay .fo-title { flex: 1 1 auto; min-width: 0; }
  #servers-overlay .fo-title .mono {
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
  }
  /* the controls never shrink and never wrap, and they take their own row when the title has
     used the first one — so Close is on screen at every width, including 320px (their 40px
     touch height is already set by the overlay block above; nothing to repeat here) */
  #servers-overlay .fo-actions { flex: none; }
}

/* THE PAYMENT STATUS (E5.2). Its own block, appended rather than folded into `.inv-*`: the two
   panels sit next to each other on an informal card and a shared rule would make a change to one
   move the other. `max-width: 100%` on the select is load-bearing — a `<select>`'s automatic
   minimum width is its longest OPTION, and the invoice panel measured a 390px phone page widened to
   585px by exactly that. This picker's options are status labels, so it would do the same. */
.pay-panel { margin: var(--s4) 0 0; }
.pay-panel .pay-meaning, .pay-panel .pay-src, .pay-panel .pay-instr, .pay-panel .pay-when,
.pay-panel .pay-amt, .pay-panel .pay-ref, .pay-panel .pay-prov, .pay-panel .pay-approval,
.pay-panel .pay-gaps li { overflow-wrap: anywhere; }
.pay-acts { display: flex; flex-wrap: wrap; gap: var(--s2); margin: var(--s3) 0 0; }
.pay-acts .pay-status { width: 100%; max-width: 100%; min-width: 0; font: inherit; flex: 1 1 100%; }
.pay-acts .pay-by { flex: 1 1 220px; min-width: 0; font: inherit; }
.pay-acts .pay-paid-on { flex: 1 1 200px; min-width: 0; font: inherit; }
.pay-acts .pay-amount { flex: 0 1 140px; min-width: 0; font: inherit; }
.pay-acts .pay-currency { flex: 0 1 90px; min-width: 0; font: inherit; }
.pay-acts .pay-reference { flex: 1 1 180px; min-width: 0; font: inherit; }
.pay-acts .pay-instruction { flex: 2 1 260px; min-width: 0; font: inherit; }
.pay-history .st-move, .pay-vocab .notes li { overflow-wrap: anywhere; }

/* ==================================================================== THE NEW JOB FORM (intake)

   The screen the office types the job into. It is the ONLY producer of values on the informal
   workflow — no model runs there at all — so it is laid out for filling in rather than for
   reading: a two-column grid on the desk, one column on a phone. `min-width: 0` on the controls
   is load-bearing: a grid item's automatic minimum is its content, and a long option or a long
   case name otherwise widens the whole page instead of scrolling inside its own box. */
.intake-panel { margin: var(--s4) 0 0; }
.intake-groups { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s3) var(--s4); }
.intake-group { min-width: 0; }
.intake-group-head { margin: 0 0 var(--s1); font-size: 0.95em; }
.intake-caption { margin: 0 0 var(--s2); }
.intake-field { margin: 0 0 var(--s2); min-width: 0; }
.intake-label { display: block; font-size: 0.85em; color: var(--ink-3); margin: 0 0 2px; }
.intake-req { color: var(--warn, #a36b00); font-size: 0.9em; }
.intake-control { min-width: 0; }
.intake-input { width: 100%; max-width: 100%; min-width: 0; font: inherit;
                padding: 3px 6px; border: 1px solid var(--line-2); border-radius: 3px;
                background: var(--raised); color: var(--ink); }
.intake-input:focus-visible { outline: 2px solid var(--accent, #2f6f4f); outline-offset: 1px; }
.intake-gap { margin: 2px 0 0; font-size: 0.8em; overflow-wrap: anywhere; }
.intake-blank .intake-label { color: var(--ink-4); }
.intake-actions { display: flex; flex-wrap: wrap; gap: var(--s2); align-items: center;
                  margin: var(--s3) 0 0; }
.intake-quiet { flex: 1 1 240px; min-width: 0; }
.intake-result { margin: var(--s2) 0 0; overflow-wrap: anywhere; }
.intake-result-bad { color: var(--danger, #a33); }
@media (max-width: 760px) { .intake-groups { grid-template-columns: 1fr; } }

/* ============================================================ THE AFFIDAVIT DOCUMENT (the page)

   The document itself, as a list of parts. Each part is a row with its value, the source it
   travelled with, and a control that opens an editor in place — "like MS Word" in the sense that
   matters here: you change the words where they are, and the change is kept against that part.
   The value is set in the serif face and at document size, because it is the wording of a sworn
   page and reading it as UI chrome is what makes a mistake easy to miss. */
.affdoc-panel { margin: var(--s4) 0 0; }
.affdoc-page { border: 1px solid var(--line-2); border-radius: 3px; padding: var(--s3);
               background: var(--raised); }
.affdoc-run { padding: var(--s2) 0; border-bottom: 1px dashed var(--line); min-width: 0; }
.affdoc-run:last-child { border-bottom: 0; }
.affdoc-run-head { display: flex; flex-wrap: wrap; gap: var(--s2); align-items: baseline; }
.affdoc-run-label { font-size: 0.82em; color: var(--ink-3); text-transform: uppercase;
                    letter-spacing: .04em; }
.affdoc-when { margin: 0; font-size: 0.78em; }
.affdoc-value { font-family: 'Times New Roman', 'Liberation Serif', Georgia, serif;
                font-size: 1.02em; line-height: 1.35; margin: 3px 0 0;
                white-space: pre-wrap; overflow-wrap: anywhere; }
.affdoc-blank .affdoc-value { font-family: inherit; font-size: 0.9em; }
.affdoc-edited { border-left: 3px solid var(--accent, #2f6f4f); padding-left: var(--s2); }
.affdoc-system { margin: 3px 0 0; font-size: 0.8em; overflow-wrap: anywhere; }
.affdoc-actions { display: flex; flex-wrap: wrap; gap: var(--s2); margin: var(--s1) 0 0; }
.affdoc-editbox { display: flex; flex-wrap: wrap; gap: var(--s2); margin: var(--s2) 0 0;
                  min-width: 0; }
.affdoc-input { flex: 1 1 100%; width: 100%; max-width: 100%; min-width: 0; font: inherit;
                font-family: 'Times New Roman', 'Liberation Serif', Georgia, serif;
                padding: 4px 6px; border: 1px solid var(--line-2); border-radius: 3px;
                background: var(--bg); color: var(--ink); }
.affdoc-instruct { flex: 2 1 260px; min-width: 0; font: inherit; padding: 3px 6px;
                   border: 1px solid var(--line-2); border-radius: 3px; background: var(--bg);
                   color: var(--ink); }
.affdoc-notary { margin: var(--s2) 0; }
.affdoc-notary.off { color: var(--ink-3); }
.affdoc-problems { margin: var(--s2) 0; }
.affdoc-problems .notes { margin: 0; padding-left: 1.1em; }
.affdoc-problems .notes li { overflow-wrap: anywhere; }
.affdoc-actions-bar { display: flex; flex-wrap: wrap; gap: var(--s2); align-items: center;
                      margin: var(--s3) 0 0; }
.affdoc-quiet { flex: 1 1 260px; min-width: 0; }
.affdoc-result { margin: var(--s2) 0 0; overflow-wrap: anywhere; }
/* A collapsed group is a line; an open one is the work. `summary` is styled to read as a heading
   rather than as a disclosure widget, because the office should see their progress before they
   see anything else. */
.intake-group > summary { cursor: pointer; list-style: none; display: flex; gap: var(--s2);
                         align-items: baseline; flex-wrap: wrap; }
.intake-group > summary::-webkit-details-marker { display: none; }
.intake-group > summary::before { content: "▸"; color: var(--ink-4); font-size: 0.9em; }
.intake-group[open] > summary::before { content: "▾"; }
.intake-group-title { font-weight: 600; }
.intake-group-todo { color: var(--warn, #a36b00); font-size: 0.78em; }
/* The rendered page, as an IMAGE. PROJECT.md §9: "Rendered PNG pages, not an embedded PDF, for
   the form preview — an embedded viewer is a blank box in headless capture." The same rule
   applies to the affidavit, and for a second reason: the office has to SEE the page a process
   server will print, because a field-level check passes happily while the rendered page clips a
   long value. */
.affdoc-preview { margin: var(--s3) 0; }
.affdoc-preview-head { margin: 0 0 var(--s2); font-size: 0.95em; }
.affdoc-page-img { display: block; width: 100%; max-width: 760px; height: auto;
                   border: 1px solid var(--line-2); border-radius: 2px; background: #fff;
                   margin: 0 0 var(--s2); }
/* A value the page will PRINT but nobody has confirmed. Its own colour, because the whole point is
   that it is neither a blank (nothing to print) nor a settled value — and a reason shown only when
   the value is MISSING is a reason nobody sees on the run that needs it most. */
.affdoc-unconfirmed { border-left: 3px solid var(--warn, #a36b00); padding-left: var(--s2);
                      color: var(--warn, #a36b00); }

/* ============================================================== THE INFORMAL FLOW

   Two screens and a job page, replacing a five-step rail and sixteen stacked panels. The office's
   own words: "This is 1 page/step only, we do not need 1 to 5 steps in informal, remove all the
   steps we do."

   THE LAYOUT FOLLOWS THE SCREENS THE OFFICE SHOWED: a heading, a label column and a control
   column, and the three actions at the foot of the details screen.

   `min-width: 0` ON EVERY CONTROL IS LOAD-BEARING. A grid item's automatic minimum size is its
   CONTENT, not zero, so one long case number or a wide time-zone string otherwise widens the whole
   page instead of scrolling or wrapping inside its own box — the failure mode is a page that is
   fine in a test with short values and broken on the first real one. */
.inf-flow { display: flex; flex-direction: column; gap: var(--s3); }
.inf-tabs { display: flex; gap: var(--s3); flex-wrap: wrap; border-bottom: 1px solid var(--line);
            padding-bottom: var(--s2); }
.inf-tab { display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
           background: none; border: 1px solid transparent; border-bottom: 0; cursor: pointer;
           padding: var(--s2) var(--s3); font: inherit; color: var(--ink-3);
           border-radius: 3px 3px 0 0; }
.inf-tab:hover { color: var(--ink); }
.inf-tab.on { color: var(--ink); border-color: var(--line-2); background: var(--raised);
              font-weight: 600; }
.inf-tab-note { font-size: 0.72em; color: var(--ink-4); margin-top: 2px; }
.inf-card { min-width: 0; }
.inf-head { display: flex; align-items: baseline; gap: var(--s3); flex-wrap: wrap;
            margin: 0 0 var(--s2); }
.inf-head h3 { margin: 0; }
.inf-count { color: var(--ink-3); font-size: 0.82em; }
.inf-empty { color: var(--ink-4); font-style: italic; }
.inf-quiet { color: var(--ink-4); font-size: 0.8em; }
.inf-warn { color: var(--warn, #a36b00); }

/* ---- 1. the documents to be served ---- */
/* TWO SECTIONS, ONE PANEL (the operator, 2026-09-28): the documents being served, and the
   supporting documents that are read for the details and never sent. Each is its own block with its
   own rule above it, so the two are told apart at a glance — the served block is the incumbent panel
   unchanged, and the supporting block is marked as the second, withheld one. */
.inf-section { min-width: 0; }
.inf-section + .inf-section { margin-top: var(--s4, 1.4rem); padding-top: var(--s3);
                              border-top: 1px solid var(--line-1, var(--line-2)); }
.inf-section[data-inf-section="supporting"] .inf-head h3 { color: var(--ink-2, var(--ink)); }
.inf-docs { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column;
            gap: var(--s3); }
.inf-doc { border: 1px dashed var(--line-2); border-radius: 3px; padding: var(--s3); min-width: 0; }
.inf-doc-head { display: flex; gap: var(--s2); align-items: baseline; flex-wrap: wrap; }
.inf-doc-file { font-size: 0.82em; color: var(--ink-3); overflow-wrap: anywhere; }
.inf-label { display: block; font-size: 0.85em; color: var(--ink-3); margin: var(--s2) 0 2px; }
.inf-input { width: 100%; max-width: 100%; min-width: 0; font: inherit; padding: 4px 7px;
             border: 1px solid var(--line-2); border-radius: 3px; background: var(--raised);
             color: var(--ink); box-sizing: border-box; }
.inf-input:focus-visible { outline: 2px solid var(--accent, #2f6f4f); outline-offset: 1px; }
textarea.inf-input { resize: vertical; font-family: inherit; }
.inf-doc-acts { display: flex; gap: var(--s2); margin: var(--s2) 0 0; flex-wrap: wrap; }
.inf-acts, .inf-next { display: flex; gap: var(--s2); align-items: center; flex-wrap: wrap;
                       margin: var(--s3) 0 0; }
.inf-next { justify-content: flex-end; }
/* THE DROP TARGET IS BIG ENOUGH TO BE ONE.
 *
 * The client: "drag and drop works though so make the drag and drop area bigger". MEASURED BEFORE:
 * the zone was authored 12px padding around one 17.85px line and came out 48px on the glass — SHALLOWER
 * than the 28px "Add files" button beside it, so it read as a rule under the list rather than a place
 * to put something.
 *
 * 120px, authored; 132px measured at the 1.1 root zoom, full width across the content column — 2.7x
 * its old height. The size comes from the usage scene, not taste: these are 10-2,000 page PDFs dragged
 * out of a file manager that may be maximised over or behind the browser, so the pointer is already
 * moving fast when it crosses the panel and the drop commits on button-release; a shallow strip is
 * simply missed. It stays a band INSIDE the panel it belongs to, so it never pushes the attached-file
 * list off the screen, and it keeps the incumbent 3px radius and --s2/--s3/--s4 spacing — a bigger
 * target is not a new visual language.
 *
 * FOUR CUES MAKE IT OBVIOUSLY A DROP TARGET, all incumbent tokens:
 *   1. a full-width band with the same dashed --line-3 frame `.inf-doc` already uses for "a container
 *      you put something into";
 *   2. a --sunken fill, so the band separates from the --surface card instead of reading as a hairline
 *      rule (ink-3 on sunken measures 5.33:1 light / 6.49:1 dark, so the label passes AA);
 *   3. the flex centring puts the label in the middle of the band, so the WHOLE rectangle is the
 *      target and not just the words;
 *   4. the drag-over state is strengthened — the incumbent only flipped border-colour, and at 132px a
 *      colour-only change is easy to miss while the operator's hand is already moving. A solid border
 *      says "committed" at exactly the moment they need to know (accent on accent-soft measures
 *      7.24:1 light / 6.59:1 dark).
 *
 * The `var(--accent, #2f6f4f)` green fallback is dropped: --accent is always defined in :root, so the
 * fallback was dead code from before the token existed.
 */
.inf-drop { margin: var(--s2) 0 0; padding: var(--s3) var(--s4); border: 1px dashed var(--line-3);
            border-radius: 3px; text-align: center; color: var(--ink-3); font-size: 0.85em;
            background: var(--sunken);
            min-height: 120px; display: flex; align-items: center; justify-content: center;
            transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease),
                        color var(--dur) var(--ease), box-shadow var(--dur) var(--ease); }
.inf-drop.over { border-style: solid; border-color: var(--accent); background: var(--accent-soft);
                 color: var(--ink); box-shadow: var(--ring); }
/* ON A NARROW SCREEN THE TARGET GETS SHORTER (96px authored / ~106px measured): the panel is one
   column there and a 132px band would cost the attached-file list a fold for no gain, and a finger has
   a bigger contact point than a mouse, so it needs less depth rather than more. */
@media (max-width: 640px) {
  .inf-drop { min-height: 96px; }
}

/* ---- 2. the details ---- */
.inf-scriptbar { display: flex; gap: var(--s2); align-items: center; flex-wrap: wrap;
                 margin: var(--s2) 0; padding: var(--s2); border-radius: 3px;
                 background: var(--sunken, rgba(0,0,0,.03)); }
.inf-unread { margin: 0 0 var(--s2); padding-left: 1.1em; }
.inf-unread li { overflow-wrap: anywhere; }
.inf-groups { display: flex; flex-direction: column; gap: var(--s2); }
.inf-group { border: 1px solid var(--line); border-radius: 3px; padding: var(--s2) var(--s3);
             min-width: 0; }
.inf-group > summary { cursor: pointer; list-style: none; display: flex; gap: var(--s2);
                       align-items: baseline; flex-wrap: wrap; }
.inf-group > summary::-webkit-details-marker { display: none; }
.inf-group > summary::before { content: "\25B8"; color: var(--ink-4); font-size: 0.9em; }
.inf-group[open] > summary::before { content: "\25BE"; }
.inf-group-title { font-weight: 600; }
.inf-group-n { color: var(--ink-4); font-size: 0.78em; }
.inf-caption { margin: var(--s2) 0; }
.inf-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s2) var(--s4); min-width: 0; }
.inf-field { min-width: 0; }
.inf-control { min-width: 0; }
.inf-req { color: var(--warn, #a36b00); font-size: 0.9em; }
.inf-flag { font-size: 0.72em; text-transform: uppercase; letter-spacing: .04em;
            color: var(--accent, #2f6f4f); border: 1px solid currentColor; border-radius: 2px;
            padding: 0 3px; margin-left: 4px; }
.inf-script, .inf-gap { margin: 2px 0 0; font-size: 0.76em; overflow-wrap: anywhere;
                        color: var(--ink-3); }
.inf-contested { margin: 2px 0 0; font-size: 0.8em; color: var(--warn, #a36b00);
                 overflow-wrap: anywhere; }
.inf-blank .inf-label { color: var(--ink-4); }

/* ---- email & share: NOTHING IS TICKED UNTIL THE OPERATOR TICKS IT ---- */
.inf-share { margin: var(--s4) 0 0; padding: var(--s3); border: 1px solid var(--line-2);
             border-radius: 3px; min-width: 0; }
.inf-share h4 { margin: 0 0 var(--s2); }
.inf-recip-group { margin: var(--s2) 0; padding: 0; list-style: none; }
.inf-recip-group h4 { margin: var(--s2) 0 2px; font-size: 0.78em; letter-spacing: .05em;
                      color: var(--ink-3); }
.inf-recip-group ul { list-style: none; margin: 0; padding: 0; }
.inf-recip label { display: flex; gap: var(--s2); align-items: baseline; cursor: pointer;
                   padding: 2px 0; flex-wrap: wrap; }
.inf-recip-name { min-width: 0; overflow-wrap: anywhere; }
.inf-recip-mail { color: var(--ink-3); font-size: 0.8em; overflow-wrap: anywhere; }
.inf-recip-note { color: var(--ink-4); font-size: 0.76em; }
.inf-star { color: var(--warn, #a36b00); }
.inf-other { margin: var(--s2) 0; }

/* ---- the three actions ---- */
.inf-actions { display: flex; gap: var(--s2); align-items: center; flex-wrap: wrap;
               margin: var(--s4) 0 0; padding-top: var(--s3); border-top: 1px solid var(--line); }
.inf-actions .inf-quiet { flex: 1 1 240px; min-width: 0; }
.btn.go { background: var(--accent, #2f6f4f); color: #fff; border-color: var(--accent, #2f6f4f); }
.btn.go:hover:not(:disabled) { filter: brightness(1.08); }
.inf-result { margin: var(--s2) 0 0; overflow-wrap: anywhere; }
.inf-result.bad { color: var(--danger, #a33); }

/* ---- 3. the job page ---- */
.inf-cols { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s4); min-width: 0; }
.inf-col { min-width: 0; }
.inf-col h4 { margin: var(--s3) 0 var(--s2); }
.inf-col h4:first-child { margin-top: 0; }
.inf-kv { display: grid; grid-template-columns: minmax(90px, auto) 1fr; gap: 2px var(--s3);
          margin: 0; min-width: 0; }
.inf-kv dt { color: var(--ink-3); font-size: 0.82em; }
.inf-kv dd { margin: 0; overflow-wrap: anywhere; }
.inf-list { list-style: none; margin: 0; padding: 0; }
.inf-list li { padding: 3px 0; border-bottom: 1px dashed var(--line); overflow-wrap: anywhere; }
.inf-attempt { display: flex; gap: var(--s2); flex-wrap: wrap; align-items: baseline; }
.inf-attempt[data-ok="served"] { border-left: 3px solid var(--accent, #2f6f4f);
                                 padding-left: var(--s2); }
.inf-attempt-acts { display: inline-flex; gap: var(--s1, 6px); margin-left: auto; }
/* AN UNAPPROVED DELIVERY IS GREYED OUT AND SAYS SO (Kazim, 2026-09-27). It stays on the list — it
   is recorded — but it is dimmed and carries its own state, so it is never read as a service that
   happened. Checked by `verify_informal_approval_gate.py`, which reads the computed opacity and the
   badge's text, not the comment. */
.inf-attempt-unapproved { opacity: .66; }
.inf-attempt-unapproved[data-ok="served"] { border-left: 3px solid var(--ink-4);
                                             padding-left: var(--s2); }
.inf-unapproved-flag { background: var(--ink-4); color: var(--paper, #fff); }
/* WHO RECORDED IT, ON THE ATTEMPT'S OWN ROW (Kazim, 2026-09-27). It sits on the row for EVERY
   delivery that stands — approved or not — so the attribution is not a property of the approval
   state. Dimmed to the quiet ink: it is a fact about the record rather than the row's headline, and
   it must not compete with the outcome text it follows. `overflow-wrap` because a typed name can be
   long and the row is a flex line. */
.inf-recorded-by { color: var(--ink-4); overflow-wrap: anywhere; }
.att-card-unapproved { border-left: 3px solid var(--ink-4); }
.att-approval { margin: var(--s2) 0 0; font-size: 12px; color: var(--ink-2); display: flex;
                flex-wrap: wrap; gap: 6px; align-items: baseline; }
.inf-col-acts { display: flex; gap: var(--s2); margin: var(--s2) 0 var(--s3); flex-wrap: wrap; }
/* THE MACHINE'S ACT BUTTONS live beside the job header's status line now; the old "Where this job
   stands" footer block is gone at the client's word (24 Sep 2026). */
.inf-head-acts { margin: var(--s2) 0 0; }
.inf-elsewhere li, .inf-shut li { overflow-wrap: anywhere; }

/* ---- 5. the affidavit: a DOCUMENT, not a form ----
   WORKSTREAM B: `.aff-toolbar`'s box treatment moved to the editor chrome block (~line 4300) —
   the picker row is now a borderless band between the acts toolbar and the format toolbar. This
   older rule keeps only the layout the blocks OUTSIDE the editor (none today) would share; the
   editor's own row is restyled by the later block, which wins by order. */
.aff-toolbar { display: flex; gap: var(--s3); align-items: center; flex-wrap: wrap; }
.aff-notary { display: flex; gap: var(--s2); align-items: center; cursor: pointer;
              font-weight: 600; }
.aff-spacer { flex: 1 1 auto; }
.aff-page { border: 1px solid var(--line-2); border-radius: 3px; padding: var(--s4);
            background: var(--raised); display: flex; flex-direction: column;
            gap: var(--s3); min-width: 0; }
.aff-run { min-width: 0; }
.aff-run .inf-label { margin: 0 0 2px; font-size: 0.8em; }
.aff-blank .inf-label { color: var(--ink-4); }
.aff-preview { margin: var(--s2) 0 0; }
.aff-preview img { max-width: 100%; border: 1px solid var(--line-2); border-radius: 3px; }
/* A RENDER THAT DID NOT HAPPEN MUST NOT DRAW AS A BROKEN-IMAGE GLYPH. When the 1.png request
   fails (no documents yet, a render error), the bare `<img>` drew the browser's torn-page icon
   with the alt text spilling beside it — the audit's "broken image" finding. `onerror` swaps it
   for a stated empty state: same box, the reason in words. Degrade honestly (R8): the failure
   says itself instead of impersonating a preview. */
.aff-preview .aff-preview-empty {
  display: flex; align-items: center; gap: var(--s2);
  min-height: 120px; padding: var(--s3) var(--s4);
  border: 1px dashed var(--line-2); border-radius: 3px;
  color: var(--ink-3); font-size: 13px; background: var(--sunken);
}
/* A page image that failed to load keeps its box (no torn-page glyph): dashed frame + the reason
   in its alt text, drawn where the preview was. */
img.img-failed {
  display: block; min-height: 120px; padding: var(--s3);
  border: 1px dashed var(--line-2); border-radius: 3px;
  color: var(--ink-3); font-size: 13px; background: var(--sunken);
  font-family: var(--ui); letter-spacing: 0;
}

@media (max-width: 860px) {
  .inf-grid, .inf-cols { grid-template-columns: 1fr; }
  .inf-kv { grid-template-columns: 1fr; }
  .inf-kv dt { margin-top: var(--s2); }
}

/* ============================================================== DENSE PANEL PROSE ON A PHONE

   The job's own panels carry a lot of explanatory prose — each note is one or two sentences, and
   on a 390px screen a sentence is 100-300px tall. Measured: the contract-format panel came out at
   2741px against an 844px viewport, i.e. three phone screens of text before you reach the control
   underneath it. That is the "too complex" complaint in miniature, and it is worst exactly where
   the screen is smallest.

   So on a phone the LONG explanatory notes are clipped to a few lines and expandable, while the
   CONTROLS and the state lines stay fully visible. Nothing is hidden — the text is one tap away —
   and the panel becomes scannable instead of a wall. On a desk there is room, so nothing changes. */
@media (max-width: 640px) {
  /* the long notes inside the job's panels */
  .inf-subjects .pset-note,
  .inf-subjects .notes li {
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }
  /* the states and every control stay whole: clipping THESE would hide what the panel is for */
  .inf-subjects .sig-state,
  .inf-subjects .set-line,
  .inf-subjects .req-card,
  .inf-subjects .att-quiet,
  .inf-subjects button,
  .inf-subjects .mono { -webkit-line-clamp: unset; display: revert; overflow: visible; }
  /* a panel's own trail list is a table of records, not prose — keep it readable */
  .inf-subjects .sig-trail, .inf-subjects .sig-trail li { -webkit-line-clamp: unset;
                                                          display: revert; overflow: visible; }
  .inf-cols { gap: var(--s3); }
  .inf-card { padding: var(--s3); }
}

/* ========================================================== A BLANK FIELD'S REASON, ONE HOVER AWAY

   Every unfilled field carries the reason it is unfilled, and that reason is real content (§6:
   "the documents do not say" is the honest answer, and a blank with a reason is not a defect).
   What is a defect is DRAWING ALL OF THEM AT ONCE. MEASURED on a fresh job (2026-09-25): 54 reason
   paragraphs = 1310px of the Job Details card's 5941px — a sentence under every blank field, so
   the fields the operator came to fill were buried under prose about the ones they had not.

   The client's ask was to lay this out like the office's own software, and that software draws a
   label above an input and NOTHING ELSE: no per-field paragraph at all. So the reason is not drawn
   by default; it is drawn the moment the operator touches the field it belongs to.

   WHY THIS IS NOT HIDING A FACT. The reason stays in the DOM in full — `display: none` keeps
   `innerText` and `textContent` intact (MEASURED in this headless build: a `display: none` element
   still answers `innerText` with its whole text), so a screen reader reads it, and any check that
   asserts on the reason's wording or counts the reasons still sees every one. It is one hover or
   one focus away, which is the same "one click from the source" the field's provenance keeps (§4),
   and the field's own `title` carries the whole reason as the native tooltip in the meantime. */
.inf-gap { display: none; margin: 2px 0 0; }
.inf-field:hover .inf-gap,
.inf-field:focus-within .inf-gap { display: block; }
.inf-gap[title] { cursor: help; }

/* ============================================================== A LINE MUST NOT CLIP ITS OWN VALUE

   Measured on a 390px phone: `.inf-input` inside an affidavit line reported
   `scrollWidth > clientWidth`, i.e. the control was narrower than the value inside it and the
   operator could not see the end of what a person will swear to. The cause is a grid/flex child's
   automatic minimum size — its CONTENT — so a long value widens the track instead of the input
   filling it, and `box-sizing` alone does not fix it. `min-width: 0` on the run and its control is
   what lets the input take the width it is given and scroll its own text instead of being clipped.
   This is the failure mode the project's own note names: "a field-level check passes happily while
   the rendered page clips a long value". */
.aff-run, .aff-run .inf-control, .aff-page .inf-field { min-width: 0; }
.aff-run .inf-input {
  width: 100%;
  max-width: 100%;
  min-width: 0;
  box-sizing: border-box;
  text-overflow: ellipsis;
}
.aff-run textarea.inf-input { overflow-x: auto; white-space: pre-wrap; overflow-wrap: anywhere; }
/* on a phone the document gets the full width of the card and nothing pads it out of the frame */
@media (max-width: 640px) {
  .aff-page { padding: var(--s3); }
  .aff-run .inf-label { font-size: 0.95em; }
}

/* ------------------------------------------------------- the affidavit as a DOCUMENT
   The client asked for the affidavit to open like a word processor and be fully editable. So the
   page is drawn as a sheet of paper with real document typography — not a form — and the only
   affordances around it are the template picker, the notary switch and the three actions.
   WORKSTREAM B: the editor's toolbar bands are styled once in the chrome block (~line 4300);
   these keep only the picker's own shape (and the dialog's reuse of it below). */
.affdoc .aff-toolbar { flex-wrap: wrap; gap: var(--s2); align-items: center; }
.aff-pick { display: inline-flex; flex-direction: column; gap: 4px; min-width: 22rem; }
.aff-pick .inf-input { width: 100%; }
.aff-choice { color: var(--ink-3); font-size: 11px; text-transform: uppercase; letter-spacing: .06em; }

/* ---- the create-affidavit template picker (the dialog `pickAffidavitTemplate()` appends) ----
   MEASURED DEFECT: the dialog was appended to <body> with these class names and NO rules anywhere
   in this file — so it painted as a bare block at the END of the page, below the fold. The job page
   stayed fully clickable behind it, the Affidavits button sat disabled on "Creating…", and the
   picker looked like a stray footer section rather than a question. The rules mirror the house
   `.modal-wrap` / `.modal` pair (line ~1094) exactly, tokens included, so dark theme and the
   phone bottom-sheet behave the same as every other dialog in this console. */
.aff-modal {
  position: fixed; inset: 0;
  background: rgba(20, 23, 27, 0.5);
  display: flex; align-items: center; justify-content: center;
  padding: var(--s4);
  z-index: 60;
  overscroll-behavior: contain;
}
.aff-modal-card {
  background: var(--surface);
  border: 1px solid var(--line-2);
  border-radius: var(--r-xl);
  max-width: 520px; width: 100%;
  padding: var(--s5);
  box-shadow: var(--e3);
}
.aff-modal h3 { margin: 0 0 var(--s3); font-size: 17px; font-weight: 680; letter-spacing: -0.02em; }
.aff-modal .pset-note { margin: 0 0 var(--s4); }
@media (max-width: 720px) {
  /* dialogs: bottom sheet behaviour, so they land under the thumb — and `.aff-pick`'s 22rem
     min-width is wider than a 390px card once padded (measured shape of the 390px defects). */
  .aff-modal { align-items: flex-end; padding: 0; }
  .aff-modal .aff-modal-card {
    max-width: none; width: 100%;
    border-radius: var(--r-xl) var(--r-xl) 0 0;
    padding: var(--s4) var(--s4) max(var(--s4), env(safe-area-inset-bottom));
    max-height: 88vh; overflow-y: auto;
    overscroll-behavior: contain;
  }
  .aff-modal .aff-pick { min-width: 0; width: 100%; }
  .aff-modal .inf-actions .btn { flex: 1 1 0; min-height: 46px; }
}

.aff-doc-wrap { margin: var(--s3) 0 var(--s2); }
.aff-doc {
  background: #fff; color: #111;
  /* A SHEET, not a panel: the document is what gets signed, so it is drawn the way it will be
     read — plain white, generous margins, a serif face, and no console chrome inside it. */
  font-family: Georgia, "Times New Roman", serif;
  font-size: 15px; line-height: 1.55;
  padding: 2.4rem 2.6rem; border: 1px solid var(--line); border-radius: 3px;
  box-shadow: 0 1px 3px rgba(0,0,0,.10), 0 10px 24px -18px rgba(0,0,0,.35);
  max-width: 62rem; min-height: 26rem;
  white-space: pre-wrap; overflow-wrap: break-word;
  outline: none;
}
.aff-doc:focus { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent); }
.aff-doc .att-null { font-family: var(--sans, system-ui); font-size: 13px; color: #666; }
html[data-theme="dark"] .aff-doc { background: #f7f7f4; color: #111; }

.aff-factlist { margin: 0; padding-left: 1.1rem; }
.aff-factlist li { margin: 0 0 6px; }
.aff-where { color: var(--ink-3); font-size: 11px; }
.affdoc .inf-actions { display: flex; gap: var(--s2); flex-wrap: wrap; margin-top: var(--s2); }

/* ------------------------------------------------- the email picker (one component, every share) */
.email-picker { border: 1px solid var(--line); border-radius: 6px; padding: var(--s2); margin: var(--s2) 0; }
.ep-head { display: flex; justify-content: space-between; align-items: baseline; gap: var(--s2); }
.ep-search { margin: 6px 0; position: relative; }
.ep-search .inf-input { width: 100%; }
/* THE MAGNIFIER MAKES THE SEARCH BAR THE SEARCH BAR (Kazim, 2026-09-29: "make the search bar
   unique so the person operating the system knows which bar is the search bar and which bar is for
   typing in the email address manually"). It sits INSIDE the field, so it costs no layout height and
   cannot be mistaken for a separate control. */
.ep-search-glyph { position: absolute; left: 9px; top: 50%; transform: translateY(-50%);
                   color: var(--ink-3); font-size: 14px; pointer-events: none; }
.ep-search-input { padding-left: 26px !important; }
/* THE MANUAL BAR IS MARKED BY WHAT IT DOES, not by another box: a mono `@` prefix on its label,
   and its own placeholder says "press Enter to add it to this send". */
.ep-manual-input { font-family: var(--mono, ui-monospace, monospace); }
/*
 * THE RESULTS DROP DOWN OVER THE FORM (Kazim, 2026-09-29): "make the searching show the email
 * addresses as a down pop up from the bar instead of filtering the list that is there".
 *
 * It used to be an in-flow block with `max-height: 22rem` — so every keystroke pushed the manual
 * address bar and the chips further down the panel, and the operator's own typed address jumped
 * around under the cursor while they were using it. As an overlay it costs no layout: the panel
 * below stays exactly where it is, and the matches appear under the bar like a combobox.
 *
 * `position: absolute` is relative to `.ep-search` (now `position: relative`), so it tracks the bar
 * it belongs to. It is NOT `position: fixed`, which would detach it from the field on scroll.
 */
.ep-results { position: absolute; z-index: 40; left: 0; right: 0; top: calc(100% + 2px);
              max-height: 20rem; overflow-y: auto; background: var(--card, var(--raised));
              border: 1px solid var(--line-2, var(--line)); border-radius: var(--r-md);
              box-shadow: 0 8px 24px rgb(0 0 0 / 28%); padding: 0 10px; }
/* An EMPTY results box must not draw a floating empty card over the form: the element is only
   painted when it holds something (`has-rows`), which the painter sets. */
.ep-results:not(.has-rows) { display: none; }
.ep-entry { padding: 8px 0; border-bottom: 1px solid var(--line); }
.ep-entry:last-child { border-bottom: 0; }
.ep-entry-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.ep-kind { font-size: 10px; letter-spacing: .07em; text-transform: uppercase; color: var(--ink-3);
           border: 1px solid var(--line); border-radius: 3px; padding: 1px 5px; }
.ep-name { flex: 1 1 auto; }
.ep-n { color: var(--ink-3); font-size: 11px; }
.ep-all { display: inline-flex; align-items: center; gap: 4px; font-size: 12px; }
.ep-addrs { list-style: none; margin: 4px 0 0; padding: 0 0 0 4px; }
.ep-addrs li { margin: 2px 0; }
.ep-addrs label { display: inline-flex; align-items: center; gap: 6px; }
.ep-label { font-size: 11px; color: var(--ink-3); }
.ep-manual { margin-top: var(--s2); }
.ep-manual-row { display: flex; gap: 6px; align-items: center; }
.ep-manual-row .inf-input { flex: 1 1 auto; }
.ep-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
.ep-chip { display: inline-flex; align-items: center; gap: 4px; border: 1px solid var(--line);
           border-radius: 999px; padding: 2px 8px; background: var(--raised); }
.ep-x { border: 0; background: transparent; cursor: pointer; font-size: 14px; line-height: 1;
        color: var(--ink-3); }
.ep-x:hover { color: var(--danger, #c0392b); }

/* ---------------------------------------------------------- the attempt report picker */
.report-picker { border: 1px solid var(--line); border-radius: 6px; padding: var(--s2); margin: var(--s2) 0; }
.rp-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.rp-count { color: var(--ink-3); margin-left: auto; }
.rp-list { list-style: none; margin: 6px 0 0; padding: 0; }
.rp-row { padding: 6px 0; border-bottom: 1px solid var(--line); }
.rp-row label { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.rp-served { border-left: 3px solid var(--accent); padding-left: 8px; }
/* AN UNAPPROVED DELIVERY IS GREYED OUT AND UNTICKABLE (Kazim, 2026-09-27). The row states its
   state rather than merely looking quiet: it is dimmed, the tick is disabled with a not-allowed
   cursor, and the badge + reason carry the record's own words. `.rp-blocked` on the label is what
   the check reads to prove the grey is applied and not just asserted in a comment. */
.rp-unapproved { border-left: 3px solid var(--ink-4); padding-left: 8px; opacity: .62; }
.rp-unapproved label { cursor: default; }
.rp-unapproved input[type="checkbox"] { cursor: not-allowed; }
.rp-unapproved-flag { background: var(--ink-4); color: var(--paper, #fff); }
.rp-unapproved-why { display: block; margin-top: 2px; }

/* ------------------------------------------------------------- the directory overlay */
/* THE DIRECTORY OVERLAY FOLLOWS THE CONSOLE'S OWN PATTERN (.src-overlay): `position: fixed; inset:
   0` with a FULL background colour, not a translucent one over the page. A translucent sheet let
   the case cards and the toolbar show through the dialog — measured in a screenshot, the client's
   own title bar read straight through "Clients, employees & servers" — which makes a form
   unreadable and looks broken. The console's other full-screen surfaces already do it this way, and
   the tokens are the ones they use. */
.dir-overlay {
  position: fixed; inset: 0; z-index: 60;
  background: var(--bg); padding: var(--s4);
  display: flex; flex-direction: column;
  overflow-y: auto; overscroll-behavior: contain;
}
.dir-overlay[hidden] { display: none; }
.dir-card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-xl);
            max-width: 68rem; width: 100%; margin: 0 auto;
            padding: var(--s4); }
.dir-search { display: flex; align-items: center; gap: 8px; margin: var(--s2) 0; }
.dir-search .inf-input { flex: 1 1 auto; }
.dir-new { border: 1px solid var(--line); border-radius: 6px; padding: var(--s2); margin-bottom: var(--s2); }
.dir-new .inf-input { width: 100%; margin-bottom: 6px; }
.dir-emails-head { display: flex; justify-content: space-between; align-items: center; margin-top: 6px; }
.dir-email-row { display: flex; gap: 6px; margin-bottom: 4px; }
.dir-email-row .inf-input { flex: 1 1 auto; margin: 0; }
.dir-new-acts { display: flex; align-items: center; gap: var(--s2); margin-top: 8px; }
.dir-list { margin-top: var(--s2); }
.dir-entry { border: 1px solid var(--line); border-radius: 6px; padding: var(--s2); margin-bottom: 8px; }
.dir-entry-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.dir-n { color: var(--ink-3); }
.dir-addrs { list-style: none; margin: 4px 0 0; padding: 0; }
.dir-addrs li { margin: 2px 0; }
.pset-h { margin: var(--s2) 0 4px; }
.brief-kv { margin-top: 4px; }

/* ================================================================== NOTIFICATIONS (E6.4)
   The feed and its pop-ups. Material, not a flat list: each row is a surface that lifts slightly on
   hover, and the kind is carried by a small coloured mark rather than by shouting the whole line.
   The pop-up is a real material — translucent, blurred, with a bright top edge — and it NEVER blocks:
   no scrim, no pointer capture, so the operator keeps working while it is there. */
.nf-item {
  display: flex; flex-direction: column; gap: 6px;
  padding: 12px 14px; margin-bottom: 8px;
  border: 1px solid var(--line); border-radius: 10px;
  background: color-mix(in srgb, var(--raised) 92%, transparent);
  transition: border-color 160ms ease, transform 160ms cubic-bezier(.2,.7,.3,1);
}
.nf-item:hover { border-color: color-mix(in srgb, var(--accent) 45%, var(--line)); }
.nf-item.read { opacity: .62; }
.nf-item.read .nf-head { font-weight: 400; }
.nf-top { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.nf-kind {
  font: 600 10.5px/1 var(--mono, ui-monospace, monospace);
  letter-spacing: .04em; text-transform: uppercase;
  padding: 3px 7px; border-radius: 999px;
  border: 1px solid var(--line); color: var(--ink-3);
}
.nf-kind.warn { border-color: var(--warn-line); color: var(--warn); }
.nf-kind.bad  { border-color: var(--danger-line); color: var(--danger); }
.nf-kind.good { border-color: var(--ok-line); color: var(--ok); }
.nf-case { font-weight: 600; cursor: pointer; }
.nf-case:hover { text-decoration: underline; text-underline-offset: 2px; }
.nf-when { margin-left: auto; font-size: 12px; color: var(--ink-3); }
.nf-head { margin: 0; font-size: 13.5px; line-height: 1.5; }
/* THE COMPOSER'S OWN TEXT, under the one-line head. A composer states a `text` (the requester's own
   note is quoted in it, and the holderless job's row says who decides there) and the row drew only
   the `head` — so the note a person typed reached nobody, on a panel that is the whole point of the
   ask. Set apart from the head so it reads as the body it is, not as a second title. */
.nf-text { margin: 2px 0 0; font-size: 12.5px; line-height: 1.5; color: var(--ink-2); }
.nf-acts { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* THE ANSWER TO A PENDING ASK, on the row itself. It sits above the row's own controls and is
   separated from the "Mark read / Clear" pair so the ACT the notification owes is never mistaken
   for the bookkeeping beside it. */
.nf-answer { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
             padding-top: 2px; }
.nf-deliv { font-size: 12px; }
.nf-deliv.good { color: var(--ok); }
.nf-deliv.bad  { color: var(--danger); }
.nf-deliv.quiet { color: var(--ink-3); font-style: italic; }
#notify-open.has-unseen { border-color: color-mix(in srgb, var(--accent) 60%, var(--line)); }

/* ---- the pop-ups: a material layer, never a modal ---- */
.toasts {
  position: fixed; right: 18px; bottom: 18px; z-index: 30;
  display: flex; flex-direction: column-reverse; gap: 10px;
  max-width: min(380px, calc(100vw - 36px)); pointer-events: none;
}

/* A POP-UP MUST NOT SIT ON TOP OF AN OPEN PANEL. Measured: with the Notifications panel open, three
   toast cards covered the feed's own rows and the panel's buttons — the operator could not read what
   the pop-up was telling them about. A toast is ambient; a panel is the thing being worked in, so the
   panel wins and the pop-ups step aside rather than stacking over it.
   They are hidden rather than removed, so they come back when the panel closes without re-announcing
   (an announcement already made is not made twice). */
body:has(.form-overlay:not([hidden])) .toasts { opacity: 0; pointer-events: none; }
.toasts { transition: opacity 180ms ease; }
@media (prefers-reduced-motion: reduce) { .toasts { transition: none; } }
.toast {
  pointer-events: auto;
  background: color-mix(in srgb, var(--raised) 78%, transparent);
  -webkit-backdrop-filter: blur(18px) saturate(180%);
  backdrop-filter: blur(18px) saturate(180%);
  border: 1px solid color-mix(in srgb, var(--line) 70%, transparent);
  border-top-color: var(--line-2);
  border-radius: 12px; padding: 12px 14px;
  box-shadow: 0 12px 34px rgba(0,0,0,.30), 0 2px 6px rgba(0,0,0,.18);
  animation: toast-in 320ms cubic-bezier(.2,.8,.25,1) both;
}
.toast-top { display: flex; align-items: baseline; gap: 8px; margin-bottom: 5px; }
.toast-case { font-weight: 600; font-size: 12.5px; }
.toast-text { margin: 0 0 9px; font-size: 13px; line-height: 1.45; }
.toast-acts { display: flex; gap: 7px; }
@keyframes toast-in {
  from { opacity: 0; transform: translateY(10px) scale(.97); }
  to   { opacity: 1; transform: none; }
}
/* Reduced motion: the pop-up ARRIVES, it just does not travel. Opacity still changes, because
   appearing is the whole point of a notification and a cross-fade carries that fine. */
@media (prefers-reduced-motion: reduce) {
  .toast { animation: toast-fade 220ms ease both; }
  .nf-item { transition: none; }
}
@keyframes toast-fade { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-transparency: reduce) {
  .toast { background: var(--raised); backdrop-filter: none; }
}

/* ============================================================================
   THE AFFIDAVIT GENERATOR'S CHROME — Workstream B, 24 Sep 2026 (impeccable · Operate,
   with the apple-design feel).

   The generator is ONLY: the acts toolbar (Save / Cancel / Share), the template picker, the
   format toolbar, the document. The chrome below is the material those four pieces are made
   of: a translucent toolbar layer the sheet scrolls beneath (apple-design §12 — content
   under the chrome, not bars that consume a fixed strip), instant press feedback on
   :active (§1 — respond on pointer-down), tokens throughout, dark theme carried by the
   tokens, and the reduced-motion / reduced-transparency equivalents stated explicitly.

   EVERY selector here was measured against the editor's own DOM (`infAffidavit` in app.js):
   `.affdoc` is the card (`section.step-card.inf-card.affdoc`), `.affed-actions` the acts
   toolbar (`#aff-acts`), `.aff-toolbar` the picker row, `.aff-fmt` the format toolbar,
   `.affed-sheet` (`#aff-doc`) the editable sheet, `.affed-pagewrap` its scroller.
   ============================================================================ */

/* ---- THE SHEET: pages of paper on a sunken desk ---- */
/* One `.aff-page` block per template page (the engine's run `page` field; see
   `informal_affidavit_doc.render_html`, which paginates the PDF the same way), and
   `.aff-line` per run carrying the run's own `style` (align/bold/italic/underline/
   size_pt/colour) as inline CSS — the same keys the PDF renderer styles from, so the
   screen cannot disagree with what prints. A legacy record draws neither and reads
   exactly as it did before. */
.affed-sheet { display: block; }
.affed-sheet .aff-page { display: block; background: inherit; }
.affed-sheet .aff-page + .aff-page {
  margin-top: 26px; padding-top: 26px; border-top: 1px dashed var(--line-2);
}
.affed-sheet .aff-line { display: block; min-height: 1em; }
.affed-sheet .aff-line:empty::after { content: "\00a0"; }

/* ---- THE PAGE DRAWN AT THE COURT FORM'S OWN MEASUREMENTS ------------------------------------
   Each `.aff-page` block is sized in POINTS from the template's own `page_geometry` (written
   inline by the renderer) and each `.aff-line` is absolutely positioned at its own measured
   spot, with its own `line-height` — so the sheet on screen is the form's page, not a browser's
   idea of a document. The engine's PDF renderer draws the same way from the same numbers
   (`informal_affidavit_doc.render_html`), so the two cannot drift (R9).

   The rules here are the ONES A STYLE ATTRIBUTE CANNOT CARRY, and nothing else: the inline
   style already says where each line is, how big it is and what colour it is. */

/* A PAGE LAID OUT AT THE FORM'S OWN POSITIONS CARRIES NO PADDING (the renderer writes `padding:0`
   inline on such a page; this is the belt to that pair of braces). An absolutely positioned
   child is measured from its containing block's PADDING box — CSS 2.1 §10.1, measured in this
   browser — and a placed line's coordinates already come from the sheet's top-left, so a page
   that also carried the form's margins as padding would push every line a margin too far right
   and down. The engine's own renderer states the same rule (`.page.abs { padding: 0 !important }`).
   `overflow: hidden` is the form's own behaviour: the form clips at its edge, it does not grow,
   and a page that silently grew would move every page break below it. */
.affed-sheet .aff-page.abs { padding: 0 !important; overflow: hidden; position: relative; }

/* THE FORM'S OWN FIELD BOXES AND RULES, UNDER THE LINES. On a court form much of what the eye
   reads as the form IS these boxes — the light blue fields and the grey rules. They are drawn at
   their measured rects, BEFORE the lines in DOM order so a value sits on its own field rather than
   under a line of text. A box that is NOT a field is decoration in the CSS sense only: no border-box
   growth, no pointer, no selection, and `contenteditable="false"` in the markup so the browser never
   puts a caret in one (they contribute no line to the text Save reads — measured). */
.affed-sheet .fbox { position: absolute; box-sizing: border-box; pointer-events: none;
  user-select: none; }

/* ---- THE BLUE BOX IS THE FIELD, AND IT IS TYPEABLE ------------------------------------------
   THE CLIENT'S OWN DEFECT, and it was not the position: every blue rect was already drawn at its
   measured PDF coordinate. What was wrong was that the box was a `pointer-events: none` decoration
   while the line's text — which IS the field's value, because the engine appends it to the label's
   own run — was drawn as a `.aff-line` flowing from the LABEL's left edge. So the operator typed
   over the printed words and could not click the box ("I can't properly type in where there should be
   a text box (but there isn't)").
   Now the box IS the input: it takes the pointer, it holds a caret, it is the thing the operator
   clicks and types in, and the value it shows is that line's own value read from the record. */
.affed-sheet .fbox.aff-field {
  pointer-events: auto; user-select: text;
  display: flex; align-items: center; gap: 3pt;
  padding: 0 3pt; overflow: hidden;
  font-size: 7.5pt; line-height: 1;
  color: #14171b; cursor: text;
}
/* THE FORM'S PRINTED WORDS STAY PRINTED WORDS. The label the form prints is drawn INSIDE the box as
   a non-editable span, so the operator reads it where the form puts it and can never type over it.
   It is `user-select: none` and `contenteditable="false"`, so it contributes nothing to the text
   Save reads. A box whose label the form prints OUTSIDE it (the caption's `TELEPHONE NO:` sits left
   of its field) carries no span: the label is the line's own printed text beside the box. */
.affed-sheet .fbox .fbox-label { color: #14171b; white-space: nowrap; user-select: none;
  pointer-events: none; }
/* THE VALUE THE OPERATOR TYPES. It is the editable part of the box, and it is the line's own value:
   what is typed here is sent as that line, so the field and the document cannot disagree. A box with
   no value from the job is drawn EMPTY and is still a clickable write-in area — absent means blank,
   with the box there to type into (the client's hardest rule: the template's own example wording is
   never put here).

   THE VALUE FILLS THE BOX'S WHOLE HEIGHT, and that is the client's own complaint being closed. It was
   `flex: 1 1 auto` inside an `align-items: center` box with a 7.5pt font in an 11pt rect, so the
   editable span occupied a thin band in the middle and the rest of the blue was the box's own
   padding — which is `pointer-events: none` on the wrapper (see `.fbox`) and therefore fell through
   to the sheet. Measured as the operator describes it: "you have to get the cursor like on exactly the
   bottom line of it to get to type in it", and a click in the blue above that band was taken by the
   LINE's handler as an advanced edit. Now the span is `align-self: stretch` and fills the rect, so a
   click anywhere the operator sees a field lands IN the field. The printed geometry is untouched:
   nothing here changes the box's position or size, only which element inside it takes the pointer. */
.affed-sheet .fbox .fbox-value { flex: 1 1 auto; min-width: 0; outline: none; align-self: stretch;
  display: flex; align-items: center;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: text;
  border-bottom: 0.5pt solid transparent;
  /* THE VALUE SPAN — NOT THE BOX AROUND IT — IS WHAT SITS ABOVE THE PRINTED LINES, AND THAT
   * DISTINCTION IS THE WHOLE FIX. Measured on EA-200 (the form in the operator's screenshot): a
   * `.fbox` and a `.aff-line` are BOTH `position: absolute` with `z-index: auto`, and the renderer
   * emits the boxes FIRST and the flow after them (`${boxes}${flow}`) — so every line the form prints
   * ACROSS a box (its caption, `Case Number:`, the paragraph to its left) painted ON TOP of the field
   * and took the pointer at the box's own centre. He reported it exactly so: "All these blue sections
   * should be text boxes yet they aren't." Measured: 20 of EA-200's 22 boxes would not take a caret
   * where he clicked, and a click there was routed to the LINE as an advanced edit; over all 82
   * templates only 453 of 2,198 boxes won the pointer (80 of 82 templates affected).
   *
   * *** WHY THE SPAN AND NOT THE BOX. *** Lifting the whole `.fbox.aff-field` wins the pointer and
   * REPAINTS the form's own text: the wrapper carries the form's measured FILL (the blue write-in
   * area), and where a box's rect square covers a line's, the box's fill then hides that line's
   * words. Measured and SEEN on this very form — the header chip, `EA-200` in white on a black box,
   * came up EMPTY (screenshots `chip-with-fix.png` vs `chip-no-fix.png`). On paper the fill lies UNDER
   * the printed words, so the wrapper must stay under them too.
   *
   * The value span is the right thing to lift because it PAINTS NOTHING AT REST: its bottom border is
   * `transparent` and shows only on hover/focus (see the rules below), so raising it can never hide a
   * printed word. It is a FLEX ITEM, so `z-index` applies to it even though it is `position: static` —
   * no `position: relative` is needed, and adding one would take it out of the flex flow and move the
   * value. `z-index: 3` sits above the lines and below `.aff-snip-host` (4), so a placed snippet still
   * takes its own click. Verified both ways on EA-200: the chip's `EA-200` stays on screen AND
   * 22/22 boxes win the pointer at their centre. */
  z-index: 3;
  position: static; }
.affed-sheet .fbox.aff-field:hover .fbox-value {
  border-bottom-color: color-mix(in srgb, var(--accent) 55%, transparent); }
.affed-sheet .fbox .fbox-value:focus {
  border-bottom-color: var(--accent);
  background: color-mix(in srgb, #fff 55%, transparent); }
/* A FIELD WITH NO VALUE YET says so by being a visible target rather than an invisible one: the
   write-in rule is drawn on hover and focus only, so an empty form box does not look like a filled
   one — but a click anywhere in the blue lands the caret in the field. */
.affed-sheet .fbox.aff-field:empty .fbox-value { min-height: 1em; }

/* A PAGE THAT HOLDS NO MEASURED LINE DRAWS IN FLOW, and it keeps room to be typed in: the form's
   own page is a sheet of paper, so the box is the page's size whether or not a line landed on it.
   This is the fallback path (an older stored record), and it draws exactly as the editor did
   before. */
.affed-sheet .aff-page:not(.abs) { padding: 36pt 54pt 0; }

/* ---- THE DRAWN SHEET IS A DRAWING OF PAPER, NOT A SHEET TO SCROLL ----
   `.paged` is added by the editor only when the markup really holds page blocks (see
   `infAffidavit`), so a legacy document — plain lines, one sheet — keeps every rule it had. When
   the pages ARE the paper, the sheet's own paper, padding and text wrapping come off: the page
   blocks carry the template's own size and margins, and a document laid out at the form's own
   coordinates must not also be flowed or padded by the house sheet.

   `white-space: pre-wrap` is the one that would silently break the drawing: the page's own
   dimensions and every line's place come from the record, and a wrapping rule on the container
   turns a line the form prints on one row into two rows. */
.affed-sheet.paged {
  background: transparent; border: 0; border-radius: 0; box-shadow: none;
  padding: 0; min-height: 0; max-width: none; white-space: normal;
  line-height: normal; width: fit-content;
}
/* THE PAGE SITS IN THE MIDDLE OF THE SPACE IT HAS, as a sheet of paper on a desk does.
   `margin-inline: auto` is NOT enough here: the pagewrap is a column flex container, and in one the
   cross axis is horizontal — a flex item is stretched or aligned by `align-items`, and its auto
   margins only take effect once it is not stretched. Measured: with the auto margin alone the CR-125
   page drew hard against the left of a 1306px scroller (the legacy sheet centred, so this was a
   difference introduced by the drawing). */
.affed-pagewrap { align-items: center; }
/* …and the sheet is the only child that must not be stretched by that alignment, so it is told its
   own width (`fit-content`, above) and the wrapper's child is not grown. */
.affed-pagewrap > .affed-sheet { flex: 0 0 auto; }
/* THE SHEET IS STILL THE EDITABLE DOCUMENT, so it still answers to focus — with a ring on the PAPER
   rather than a box-shadow around a container that is now only as big as the page. */
.affed-sheet.paged:focus { box-shadow: none; }
.affed-sheet.paged:focus .aff-page { outline: 2px solid
  color-mix(in srgb, var(--accent) 55%, transparent); outline-offset: 2px; }
/* THE PAGE BLOCKS ARE THE PAPER. `background: inherit` above exists for the legacy sheet (it took
   the sheet's own tint); a drawn page is white, like the sheet it came from. */
.affed-sheet.paged .aff-page { background: #fff; }

/* ---- THE PAGES ARE SCALED TO FIT THE SCREEN, AND THE DOCUMENT IS NOT ----
   The editor measures the room and writes `transform: scale(k)` plus the scaled size here (see
   `affFitPages`). The scale is a DISPLAY scale: every point length, font size and line height
   inside is the form's own and stays that way, so the page on screen is the page that prints. */
.affed-sheet .aff-pages { transform-origin: top left; }
/* A SCALED BLOCK STILL RESERVES ITS UNSCALED BOX, so the wrapper is given the scaled size inline
   and the page blocks are taken out of its flow — otherwise the sheet would be twice as tall as
   what is drawn on it. */
.affed-sheet.paged .aff-page { position: relative; }
.affed-sheet .aff-pages .aff-page + .aff-page {
  margin-top: 26px; padding-top: 0; border-top: 0;
  box-shadow: 0 -13px 0 rgb(0 0 0 / 0.10);      /* the edge of the sheet under it */
}

/* ---- THE ACTS TOOLBAR: a translucent material the sheet scrolls under ---- */
.affed-actions {
  position: sticky; top: 0; z-index: 6;
  display: flex; gap: var(--s2); align-items: center; flex-wrap: wrap;
  margin: 0; padding: 8px 12px;
  background: color-mix(in srgb, var(--surface) 72%, transparent);
  -webkit-backdrop-filter: blur(20px) saturate(160%);
  backdrop-filter: blur(20px) saturate(160%);
  border-bottom: 1px solid var(--line);
}
/* The bright top edge — light catching the material (apple-design §12). */
.affed-actions::before {
  content: ""; position: absolute; inset: 0 0 auto 0; height: 1px; pointer-events: none;
  background: color-mix(in srgb, var(--raised) 55%, transparent);
}
.affed-actions .affed-status { margin-inline-start: auto; }
.affed-actions .affed-tpl {
  color: var(--ink-3); font-size: 11px; letter-spacing: .02em;
  max-width: 22ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.affed-actions .affed-editing { margin-inline-start: var(--s2); }

/* ---- THE PICKER ROW: the picker, and nothing else ---- */
.aff-toolbar {
  display: flex; gap: var(--s2); align-items: center; flex-wrap: wrap;
  padding: 6px 12px; margin: 0;
  background: color-mix(in srgb, var(--surface) 55%, transparent);
  border-bottom: 1px solid var(--line);
}
.affdoc .aff-toolbar { flex-wrap: wrap; gap: var(--s2); align-items: center; }
.aff-pick { display: inline-flex; flex-direction: column; gap: 3px; min-width: 22rem;
  flex: 1 1 18rem; }
.aff-pick .inf-label { font-size: 11px; color: var(--ink-3); letter-spacing: .04em; }
.aff-pick .inf-input { width: 100%; }
.aff-choice { color: var(--ink-3); font-size: 11px; text-transform: uppercase; letter-spacing: .06em; }

/* ---- THE FORMAT TOOLBAR: tight, sticky, and the document's own tools ---- */
.aff-fmt {
  display: flex; align-items: center; gap: 4px; flex-wrap: wrap;
  padding: 6px 10px; margin: 0;
  background: color-mix(in srgb, var(--surface) 62%, transparent);
  -webkit-backdrop-filter: blur(14px) saturate(150%);
  backdrop-filter: blur(14px) saturate(150%);
  border-bottom: 1px solid var(--line);
  position: sticky; top: 53px; z-index: 5;
}
/* THE TOOLBAR STAYS PUT while the document scrolls under it — a word processor's toolbars do, and a
   bold button that scrolls away is a bold button the operator has to hunt for mid-sentence. */
.aff-fmt-btn { min-width: 32px; padding: 3px 8px; font-size: 13px; line-height: 1.4;
  border-color: transparent; }
.aff-fmt-btn:hover:not(:disabled) { border-color: var(--line-2); }
.aff-fmt-b { font-weight: 800; }
.aff-fmt-i { font-style: italic; font-family: Georgia, serif; }
.aff-fmt-u { text-decoration: underline; }
.aff-fmt-sep { width: 1px; align-self: stretch; background: var(--line); margin: 0 4px; }
.aff-fmt-size { font-size: 12px; padding: 3px 6px; border: 1px solid var(--line-2);
  border-radius: var(--r-sm); background: var(--surface); color: var(--ink); }
.aff-fmt-state { font-size: 11px; color: var(--ink-4); }
/* The notary switch rides at the toolbar's end, beside the tools it qualifies. */
.aff-fmt .aff-notary { margin-inline-start: 2px; }

/* ---- INSTANT PRESS FEEDBACK on the generator's own controls ----
   The console's `.btn:active` already answers on pointer-down (apple-design §1: feedback on
   the press, not the release). The generator's tools get the same answer with a slight scale
   — a press you can SEE at the moment it happens — released by the shared transition. */
.aff-fmt-btn:active:not(:disabled) { transform: scale(0.94); }
.affed-actions .btn:active:not(:disabled) { transform: scale(0.97); }
@media (prefers-reduced-motion: reduce) {
  .aff-fmt-btn:active:not(:disabled), .affed-actions .btn:active:not(:disabled) { transform: none; }
}

/* ---- REDUCED TRANSPARENCY: the materials go frosty, not opaque-flat ---- */
@media (prefers-reduced-transparency: reduce) {
  .affed-actions, .aff-toolbar, .aff-fmt { background: var(--surface); backdrop-filter: none; }
}

/* ---- 390px PHONE: the chrome stays one column and nothing drags sideways ----
   The picker's 22rem floor is wider than a 390px screen once padded, so it drops to full width;
   the acts bar lets its status wrap under the buttons instead of squeezing them. */
@media (max-width: 720px) {
  .affed-actions { padding: 8px 10px; gap: var(--s2); }
  .affed-actions .affed-status { flex-basis: 100%; margin-inline-start: 0; }
  .affed-actions .affed-tpl { display: none; }   /* the picker below names the template */
  .aff-toolbar { padding: 6px 10px; }
  .aff-pick { min-width: 0; width: 100%; flex-basis: 100%; }
  .aff-fmt { position: static; top: auto; }      /* sticky bands eat a 844px-tall screen */
  .affed-sheet { padding: 28px 20px; font-size: 13px; }
}

/* ---- THE DOCUMENT COLUMN: sheet on a sunken ground, gap prose gone ---- */
.affed-pagewrap { padding-bottom: var(--s6); }
/* THE CHANGE LOG FOLLOWS THE WORK. It stays on the affidavit view (the E4.10 check opens this
   view and asserts the panel), but below the fold of the document — the record of changes, not
   part of the working surface. */
.aff-log-wrap { margin: var(--s4) 0 0; }

/* ============================================================================
   LOADING — a skeleton that holds the shape of what is coming
   ============================================================================
   The old loading state was one line of grey text in an empty pane: the screen reflowed from
   nothing into a full layout, and the operator could not tell whether it was working or stuck.
   This holds the structure instead, so the page settles rather than jumps (Apple: hint in the
   direction of the outcome). The shimmer is slow and low-contrast on purpose — a loading
   indicator should be calm, not a strobe.
   ============================================================================ */
.skel { padding: 26px 24px; display: flex; flex-direction: column; gap: 14px; }
.skel-head { display: flex; align-items: center; gap: 12px; }
.skel-sub, .skel-row { display: flex; align-items: center; gap: 14px; }
.skel-row { padding-top: 10px; border-top: 1px solid var(--line); }
.skel-bar, .skel-chip {
  display: block; height: 12px; border-radius: 6px;
  background: linear-gradient(90deg, var(--line) 0%, var(--raised) 50%, var(--line) 100%);
  background-size: 200% 100%;
  animation: skel-sweep 1.6s ease-in-out infinite;
}
.skel-chip { height: 20px; width: 76px; border-radius: 999px; }
.skel-bar.w40 { width: 40%; height: 17px; }
.skel-bar.w34 { width: 34%; }
.skel-bar.w30 { width: 30%; }
.skel-bar.w26 { width: 26%; }
.skel-bar.w22 { width: 22%; }
.skel-bar.w18 { width: 18%; }
.skel-bar.w14 { width: 14%; }
/* `--muted` IS NOT A TOKEN: used with NO fallback at all, so `color: var(--muted)` was INVALID AT
   COMPUTED-VALUE TIME and the element inherited its parent's colour instead of any dim tone. The
   themed step for this is `--ink-4` (this file's own measured AA-safe metadata token). */
.skel-note { margin: 6px 0 0; color: var(--ink-4, #656b73); font-size: 13px; letter-spacing: 0.01em; }
@keyframes skel-sweep { 0% { background-position: 180% 0; } 100% { background-position: -80% 0; } }

/* ---------------------------------------------------------------------------
   FEEL — instant press feedback, and motion that does not fight the operator
   ---------------------------------------------------------------------------
   `:active` on the press, never on release: feedback on touch-DOWN is what makes a control feel
   connected to the finger. Transform + opacity only, so it stays on the compositor.
   --------------------------------------------------------------------------- */
button:not(:disabled):active,
.wf-tab:active,
.home-card:active .home-card-main,
.case-back:active { transform: scale(0.98); }
button, .wf-tab, .home-card, .nf-item {
  transition: transform 110ms cubic-bezier(0.2, 0, 0, 1), background-color 140ms ease,
              border-color 140ms ease, opacity 140ms ease;
}
button:disabled { opacity: 0.5; cursor: not-allowed; }
/* The delete control must never look available while the case is still loading — the action has no
   valid target yet, and a destructive control the operator cannot judge is worse than none. */
#delete-case[data-loading] { opacity: 0.4; }

/* Reduced motion: keep the feedback, drop the movement (Apple §14). */
@media (prefers-reduced-motion: reduce) {
  .skel-bar, .skel-chip { animation: none; background: var(--line); }
  button, .wf-tab, .home-card, .nf-item { transition: opacity 120ms ease; }
  button:not(:disabled):active, .wf-tab:active,
  .home-card:active .home-card-main, .case-back:active { transform: none; }
}

/* More contrast: the skeleton must stay visible when the operator has asked for contrast. */
@media (prefers-contrast: more) {
  .skel-bar, .skel-chip { background: var(--ink); opacity: 0.28; animation: none; }
  .skel-row { border-top-color: var(--ink); }
}

/* The notification counts: separate chips that never split a number from its word. */
.nf-count {
  display: inline-flex; align-items: center; white-space: nowrap;
  color: var(--ink-4, #656b73); font-size: 12.5px; letter-spacing: 0.01em;
}
.nf-count + .nf-count::before { content: "·"; margin: 0 6px; color: var(--ink-4); }
#nf-summary { display: flex; flex-wrap: nowrap; gap: 0; align-items: baseline; white-space: nowrap; }

/* ---------------------------------------------------------------------------
   PHONE WIDTH — the workflow tabs and the job tabs must not be cut off
   ---------------------------------------------------------------------------
   Seen in a capture at 390px: the top-right "Jobs" tab was clipped off the right edge, and on the
   job page "Documents to be serv[ed]" and "Job Details" ran to the edge with their last letters
   chopped. Both rows are horizontal flex lists of fixed-ish items, so at phone width they simply
   overflowed. They now WRAP instead of being cut: a second line is honest, a clipped label is not.
   --------------------------------------------------------------------------- */
@media (max-width: 700px) {
  .wf-tabs { flex-wrap: wrap; }
  .wf-tab { flex: 0 1 auto; }
  .inf-tabs { gap: var(--s2) var(--s3); }
  .inf-tab { min-width: 0; }
  .inf-tab .inf-tab-label { white-space: normal; overflow-wrap: anywhere; }
}
/* Long values (a job's title, a case id) must wrap rather than run under the edge. */
.entry-name, .home-card-name, .inf-head h3 { overflow-wrap: anywhere; }

/* ---------------------------------------------------------------------------
   A MONOSPACED NOTE MUST BREAK, NOT OVERFLOW
   ---------------------------------------------------------------------------
   Measured at 390px: `.inv-panel` and its `.pset-note.mono` were 276px of content inside 273px of
   box — a 3px overflow, which is enough to clip the last character of a sentence and enough to trip
   the "nothing is clipped" audit. Monospaced text is wider per character and has no word-break
   rules of its own, so it needs an explicit one. `overflow-wrap: anywhere` (not `break-word`) is
   what lets a long unbroken note wrap instead of pushing the box wider than its parent.
   --------------------------------------------------------------------------- */
.pset-note, .pset-note.mono, .inv-panel, .att-panel { min-width: 0; overflow-wrap: anywhere; }

/* PHONE: the pop-ups must not become the page.
   Measured at 390x844: three stacked toasts covered the job's tabs and most of its text. On a phone
   they are limited to one, given the full width of the margin box, and kept to a single line each —
   a notification is an interruption, not a reading task. */
@media (max-width: 700px) {
  .toasts { left: 12px; right: 12px; bottom: 12px; }
  .toast { padding: 10px 12px; }
  .toast-text { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
                overflow: hidden; }
}

/* ---------------------------------------------------------------------------
   CONDITIONAL FIELDS — the field's own rule decides whether it is on screen
   ---------------------------------------------------------------------------
   The office's screenshots: Individual shows the party's own details, Organization shows *Agent for
   Service* instead; an Employee process server picks a name from its roster, a Contractor is typed
   fields. Hidden means `hidden`, so it takes no space and is not focusable — a field that is not
   shown must not be tabbable into.
   --------------------------------------------------------------------------- */
.inf-field[hidden], .inf-conditional-hidden { display: none !important; }
.inf-grid-empty { display: none; }
/* A conditional field that disappears must not leave the grid looking broken: the gap it held closes
   with it, which `hidden` already does. */

/* ============================================================================
   THE AFFIDAVIT GENERATOR — the three-pane editor
   ============================================================================
   Built to the office's own reference screens: a FIELDS column on the left holding every job field
   grouped as the job record groups them, the DOCUMENT in the middle as the page itself, and the
   TEMPLATE column on the right holding the template's own answers. The document is the centre of
   the screen, because it is the thing being produced — not a form with a preview beside it.

   Colours and spacing come from this console's tokens, so the editor is the same product as the
   rest of the console rather than a screen borrowed from another one.
   ============================================================================ */
.affed { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0; margin: var(--s3) 0 0;
         border: 1px solid var(--line); border-radius: var(--r-lg); overflow: hidden;
         background: var(--bg); min-height: 560px; }
.affed-col { min-width: 0; display: flex; flex-direction: column; }
.affed-left  { border-right: 1px solid var(--line); background: var(--surface); }
.affed-right { border-left: 1px solid var(--line); background: var(--surface); }
.affed-mid   { background: var(--sunken); }

/* the column headings, in the office's own words */
.affed-head { display: flex; align-items: center; justify-content: space-between; gap: var(--s2);
              padding: var(--s3) var(--s3) var(--s2); border-bottom: 1px solid var(--line); flex: none; }
.affed-title { font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.12em;
               color: var(--ink-3); }
.affed-search { padding: var(--s2) var(--s3); border-bottom: 1px solid var(--line); flex: none; }
.affed-search input { width: 100%; }
.affed-scroll { overflow-y: auto; flex: 1; min-height: 0; padding: var(--s2) 0 var(--s4); }

/* ---- the field palette: one group per job-record group, in the record's own order ---- */
.affed-group { border-bottom: 1px solid var(--line); }
.affed-group:last-child { border-bottom: 0; }
.affed-group > summary { display: flex; align-items: center; justify-content: space-between;
  gap: var(--s2); padding: 9px var(--s3); cursor: pointer; list-style: none;
  font-size: 10.5px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-3); }
.affed-group > summary::-webkit-details-marker { display: none; }
.affed-group > summary:hover { color: var(--ink); }
.affed-group-n { font-weight: 600; color: var(--ink-4); letter-spacing: 0; }
.affed-fields { padding: 2px 0 8px; }
/* One field: the token the template writes, and what the record holds for it. A field with no
   value says so rather than showing an empty chip — the editor must not read as though a value
   exists (R1). */
.affed-field { display: flex; align-items: baseline; gap: 8px; width: 100%; text-align: left;
  padding: 5px var(--s3) 5px var(--s3); border: 0; background: transparent; cursor: pointer;
  font: inherit; color: var(--ink-2); }
.affed-field:hover { background: var(--hover); color: var(--ink); }
.affed-token { font-family: var(--mono); font-size: 11px; color: var(--accent); flex: none;
               overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 58%; }
.affed-val { font-size: 11px; color: var(--ink-4); overflow: hidden; text-overflow: ellipsis;
             white-space: nowrap; flex: 1; text-align: right; }
.affed-field.affed-blank .affed-val { font-style: italic; color: var(--ink-4); }
.affed-field.affed-blank .affed-token { color: var(--ink-3); }

/* ---- the document, on its own sheet in the middle ---- */
.affed-pagewrap { overflow: auto; flex: 1; min-height: 0; padding: var(--s4) var(--s3); }
.affed-sheet { background: #fffdf8; color: #14171b; margin: 0 auto; max-width: 780px; min-height: 520px;
  padding: 44px 52px; border: 1px solid var(--line-2); border-radius: 3px;
  box-shadow: 0 8px 28px rgba(0,0,0,0.18);
  font-family: "Times New Roman", Times, serif; font-size: 14px; line-height: 1.72; white-space: pre-wrap; }
.affed-sheet:focus { outline: none; box-shadow: 0 8px 28px rgba(0,0,0,0.18), 0 0 0 3px color-mix(in srgb, var(--accent) 30%, transparent); }
.affed-sheet .att-null { font-family: var(--sans, system-ui); font-size: 12px; color: #7a6252;
  background: #f6ecd9; border-bottom: 1px dashed #b99a6b; padding: 0 3px; }

/* ---- the template column ---- */
.affed-card { padding: var(--s3); border-bottom: 1px solid var(--line); }
.affed-card h4 { margin: 0 0 6px; font-size: 10.5px; font-weight: 700; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--ink-3); }
.affed-card p { margin: 0 0 8px; font-size: 12px; color: var(--ink-3); line-height: 1.5; }
.affed-key { display: inline-flex; align-items: center; gap: 6px; font-size: 11.5px;
  padding: 3px 9px; border: 1px solid var(--line-2); border-radius: var(--r-pill);
  background: var(--raised); color: var(--ink-2); margin: 0 5px 5px 0; }
.affed-key .mono { color: var(--accent); }

/* ---- the actions, above the panes where the office's screen has them ----
   WORKSTREAM B: the acts toolbar is now the sticky translucent material styled in the editor
   chrome block (~line 4280). This older block keeps only the pieces that block does not restate
   — the spacer, the status and the editing chip. */
.affed-actions .affed-spacer { flex: 1; }
.affed-status { font-size: 11.5px; color: var(--ink-3); }
.affed-status.dirty { color: var(--warn, var(--ink-2)); font-weight: 600; }
.affed-editing { font-size: 11px; padding: 2px 8px; border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--accent) 16%, transparent); color: var(--accent);
  border: 1px solid var(--accent-line); }

/* ---- narrow screens: the panes stack, document first ---- */
@media (max-width: 1100px) {
  .affed { grid-template-columns: minmax(0, 1fr); }
  .affed-left, .affed-right { border: 0; border-top: 1px solid var(--line); }
  .affed-mid { order: -1; }
  .affed-sheet { padding: 28px 22px; font-size: 13px; }
}

/* ================================================================================================
   THE TRANSLATIONS WORKFLOW'S SCREEN. Its own block at the end, so it cannot perturb a rule above
   it — a second agent is working the Hague surface in this file's other sections.
   ================================================================================================ */
.tr-form { display: flex; flex-direction: column; gap: var(--s3); margin: var(--s3) 0; }
.tr-field { display: flex; flex-direction: column; gap: 6px; }
.tr-field > span { font-size: 13px; font-weight: 600; color: var(--ink-2); }
.tr-field > span em { font-weight: 400; color: var(--ink-3); font-style: normal; }
.tr-field input, .tr-field textarea {
  font: inherit; color: var(--ink); background: var(--surface);
  border: 1px solid var(--line); border-radius: var(--r-md); padding: 8px 10px; width: 100%;
  max-width: 62ch;
}
.tr-field textarea { resize: vertical; min-height: 72px; }
.tr-field input:focus, .tr-field textarea:focus { outline: 2px solid var(--accent-line); outline-offset: 1px; }

.tr-block { margin: var(--s4) 0 0; padding: var(--s3); border: 1px solid var(--line);
  border-radius: var(--r-lg); background: var(--surface); }
.tr-block h4 { margin: 0 0 8px; font-size: 13px; font-weight: 660; }
.tr-note { margin: 0; font-size: 12.5px; color: var(--ink-2); max-width: 82ch; }
.tr-note.ok { color: var(--ok, var(--accent)); font-weight: 560; }
.tr-warn { margin: 6px 0 0; font-size: 12.5px; color: var(--warn, #9a6b00); }
.tr-missing { color: var(--warn, #9a6b00); font-style: italic; }
.tr-src { font-size: 11.5px; color: var(--ink-3); }
.tr-list { margin: 6px 0 0; padding-left: 18px; font-size: 12.5px; color: var(--ink-2); }
.tr-list li { margin: 3px 0; }
.tr-refusals { margin: var(--s3) 0 0; padding-left: 18px; font-size: 12.5px; color: var(--ink-2); }
.tr-refusals li { margin: 4px 0; }

.tr-table { width: 100%; border-collapse: collapse; font-size: 12.5px; margin: var(--s2) 0 0; }
.tr-table th { text-align: left; font-weight: 620; color: var(--ink-2); font-size: 11.5px;
  text-transform: uppercase; letter-spacing: 0.04em; padding: 4px 8px 6px; border-bottom: 1px solid var(--line); }
.tr-table td { padding: 6px 8px; border-bottom: 1px solid var(--line); vertical-align: top; }
.tr-table code { font-size: 12px; color: var(--ink-2); }

/* ============================================================ APPENDED: the confirm layer above a page

   MEASURED DEFECT: pressing Remove in the Directory asked for confirmation, and the confirm was
   INVISIBLE until you left the page.

   Cause: `.modal-wrap` is `position: fixed; inset: 0; z-index: 60` and `.dir-overlay` is ALSO
   `z-index: 60`. Equal z-index means DOM ORDER decides, and `#dir-overlay` is appended to <body>
   at runtime - AFTER `#modal-wrap` (index.html line 419) - so the directory page painted over its
   own confirmation. The confirm was really there and really focused; it just had a full-screen
   panel in front of it. Pressing Escape or Cancel then "made it appear", because those closed the
   page behind it.

   Every other user of confirmDialog is inside a modal or the page flow, so none of them hit this.

   THE FIX IS SCOPED RATHER THAN GLOBAL. A hidden modal paints nothing, so raising its z-index can
   only affect a confirm that is actually open. `.src-overlay` (the provenance view, z-index 60) is
   included because it has the identical 60-vs-60 collision and is likewise appended to <body> at
   runtime; raising it unconditionally would be the same defect waiting for the next confirm opened
   from there. `.modal-wrap` is deliberately NOT raised globally, so that surfaces which do not need
   it keep exactly the layer they have always had.

   `:has()` is supported in every browser this console runs in; without support the rule simply
   does not apply and the behaviour is exactly as before. */
body:has(.dir-overlay:not([hidden])) #modal-wrap,
body:has(.src-overlay:not([hidden])) #modal-wrap {
  z-index: 100;
}


/* ================================================================================================
   THE CAPABILITY PANEL (E3.1) — APPENDED, never inserted mid-file.

   Every selector below is namespaced `#access-overlay` or `.acc-`, so this block cannot restyle
   anything that already exists. It is deliberately self-contained: the panel is an additive
   surface and a sibling session owns `app.css`'s other regions.

   Two things the layout has to get right, both measured elsewhere in this repo:
     * the panel can be TALL (22 capability rows + a 15-entry legend), so it scrolls INSIDE the
       overlay rather than growing the page — `overflow: hidden` on the shell plus one scrolling
       body is the pattern `.form-overlay` already uses;
     * `.acc-key` and `.acc-kind` are `mono` and can be long, so they wrap rather than push the
       row wider than its box on a phone.
   ================================================================================================ */
#access-overlay .acc-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--s3);
  flex-wrap: wrap; margin-bottom: var(--s4);
  padding-bottom: var(--s3); border-bottom: 1px solid var(--line, #e3e5e8);
}
#access-overlay .acc-head > span:first-child { font-size: 14px; font-weight: 640; }
#access-overlay .acc-counts { font-size: 12px; color: var(--ink-4); }

#access-overlay .acc-banner {
  margin: 0 0 var(--s4); padding: var(--s3) var(--s4); border-radius: 8px;
  background: var(--danger-soft); color: var(--danger, #a3261a); font-size: 13px;
  overflow-wrap: anywhere;
}

#access-overlay .acc-rows { display: grid; gap: var(--s2); }
#access-overlay .acc-row {
  padding: var(--s3) var(--s4); border-radius: 8px; background: var(--sunken, #f6f7f9);
  min-width: 0;
}
#access-overlay .acc-row[data-allowed="no"] { background: var(--sunken, #f6f7f9); }
#access-overlay .acc-row-head {
  display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; min-width: 0;
}
#access-overlay .acc-label { font-size: 14px; font-weight: 640; overflow-wrap: anywhere; }
#access-overlay .acc-row .acc-key {
  font-size: 11px; color: var(--ink-4); overflow-wrap: anywhere; margin-left: auto;
}
#access-overlay .acc-pill {
  flex: none; font-size: 11px; text-transform: uppercase; letter-spacing: .04em;
  padding: 2px 7px; border-radius: 999px; border: 1px solid var(--line, #e3e5e8);
}
#access-overlay .acc-pill.on { border-color: var(--ok-line, #6f9c7f); background: var(--ok-soft);
                              color: var(--ok, #1f6b3c); }
#access-overlay .acc-pill.off { color: var(--ink-4); }
#access-overlay .acc-reason {
  margin: var(--s2) 0 0; font-size: 13px; color: var(--ink-3); overflow-wrap: anywhere;
}
#access-overlay .acc-reason.acc-held { color: var(--ink-4); }
#access-overlay .acc-needs {
  margin: 6px 0 0; font-size: 11px; color: var(--ink-4); overflow-wrap: anywhere;
}

#access-overlay .acc-legend { margin-top: var(--s5); }
#access-overlay .acc-legend h4 {
  margin: 0 0 var(--s3); font-size: 13px; font-weight: 640; color: var(--ink-3);
}
#access-overlay .acc-legend-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s2); }
#access-overlay .acc-legend-item {
  font-size: 12px; color: var(--ink-3); overflow-wrap: anywhere;
  padding-left: 0;
}
#access-overlay .acc-legend-item[data-reached="no"] { color: var(--ink-4); }
#access-overlay .acc-kind { color: var(--ink); }
#access-overlay .acc-legend-ex {
  margin: 4px 0 0; font-size: 12px; color: var(--ink-4); overflow-wrap: anywhere;
}

/* A phone gets the same rows in one column; nothing here needs a second rule set, because the
   layout is already a single column of grid rows. The only thing the narrow width changes is the
   key, which stops being pushed to the right when there is no room to push it to. */
@media (max-width: 720px) {
  #access-overlay .acc-row { padding: var(--s3); }
  #access-overlay .acc-row .acc-key { margin-left: 0; flex-basis: 100%; }
}

/* ================================================================================================
   WHO HOLDS THIS JOB (E4.1, per D3) — APPENDED, never inserted mid-file.

   The access-control board's rule for this shared file is append-only, so every rule below is
   scoped to the elements `web/owner.js` creates (`#owner-strip`, `.owner-mini`, `.owner-tabnote`)
   and nothing above this line is changed. The four states carry a severity the SERVER states
   (`ok` / `warn` / `bad`), so the colour follows the module's reading rather than a rule typed
   here: a holder whose account has been deleted reads as a fault, and a job nobody holds reads as
   something to attend to — not as the same grey as everything else.
   ================================================================================================ */

.owner-strip {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s2, 6px) var(--s3, 8px);
  padding: 6px 0 8px;
  margin: 0 0 var(--s3, 8px);
  border-bottom: 1px dashed var(--line, #ddd);
  font-size: 12px;
  line-height: 1.45;
  color: var(--ink-2, #555);
}

.owner-chip {
  flex: none;
  font-family: var(--mono, monospace);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.01em;
  padding: 2px 7px;
  border-radius: 999px;
  border: 1px solid currentColor;
  white-space: nowrap;
}
.owner-chip[data-sev="ok"]   { color: var(--ok, #1a7f37); }
.owner-chip[data-sev="warn"] { color: var(--warn, #8a5a00); }
.owner-chip[data-sev="bad"]  { color: var(--danger, #b42318); }

.owner-strip .owner-reason { min-width: 0; overflow-wrap: anywhere; }
.owner-strip .owner-src,
.owner-strip .owner-same,
.owner-strip .owner-none {
  color: var(--ink-4, #888);
  font-family: var(--mono, monospace);
  font-size: 11px;
}
.owner-strip .owner-split { color: var(--ink, #111); font-weight: 600; }
.owner-strip .owner-why {
  flex-basis: 100%;
  color: var(--ink-4, #888);
  overflow-wrap: anywhere;
}
.owner-strip .owner-flag {
  flex: none;
  color: var(--danger, #b42318);
  font-size: 11px;
}

/* the one-line chip on a rail row and on a card */
.owner-mini {
  font-family: var(--mono, monospace);
  font-size: 11px;
  padding: 1px 6px;
  border-radius: 999px;
  border: 1px solid currentColor;
  max-width: 22ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.owner-mini[data-sev="ok"]   { color: var(--ok, #1a7f37); }
.owner-mini[data-sev="warn"] { color: var(--warn, #8a5a00); }
.owner-mini[data-sev="bad"]  { color: var(--danger, #b42318); }

/* the tab-level line: how many of the jobs on this tab are not shown as anyone's */
.owner-tabnote {
  margin: 0 0 var(--s4, 12px);
  padding: 8px 10px;
  border-left: 3px solid var(--warn, #8a5a00);
  background: var(--hover, #f6f6f6);
  font-size: 12px;
  line-height: 1.5;
  color: var(--ink-2, #555);
}

@media (max-width: 720px) {
  .owner-strip { padding: 4px 0 6px; }
  .owner-strip .owner-why { display: none; }   /* the chip keeps the reading; the why is on it */
  .owner-mini { max-width: 14ch; }
}

/* THE REASON LINE ON A PHONE. `.owner-why` is the module's full sentence ("the record names 'sara'
   as the holder but no account with the identifier 'sara' exists any more — this job is FLAGGED,
   not cleared …"). On a desk there is room for it under the chip; at 390px it is a WALL of text
   above the step-1 card the operator came to use, and the reading itself — the chip, the role and
   the second fact — is already on the line above it. This is an APPENDED override, so the base
   rules above are untouched; measured on the rendered phone screenshot, where the paragraph ran
   longer than the intake card beneath it. */
@media (max-width: 720px) {
  .owner-strip .owner-why {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }
}

/* ================================================================================================
   THE FRONT DOOR AND THE SETTINGS SHELL (E1.5 / E5.6 / E5.2) — APPENDED, never inserted mid-file.

   Every selector below is namespaced `#settings-overlay` or `.st-`, so no existing rule can be
   restyled by this block. `web/capability_panel.js` already owns `#access-overlay` and the whole
   `.acc-*` family; this screen deliberately shares only the console's own `.form-overlay` and
   `.btn`, so it belongs to the same shell instead of inventing a second one.

   The widths are set in `em`/`px` against the console's own scale, and NOTHING here is gated on
   `hover`/`pointer`: this desk reports `hover: none`, so a pointer query would leak onto it. The one
   media query is the phone geometry the rest of the console uses (720px), and the rows are allowed
   to WRAP rather than clip — a clipped label is worse than a wrapped one.
   ================================================================================================ */

#settings-overlay .form-overlay-body {
  padding: 0;
  /* THE PANEL FILLS THE SHELL — ONE RECTANGLE, NOT A NARROW COLUMN UNDER A WIDE BAR.
     (Kazim, 2026-09-25: «it should be like a pop up yes but like a proper horizontal same ratio as
     the browser tab and almost as big as the whole page».)

     WHAT WAS WRONG. The shell is `display: flex; flex-direction: column`, so `.form-overlay-head`
     STRETCHES to the shell's full width while the body carried a `max-width`. MEASURED at his own
     2541x1285: head 2488x71 at x=26, body 1078x1161 at x=732. A full-width title bar floating over
     a narrow column is what «cooked» was describing — the two halves of one panel were two
     different shapes. The head's stretch is correct; the body's clamp was the odd one, and it is
     gone. Both now share the shell's width, so the panel reads as ONE landscape slab.

     THE SIZE IS THE SHELL'S OWN PADDING BOX: `inset: 0` less the shell's 24px gutter, i.e. the page
     minus a thin margin on every side — «almost as big as the whole page». Because the panel tracks
     the viewport in both axes, its shape IS the browser tab's shape at every window size, which is
     the other half of the ask. No `max-width` here on purpose: any cap would reintroduce the narrow
     column on a wide desk. Text does not run to the panel's edge — the content inside carries its
     own measure (`.st-section`), which is where a reading width belongs. */
  max-width: none;
  width: 100%;
  box-sizing: border-box;
}

/* THE PANEL IS CENTRED; THE SCRIM STILL COVERS THE SCREEN (fixed 2026-09-24, Kazim: «the settings
   and account settings still in mobile type mode»).

   WHAT WAS WRONG, and why the two earlier attempts at this did not fix it. `#settings-overlay` is
   the overlay ROOT: it is the element that paints the dimming scrim (`background: rgba(20,23,27,.55)`)
   and it is the element `inset: 0` stretches over the viewport. Narrowing the ROOT with a
   `max-width` therefore narrows the SCRIM WITH IT. MEASURED at the operator's own 2544x1291: the
   root ran x 733..1811 and every desk pixel beside it read (28,32,36) — the console's own dark
   ground, at FULL brightness — while the same reading inside the panel was also (28,32,36). The
   panel was a full-height column standing on an undimmed desk: precisely a phone sheet, which is
   what the operator kept calling it. My first two fixes were both real defects fixed (a 21em clamp
   on the shared `#ac-body` host, then a missing `margin: auto`), and neither was THE defect,
   because both treated the root's width as the panel's width.

   THE CONSOLE ALREADY HAS THIS RIGHT. `#notify-overlay` and `.mc-who-overlay` keep the root at
   full viewport and put the width on the INNER element (`.mc-who-overlay .form-overlay-body
   { max-width: 640px; margin: 0 auto; }`), so the scrim covers the whole screen and only the panel
   is narrow. This rule now follows that proven pattern: the root keeps `inset: 0` and NO max-width,
   and the width rides on the body — one pattern in the console, not two.

   The phone rules below are unchanged: at phone geometry the body is unclamped again and the panel
   becomes the page, which is the intended phone behaviour. */
#settings-overlay.st-overlay { max-width: none; }

/* THE FIRST SCREEN EVERY OPERATOR SEES. It sat at `8vh auto 0` — a form floating in the top
   third with dead space below it, on the one surface with nothing else on it. The overlay owns
   the whole screen (`body.signed-out .st-overlay`), so the login centres in it: a single column,
   optically centred (a touch above true centre, where a heading sits well), with the console's
   own mark above the lede so the screen opens with the thing it belongs to. */
.st-login {
  max-width: 21em;
  margin: 0 auto;
  min-height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 0 1.2em;
  box-sizing: border-box;
}

/* THE LOGIN SIZES TO ITS CONTAINER, NOT THE VIEWPORT — MEASURED (Kazim, 2026-09-24):
   the sign-in renders INSIDE the settings overlay (head + scrollable body), and
   `min-height: 100dvh` demanded 900px for a 713px box — the overlay scrolled ~190px
   ("it is scrollable for some reason") and the card was NOT centred in what was on
   screen (its centre sat 187px above the clip). `100%` of the overlay body is the
   honest box; the body is a flex column so the auto margins centre without overflow. */
.st-overlay:not([hidden]) > .form-overlay-body {
  display: flex;
  flex-direction: column;
}
.st-overlay:not([hidden]) > .form-overlay-body > #ac-body {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
  margin: 0 auto;
  width: 100%;
  box-sizing: border-box;
}
/* THE 21em CARD IS THE SIGN-IN'S, NOT THE SHELL'S (measured 2026-09-24, Kazim: «its display
   mobile style one but on desktop look»). `#ac-body` is the ONE host every view draws into —
   the login, the four Settings sections, and the account view — so the unqualified 21em clamp
   squeezed the whole shell into a narrow centred column on a desktop: the Settings tabs wrapped
   onto two rows and each section read as a phone sheet. The clamp now rides on the login's own
   wrapper: when `.st-login` is the content, the card keeps its measured 21em; any other view
   sizes to the shell (980px settings / 720px account, set by their own rules). `:has()` is the
   same selector this file already uses for the code-step (line ~4911) and the desk shell. */
.st-overlay:not([hidden]) > .form-overlay-body:has(> #ac-body > .st-login) > #ac-body {
  max-width: 21em;
}
/* THE CODE STEP JOINS THE CARD, IT DOES NOT QUEUE BELOW IT. `#stwofa` is appended after
   the login form inside `#ac-body`; with the form's full-height centring still in force
   the panel landed ~340px below it at the screen's foot (Kazim: "the gap is crazy and
   it's like at the bottom of the screen"). With the panel up, the centred-height demand
   is dropped, the suspended form sits compact at top, and the code step follows it at a
   conversational gap — one flow, top to bottom, nothing pushed off-screen. */
.st-overlay:not([hidden]) > .form-overlay-body:has(#stwofa:not([hidden])) > #ac-body,
.st-overlay:not([hidden]) > .form-overlay-body:has(#stwofa:not([hidden])) .st-login {
  min-height: 0;
  flex: 0 0 auto;
  justify-content: flex-start;
}
.st-overlay:not([hidden]) > .form-overlay-body:has(#stwofa:not([hidden])) > #ac-body {
  gap: 1.6em;
  padding-top: 1.6em;
}
/* the code panel speaks WITH the suspended form: same column, its lede as the active
   heading, and a hairline separator marking the step change — not a floating panel
   marooned in empty space. */
#stwofa {
  padding-top: 1.4em;
  border-top: 1px solid var(--line);
}

/* The mark, drawn by `renderLogin` before the lede: the same rosette the bar carries, so the
   signed-out screen is unmistakably this console and not a bare form. `.mark-rose`'s base rule
   pins 26px for the bar; the login's mark is its own size here, full opacity — it is the heading
   of this screen, not a toolbar glyph. */
.st-login-mark { margin: 0 0 1.1em; color: var(--accent); }
.st-login-mark .mark-rose { width: 34px; height: 34px; display: block; opacity: 1; }

/* THE LEDE IS THE HEADING. It was a 1.05em paragraph under nothing; as the first line of a
   centred screen it is the heading, and headings get size and tight tracking, not opacity. */
.st-login-lede {
  margin: 0 0 1.3em;
  font-size: 1.3em;
  font-weight: 680;
  letter-spacing: -0.02em;
  line-height: 1.25;
  color: var(--ink);
}

.st-login-form { display: flex; flex-direction: column; gap: 0.9em; align-items: stretch; }

.st-field { display: flex; flex-direction: column; gap: 0.35em; }

.st-field-label { font-size: 0.85em; opacity: 0.75; }

.st-input {
  font: inherit;
  padding: 0.55em 0.7em;
  min-width: 0;
  width: 100%;
  box-sizing: border-box;
  border-radius: 6px;
  border: 1px solid var(--line, currentColor);
  background: var(--raised, transparent);
  color: inherit;
}

/* The one action on this screen carries the primary weight: it is the only solid-accent control
   here, the same treatment "Create Hague" gets on the desk — a ghost button under a form read as
   a secondary action and left the screen without a visual anchor. */
.st-login-submit {
  align-self: stretch;
  min-height: 36px;
  margin-top: 0.2em;
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
  font-weight: 600;
}
.st-login-submit:hover:not(:disabled) { background: var(--accent-hi); border-color: var(--accent-hi); }

.st-login-message {
  margin: 0.2em 0 0;
  padding: 0.6em 0.8em;
  border-left: 3px solid var(--fault, #b4463a);
  background: var(--sunken, transparent);
  border-radius: 4px;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.st-banner {
  margin: 0.6em 0;
  padding: 0.7em 0.9em;
  border-left: 3px solid var(--fault, #b4463a);
  background: var(--sunken, transparent);
  border-radius: 4px;
  overflow-wrap: anywhere;
}

/* ---- the Settings tabs. A horizontal row of labels WRAPS at narrow widths rather than clipping
   its last word, which is the measured failure on this console's other tab strips. */
.st-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35em;
  padding: 0.8em 1.2em 0.4em;
  border-bottom: 1px solid var(--line, currentColor);
  /* THE TAB STRIP SHARES THE CONTENT'S MEASURE (Kazim, 2026-09-25: «it should be like a pop up yes
     but like a proper horizontal same ratio as the browser tab and almost as big as the whole
     page»). The panel is page-sized now, so a strip that ran the panel's full width would put the
     tabs at the far left while the section content sat centred 600px in — two things that belong to
     one column reading as two unrelated ones. The strip carries the SAME measure as the section
     below it and centres with it, so the tabs and the content share one left edge, and the border
     under the strip spans that measure — the column's own rule, not the panel's. */
  /* THE STRIP'S OWN SIDE PADDING IS ZERO ON A DESK, and that is what makes the alignment exact:
     the section's inner column is 72rem centred inside the section's 1.2em padding, so its content
     starts 1.2em in from the column's box. If the strip also carried 1.2em inside a 72rem box, the
     first tab would sit one padding further right than the section's first line (measured: 658 vs
     637). With no side padding and the same 72rem, the strip's content edge IS the section's content
     edge — first tab and first line share one left edge, and the border under the strip spans exactly
     the column's measure. The phone rules further down restore the side padding, where the panel is
     the page and there is no column to align with. */
  width: 100%;
  max-width: 72rem;
  /* THE STRIP CENTRES WITH THE COLUMN (restored 2026-09-28). The strip and the section content share
     ONE left edge, so they move together: centring the column and left-anchoring the strip was how the
     two came apart. The alignment is exact because the strip carries NO side padding inside the same
     measure — see the note on the padding below. */
  margin-left: auto;
  margin-right: auto;
  /* NO SIDE PADDING ON A DESK, AND THAT IS WHAT MAKES THE ALIGNMENT EXACT. The section's inner column
     is `72rem` centred inside the section's own `1.2em`; if the strip also carried `1.2em` inside its
     own `72rem` box, the first tab would sit one padding further in than the section's first line
     (measured 658 vs 637). With no side padding and the same measure, the strip's content edge IS the
     column's content edge. On a phone the panel is the page, there is no column to centre, and the
     rules further down restore the side padding. */
  padding-left: 0;
  padding-right: 0;
  /* `border-box`, NOT `content-box`: `content-box` ADDS the padding to the 100% width, and at 390px
     the strip drew 412px wide — caught by `verify_settings_shell_e56.py` ("nothing on the page is
     wider than the viewport"). Nothing may exceed the viewport. */
  box-sizing: border-box;
}

.st-tab {
  font: inherit;
  padding: 0.4em 0.8em;
  min-height: 32px;
  border-radius: 6px 6px 0 0;
  border: 1px solid transparent;
  background: transparent;
  color: inherit;
  cursor: pointer;
}

.st-tab[aria-selected="true"] {
  border-color: var(--line, currentColor);
  border-bottom-color: transparent;
  background: var(--raised, transparent);
}

.st-section { padding: 1em 1.2em 2em; }

/* THE SECTION'S CONTENT MEASURE INSIDE A PAGE-SIZED PANEL (Kazim, 2026-09-25: «it should be like a
   pop up yes but like a proper horizontal same ratio as the browser tab and almost as big as the
   whole page»). The panel now fills the shell, so something inside must hold the reading measure or
   a sentence of the console's own prose runs the full width of a 2500px screen. A `max-width` on
   the section's inner column does that WITHOUT shrinking the panel: the slab stays page-sized and
   the text keeps a readable line. 72rem (~1152px at the console root, ~1267 under the 1.1 zoom) is
   deliberately WIDER than a prose column: this surface is dense product UI — cards, field rows and
   controls — and `operate` mode's rule is that data and compact UI run denser than prose while a
   real paragraph still lands in the 65–75ch band. The column is centred so the panel's own mass
   reads symmetrically. */
/* LEFT TO RIGHT, AND THE SPARE WIDTH ON THE RIGHT (Kazim, 2026-09-28, over a screenshot of the Team
   tab: «Look there is still a gap on the left, it should go from left to right, I wouldn't mind a gap
   on the right»). `margin-left: auto` was CENTRING this column, so at his 2541px window the content
   began 281px in from the panel's left edge and ended 281px short of the right — an inset on BOTH
   sides. The measure is still held (that is what keeps a paragraph readable and stops the cards
   running the full width of a 2500px desk), but the auto margin is now on the RIGHT ONLY: the content
   starts flush at the section's own padding and every spare pixel falls off the right-hand end.
   NOT `max-width: none` — that would grow the prose to the panel and lose the reading measure he
   never asked to lose; he asked where the column SITS, not how wide it is. */
/* CENTRED, WHICH IS WHAT THIS COLUMN IS FOR (Kazim, 2026-09-28, over the Directory tab: «the directory
   tab is now to the left, it should be centered»). Left-aligning every section was my overreach: I did
   it to close a 19px step between the tab strip and the first line, and it took the centring off four
   sections that never had a defect. Measured cause of the step, for the record: the strip and the
   column each carried `1.2em` inside a `72rem` box, so the first tab sat one padding to the RIGHT of
   the section's first line (658 vs 637). The fix for that is the strip carrying no side padding inside
   the same measure — which is what `.st-tabs` below does — and NOT removing the column's centring.
   THE ROSTER IS THE ONE EXCEPTION and it keeps its own override further down. */
.st-section > * { max-width: 72rem; margin-left: auto; margin-right: auto; }
/* VERY WIDE DESKS: THE CONTENT GROWS WITH THE PANEL (Kazim, 2026-09-25: «it should be like a pop up
   yes but like a proper horizontal same ratio as the browser tab and almost as big as the whole
   page»). At his 2541px window the 72rem column used 50% of the panel — a page-sized slab holding a
   ribbon, with 610px of dead gutter on either side. This step is a STEP, not fluid type: `operate`
   mode's rule is a fixed rem scale, so the measure jumps once at a width where the extra room is
   real, and the account cards tile into two columns inside it. The tabs carry the same measure (see
   `.st-tabs`) so the alignment the column depends on is preserved at this width too. */
@media (min-width: 1700px) {
  .st-section > *, .st-tabs { max-width: 104rem; }
  /* THE SECTION'S OWN LEFT PADDING STAYS AT ITS BASE VALUE (1.2em, set on `.st-section` above), and the
     desk-width override no longer zeroes it. Zeroing it was how the last change closed a 19px step
     between the tab strip and the section's first line — but the step existed only because the strip
     had ALSO been zeroed, so the fix took the gutter off the whole settings surface: measured, every
     tab's content then sat 27px from the window edge (the overlay's own 24px, plus nothing), with text
     hard against the panel's left edge. Both boxes now carry 1.2em, so they still share one left edge
     AND the surface keeps a real gutter. */
  .st-section { padding-left: 1.2em; }

  /* THE ROSTER TAKES THE PANEL'S WIDTH; THE PROSE KEEPS THE MEASURE (Kazim, 2026-09-28: «nah the
     ratio is ruined»). With the 104rem measure the roster covered 1830 of the panel's 2486px — three
     cards in the left-hand 44% of a page-sized slab with a 650px void beside them, which is what "the
     ratio" means here. The measure is the right instrument for a COLUMN OF PROSE and the wrong one for
     a TILED ROSTER.

     SO ONLY THE ROSTER IS EXEMPTED, not the section. Widening every child was the first attempt and it
     was wrong in the other direction: the section's two explanatory notes then ran the full 2486px,
     which is a ~230-character line — the exact readability the measure exists to prevent. The roster
     is the one child that is a GRID OF RECORDS rather than a sentence. The cards keep their own size
     regardless, because the grid is `auto-fit` over a 34rem floor: on this panel that is four columns
     of ~611px, spread across the width instead of bunched in the corner. The tab strip is widened with
     it so its rule spans the same column the cards do rather than stopping mid-panel.

     Scoped to THIS section: the Directory, Roles and Feature-blocks sections are prose-and-table
     surfaces where the reading measure is doing real work and is left alone. */
  /* EVERY SECTION IS CENTRED, INCLUDING THIS ONE (Kazim, 2026-09-28: «no keep all centered»). The
     roster had an override taking it to the panel's full width; that made the Team tab the odd one out
     and he asked for one rule across the surface instead. It is removed, so the roster takes the same
     centred measure as Directory, Roles, Feature blocks and Templates — and because the measure is
     `104rem` at this width the cards still tile three-up at ~608px each, centred. */
}
@media (max-width: 720px), (max-width: 900px) and (max-height: 500px) {
  /* ON A PHONE THE PANEL IS THE PAGE and there is no spare width to centre a column in. */
  .st-section > * { max-width: none; }
}

/* A PERSON'S OWN ACCOUNT, BESIDE THE SECTION TABS AND NOT ONE OF THEM. It sits at the far END of the
   same strip so it reads as part of the same menu, but it carries its own class — the four `.st-tab`s
   are the server's catalogue and a check counts them, so this must not be one. `margin-left: auto`
   pushes it to the right, away from the administrative sections, because it is a different kind of
   thing: an employee's own account is not an administrative section at all. */
.st-own-entry {
  font: inherit;
  font-size: 0.9em;
  margin-left: auto;
  padding: 0.4em 0.8em;
  min-height: 32px;
  border-radius: 6px 6px 0 0;
  border: 1px solid var(--line, currentColor);
  background: transparent;
  color: inherit;
  cursor: pointer;
}

.st-own-entry[data-selected="true"] {
  background: var(--raised, transparent);
  border-bottom-color: transparent;
}

@media (max-width: 640px) {
  /* ON A PHONE THE STRIP WRAPS AND THE ENTRY TAKES ITS OWN FULL-WIDTH ROW: at 390px a right-aligned
     control beside four tabs is a squeezed row with the label clipped, and `margin-left: auto` on a
     wrapped flex line leaves it against the right edge with dead space beside it. */
  .st-own-entry {
    margin-left: 0;
    flex-basis: 100%;
    width: 100%;
    min-height: 44px;
  }
}

.st-note { margin: 0.4em 0; opacity: 0.8; overflow-wrap: anywhere; }

.st-team-rows { display: flex; flex-direction: column; gap: 0.8em; margin: 1em 0; }

/* THE ROSTER IS A GRID, AND A GRID FIXES BOTH DEFECTS THE COLUMNS VERSION INTRODUCED (Kazim,
   2026-09-28: «nah the ratio is ruined … you put it under the other 2 on the right underneath the dev
   account there's still a huge gap»).

   MEASURED CAUSE OF THE HOLE. `columns` + `column-fill: balance` does not fill the columns in DOM
   order and then let the last one run short — it BALANCES the total height across them, so with one
   card markedly taller than the rest it gave the tall card a column to itself: measured, the five
   cards laid out 1 / 2 / 2, the developer's card alone in the first column, and the space beneath it
   ran to the bottom of the other two columns. That is the "huge gap underneath the dev account".

   A grid cannot do that: every row is as tall as its tallest card and NO card is moved to another
   column, so the DOM order is the reading order at every width. `1fr` on the track means the columns
   divide the container exactly, so the roster also fills its width instead of leaving a gutter on the
   right. `auto-fit` (not a fixed count) keeps the number of columns following the container — one on
   a phone, three or four on a desk — and the 34rem floor is generous on purpose, because each card
   carries a form and its own explanation prose and below that they wrap badly.

   `align-items` IS `start`, NOT ITS DEFAULT `stretch` (Kazim, 2026-09-28: «under each users card it has
   a lot of negative space, empty space»).

   MEASURED CAUSE. A grid row is as tall as its tallest card, and `stretch` (the default) then pulls
   every other card in that row down to that height. Measured in his own window: the developer's card
   is 351px tall and the other three are 351px TOO — but their content stops at 453px, so each of them
   carries 138px of EMPTY SPACE inside its own border. That is the negative space he is looking at: it
   is inside the card, under the controls, and it makes three short cards look like they are missing
   something.

   AN EARLIER NOTE HERE ARGUED FOR `stretch` — «a row of equal-height cards reads as organised, where
   ragged bottoms read as a broken table». THAT REASONING WAS ABOUT A DIFFERENT DEFECT and does not
   survive the measurement. The "ragged" case it feared is the `columns` version, which moved a card to
   its OWN column and left a hole running the full height of the row beside it. `start` cannot do that:
   the grid still places four cards per row in DOM order, and it only lets each card END where its
   content ends.
   AND THE BOTTOMS BARELY RAG HERE IN PRACTICE: the three operator cards carry the same blocks in the
   same order, so they end at the same pixel (453px); only the developer's card runs longer, because it
   explains the acts nobody else may perform. One card with more to say is not a broken table — and it
   is honest, where 138px of padding pretending the cards are equivalent is not. */
@media (min-width: 1200px) {
  /* FOUR ACCOUNTS PER ROW (Kazim, 2026-09-28: «make it 4 users per row in Team tab»). The floor is
     what decides the count: the roster is a CENTRED `104rem` column, which is ~1664px of CSS width at
     this root size, so a 34rem (544px) floor only fits THREE cards. 25rem (400px) fits four with the
     gaps, which is why the floor is lowered rather than the column widened — widening the column is
     what puts the roster edge-to-edge, and he has asked for the surface to stay centred.
     THE 34rem FLOOR WAS CHOSEN FOR WRAPPING, so this trades some of that away knowingly: the comment
     there read «below that they wrap badly». 25rem is the largest floor that still yields four
     columns in the centred measure, and the render is checked rather than assumed — the card's own
     prose and its 2FA panel are read at this width in `verify_team_tab_layout.py`, which asserts the
     column count so a change to the measure or the root size cannot silently drop it back to three. */
  .st-team-rows {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(25rem, 1fr));
    gap: 0.8em;
    margin: 1em 0;
    /* EACH CARD ENDS WHERE ITS CONTENT ENDS — no card is stretched to fill the row. */
    align-items: start;
  }
}

.st-team-row {
  border: 1px solid var(--line, currentColor);
  border-radius: 8px;
  padding: 0.8em 1em;
  background: var(--surface, transparent);
  /* A flex column shrinks its children; this row's children are blocks, so the row itself must be
     allowed to grow or a locked row (five refusal paragraphs) collapses to a sliver. */
  flex: 0 0 auto;
}

.st-team-row-locked { background: var(--sunken, transparent); }

.st-team-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5em 0.8em;
}

.st-team-name { font-weight: 600; }

.st-team-key { opacity: 0.7; overflow-wrap: anywhere; }

.st-chip {
  font-size: 0.8em;
  padding: 0.15em 0.5em;
  border-radius: 10px;
  border: 1px solid var(--line, currentColor);
}

.st-team-facts { margin: 0.5em 0 0; opacity: 0.8; overflow-wrap: anywhere; }

.st-team-note { margin: 0.4em 0 0; opacity: 0.85; overflow-wrap: anywhere; }

.st-team-controls { display: flex; flex-wrap: wrap; gap: 0.5em; margin: 0.7em 0 0; }

.st-team-controls .btn { min-height: 32px; }

/* ---- THE REFUSALS. A withheld act is a PARAGRAPH, never a greyed control (R14), and it is set
   apart from the controls so a reader can see at a glance that nothing here is pressable. */
.st-team-refusals { margin: 0.6em 0 0; }

/* THE SUMMARY LINE — one sentence in place of as many paragraphs as there are refused acts (Kazim,
   2026-09-28: «can you also get rid of clutter and notes here in the team tab»). It is set quieter
   than a single refusal, because it is a COUNT and not a sentence someone has to read: the sentences
   themselves are still on the element (`data-refusals`), which is what the shell's own check reads. */
.st-refusal-summary { opacity: 0.85; font-style: italic; }

.st-refusal-sentence {
  margin: 0.3em 0;
  padding: 0.45em 0.7em;
  /* a refusal is a paragraph, not a severity: no bar, the words carry it */
  border-left: 3px solid transparent;
  opacity: 0.85;
  overflow-wrap: anywhere;
}

.st-panel {
  margin: 0.8em 0 0;
  padding: 0.8em 0.9em;
  border: 1px solid var(--line, currentColor);
  border-radius: 6px;
  background: var(--raised, transparent);
  /* THE PANEL IS A STACK WITH ITS OWN RHYTHM (Kazim, 2026-09-28: «when deleting a user this part
     overlaps for somereason»).

     MEASURED CAUSE, and it is not a broken rule — it is NO rule. The delete panel's children each
     carried their own bottom margin (`h4`, `p`, `label`, every `button`), so the vertical gaps were an
     accident of which elements happened to have one: 29px under the heading, 11px under the note, and
     then NOTHING between the input and the button under it. Measured in his 4-per-row card: input
     bottom 923px, button top 923px — zero gap.
     A ZERO GAP IS NOT YET AN OVERLAP, so what he photographed is the FOCUS RING: the shell's own
     `:focus-visible` rule (line 173) draws a 2px outline with a 2px offset, i.e. the ring reaches 4px
     OUTSIDE the input's box. Typing into the confirm field — which is the whole act of deleting
     somebody — therefore paints that ring straight across the top edge of the red button below it.
     ONE `gap` ON THE STACK fixes the class rather than the instance: the ring always has room, and the
     panel's spacing stops depending on which child happens to carry a margin. */
  display: flex;
  flex-direction: column;
  gap: 0.7em;
  /* A FOCUS RING MUST NOT BE CLIPPED, so the panel never hides its own overflow. */
  overflow: visible;
}

/* THE CHILDREN STOP CARRYING THEIR OWN MARGINS, since the stack now sets the rhythm. Leaving one in
   place would add to the gap and put the panel back to spacing by accident. */
.st-panel > * { margin: 0; }

/* A BUTTON IN THE STACK SIZES TO ITS CONTENT, AS IT DID BEFORE THE STACK. In a column flex container
   `align-items` defaults to `stretch`, so every button in a panel stretched to the panel's full width:
   measured on the delete panel, where the stack turned a 154px destructive control into a full-width
   red bar. A destructive action should not become the widest thing on the card, and its hit area
   should match its own label rather than the panel's edges. `align-self` rather than `align-items` on
   the container, because the FIELDS must still stretch. */
.st-panel > .btn,
.st-panel > .st-act { align-self: flex-start; }

/* THE HEADING ROW: TITLE, THEN THE CLOSE ARROW AT THE END OF THE LINE. Measured without this: the
   arrow wrapped onto a second line and sat under the title at the panel's left edge (top 702 vs the
   title's 672), because an `h4` is a block and fills the row. `space-between` puts the arrow at the
   far end, where a close control belongs, and `align-items: center` lines it up with the title text. */
.st-panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6em;
}

.st-panel-close {
  flex: 0 0 auto;
  min-width: 28px;
  min-height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font: inherit;
  font-size: 0.85em;
  line-height: 1;
  padding: 0 0.4em;
  border: 1px solid var(--line, currentColor);
  border-radius: 6px;
  background: var(--surface, transparent);
  color: inherit;
  cursor: pointer;
}

.st-panel-close:hover { background: var(--sunken, var(--raised, transparent)); }

.st-panel-title { margin: 0 0 0.4em; font-size: 0.95em; }

.st-panel-note { margin: 0.3em 0 0.7em; opacity: 0.8; overflow-wrap: anywhere; }

.st-panel .st-input { max-width: 28em; }

.st-create-block { margin: 1em 0; }

.st-create { margin: 0.8em 0 0; }

.st-create-form { display: flex; flex-direction: column; gap: 0.7em; max-width: 28em; }

.st-create-form .btn { align-self: flex-start; }

.st-act-report {
  margin: 0 0 1em;
  padding: 0.7em 0.9em;
  border-left: 3px solid transparent;  /* the outcome states below colour the bar */
  border-radius: 4px;
  overflow-wrap: anywhere;
}

.st-act-report[data-ok="yes"] { border-left-color: var(--settled, #3c7a4a); }

.st-act-report[data-ok="no"] { border-left-color: var(--fault, #b4463a); }

.st-act-ok { font-weight: 600; }

.st-act-fail { font-weight: 600; }

.st-act-fail-said { opacity: 0.9; }

.st-danger { border-color: var(--fault, #b4463a); }

/* The desk behind the overlay is not reachable while it is up. `st-locked` is set on <body> by the
   overlay and removed when it closes, so a stray scroll cannot move the page under it. */
body.st-locked { overflow: hidden; }

/* ---- phones. One site, two rule sets, and the split is by GEOMETRY only. At the phone width the
   panel is the page: the overlay becomes a full-height sheet, and the rows wrap rather than clip. */
@media (max-width: 720px), (max-width: 900px) and (max-height: 500px) {
  /* ON A PHONE THE PANEL IS THE PAGE, and that is now about the BODY: the root spans the viewport
     in every width (it is the scrim), so the phone rule is what lifts the body's 980px clamp and
     lets the panel run edge to edge. The account view's 720px body clamp is lifted here too. */
  #settings-overlay.st-overlay {
    max-width: none;
    inset: 0;
    border-radius: 0;
  }
  #settings-overlay .form-overlay-body,
  #settings-overlay.st-own-view .form-overlay-body { max-width: none; width: 100%; margin: 0; }
  /* the login stays centred on a phone too: the desktop rule's min-height flexbox holds, only
     the outer padding changes (the old `margin-top: 4vh` pushed the form back to the top) */
  .st-login { margin: 0 auto; padding: 0 1em; }
  .st-tabs { padding: 0.6em 0.8em 0.3em; }
  .st-section { padding: 0.8em 0.9em 2em; }
  .st-team-row { padding: 0.7em 0.8em; }
  .st-team-head { gap: 0.35em 0.5em; }
  .st-team-controls { gap: 0.4em; }
  .st-panel .st-input, .st-create-form { max-width: none; }
}

/* ================================================================================================
   THE DEVELOPER'S ROW, AND THE CONTROLS IT DOES NOT HAVE (E0.5 / D5 / R14 / R15)

   APPENDED, never inserted (the board's rule for this file). All selectors are namespaced
   `.acc-dev*`, so nothing already in the console can be restyled by this block — the capability
   panel's own styles are `#access-overlay` / `.acc-*` and this adds ONE child element to it.

   WHAT IT DRESSES. The panel draws the developer's account as a FACT with no control for any of
   the five acts that land on it (D5: "NOT drawn as an editable row in an admin's list"), and it
   draws each refused act as a LINE rather than as a dead button, because a disabled button is a
   control that is drawn and cannot be used — the other half of the lie R14 rules out. The lines
   are deliberately quiet and left-bordered: they are statements of what is NOT offered, not
   actions, and they must not look like something to click.

   NO `button`/`input` SELECTOR APPEARS HERE AT ALL: `capability_panel.js` counts the controls
   inside this block off the DOM, and this file must not be the reason that count is non-zero. */
.acc-devrow {
  margin: 1em 0 0.4em;
  padding: 0.7em 0.8em;
  border: 1px solid var(--line, #2b3440);
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.02);
}
.acc-devrow h4 {
  margin: 0 0 0.45em;
  font-size: 0.95em;
  letter-spacing: 0.02em;
}
.acc-devrow p { margin: 0.3em 0; }
.acc-devfacts { font-weight: 600; }
.acc-devnote { opacity: 0.85; }
.acc-devself { opacity: 0.85; font-style: italic; }
/* One line per act that is NOT drawn as a control. Left border = "this is a statement about an
   act", never an affordance: no pointer cursor, no hover state, no button role. */
.acc-devact {
  border-left: 3px solid var(--warn-line);  /* token, not a literal amber */
  padding: 0.2em 0 0.2em 0.6em;
  font-size: 0.88em;
  opacity: 0.9;
  cursor: default;
  white-space: normal;
  overflow-wrap: anywhere;
}

/* ---- phones: the same geometry-only split the rest of this site uses. The row is the page width
   and the act lines wrap rather than scroll sideways (a horizontal scroller on a phone hides half
   of every line, and the act NAMES are the point). */
@media (max-width: 720px), (max-width: 900px) and (max-height: 500px) {
  .acc-devrow { border-radius: 0; margin: 0.7em 0; padding: 0.6em 0.7em; }
  .acc-devact { font-size: 0.9em; padding-left: 0.5em; }
}

/* ================================================================================================
   WHO IS SIGNED IN, AND SIGNING OUT (E5.1) — APPENDED, never inserted mid-file.

   Namespaced `#team-session` / `.mc-who*` / `#signout-*` / `.so-*`, so no rule above this line can
   be restyled by it and the two sessions appending to this file have no third writer in their own
   region.

   `#team-session` LIVES IN `.bar-tools`, BESIDE DIRECTORY AND NOTIFICATIONS. Its box is measured
   against the viewport rather than trusted. On a phone `.bar-tools` has only about 215px of room
   (measured in `bar-phone.js`: three controls already ran 131 -> 495 in a 390px viewport), and the
   chip's text is the OPERATOR'S OWN NAME, which has no length this file can assume — so the chip is
   given `max-width` and a truncating ellipsis rather than pushing the bar out, and on a phone it is
   narrowed further so the "More" button cannot be pushed off the edge. `min-width: 0` on the parts
   allowed to shrink is what makes a flex child actually shrink: without it a text node's own width
   is a floor and the control is what gets clipped.

   `#signout-dialog` IS A `.modal-wrap`-CLASS LAYER. Its own class is at `z-index: 60`, and the
   sign-out control is in the BAR (not inside a full-screen page), so nothing sits over it. The
   console's own rule about this — that two full-screen layers at the same `z-index` are ordered by
   DOM position, and a runtime-appended one wins — is why this dialog does not use the shared
   `#modal-wrap` NODE: that node is a sibling board's surface and `app.css` already lifts it over
   `.dir-overlay` and `.src-overlay` with a scoped `:has()` rule. A second user of that node would
   inherit that scoping for no reason; its own node at the same layer cannot collide with anything
   that was appended before it. */

#team-session {
  display: inline-flex; align-items: center; gap: 8px;
  max-width: 240px; min-width: 0; padding: 4px 10px;
  cursor: pointer; text-align: left;
}
#team-session[hidden] { display: none; }
#team-session .mc-who-face {
  font-weight: 640; font-size: 13px; color: var(--ink);
  max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
#team-session .mc-who-sub {
  font-size: 11px; letter-spacing: 0.05em; text-transform: uppercase; color: var(--ink-3);
  padding-left: 8px; border-left: 1px solid var(--line-2); white-space: nowrap;
}
/* The chip is a STATE, not an action, and it must not read as a primary control: it keeps the
   console's own ghost look and adds the one thing that matters at a glance — who you are. */
#team-session:hover { background: var(--hover); }
#team-session:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* SIGNED OUT, THE CHIP IS NOT DRAWN. `body.signed-out` is E1.5's own class on <body> and the desk
   behind the login is hidden by `access-signedout.css`; a "nobody is signed in" chip floating on the
   login screen would be the console talking about a session to the person who has not got one. The
   element is NOT removed — `hidden` is what a check reads, and a removed element is
   indistinguishable from one that was never built. */
body.signed-out #team-session { display: none !important; }
body.signed-out #signout-open { display: none !important; }

/* THE PANEL THE CHIP OPENS. Reuses the console's own `.form-overlay` shell so it belongs to the
   same screen family; every inner class is namespaced. */
.mc-who-overlay { align-items: center; }
.mc-who-overlay .form-overlay-body { max-width: 640px; width: 100%; margin: 0 auto; }
#mc-who-body { padding: var(--s4); }
.mc-who-name { font-size: 20px; font-weight: 680; letter-spacing: -0.01em; color: var(--ink); }
.mc-who-role {
  display: inline-block; margin: 6px 0 var(--s4);
  font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--accent); background: var(--accent-soft);
  border: 1px solid var(--accent-line); border-radius: var(--r-sm); padding: 3px 9px;
}
.mc-who-row { display: flex; align-items: baseline; gap: var(--s3); padding: 7px 0;
              border-bottom: 1px solid var(--line); min-width: 0; }
.mc-who-k { flex: none; width: 118px; font-size: 11px; text-transform: uppercase;
            letter-spacing: 0.06em; color: var(--ink-3); }
.mc-who-v { flex: 1 1 auto; min-width: 0; font-size: 13px; color: var(--ink);
            overflow-wrap: anywhere; }
.mc-who-v[data-absent="yes"] { color: var(--ink-4); font-style: italic; }
.mc-who-note { margin: var(--s4) 0 0; font-size: 12px; color: var(--ink-3);
               line-height: 1.45; overflow-wrap: anywhere; }
.mc-who-said { padding: var(--s4); font-size: 13px; color: var(--ink-2);
               line-height: 1.5; overflow-wrap: anywhere; }

/* THE SIGN-OUT CONFIRMATION. Its card is the console's `.modal` look with the paragraphs spaced. */
#signout-dialog .so-card { max-width: 520px; width: 100%; }
#signout-dialog .so-who { margin: 0 0 var(--s3); font-size: 14px; font-weight: 620; color: var(--ink); }
#signout-dialog .so-what,
#signout-dialog .so-effect { margin: 0 0 var(--s3); font-size: 13px; color: var(--ink-2);
                             line-height: 1.5; overflow-wrap: anywhere; }
#signout-dialog .so-effect { color: var(--ink-3); }
#signout-dialog .so-said {
  margin: var(--s3) 0 0; padding: 10px 12px; font-size: 13px; line-height: 1.5;
  border-radius: var(--r-md); overflow-wrap: anywhere;
  border: 1px solid var(--line-2); background: var(--sunken); color: var(--ink-2);
}
#signout-dialog .so-said[hidden] { display: none; }
#signout-dialog .so-said[data-kind="ended"] { border-color: var(--ok-line); }
#signout-dialog .so-said[data-kind="refused"] { border-color: var(--amber, #c98a00); color: var(--ink); }

@media (max-width: 720px), (max-width: 900px) and (max-height: 500px) {
  /* On a phone the chip shows the name only: the role is on the panel a press away, and the bar's
     own overflow sheet carries the whole control with its accessible name intact. */
  #team-session { max-width: 150px; padding: 4px 8px; }
  #team-session .mc-who-face { max-width: 118px; }
  #team-session .mc-who-sub { display: none; }
  .mc-who-k { width: 92px; }
  #mc-who-body { padding: var(--s3); }
}

/* ================================================================================================
   E5.1 — THE BAR FITS, WITH THE IDENTITY IN IT (APPENDED, scoped, and MEASURED).

   THE DEFECT, measured before this block existed, at 1440x900 on a signed-in desk:
       bar 1440x62      mark 392x63      (the mark's DESIGN height is 42)
       .mark-name  "Project Control" rendered on TWO LINES and .bar-meta's "queue" sentence on
       THREE, with the identity chip and Sign out in the bar.
   AND THE ATTRIBUTION, which is the part worth recording: removing E5.1's two controls entirely
   (not hiding them — the sign-out module re-applies its own visibility, so a hidden node measures
   nothing) left the bar at **62px with the mark at 63px and "Project Control" still on two lines**.
   So the desk's top bar had no slack BEFORE this card's controls arrived, and the card's own
   obligation is the bar that results WITH them.

   THE CAUSE, and it is one line of CSS one level in. `.mark-sub` ("Hague · Informal · Translations")
   carries `white-space: nowrap`, so its min-content width is a FLOOR: the mark cannot shrink below
   it, the flex line therefore refuses to shrink, and `.mark-name` absorbs the shortfall by wrapping
   onto a second line. Measured: keeping the slogan, the bar needs **1458px of a 1440px viewport**;
   without it, **1210px**.

   THE FIX IS THE ONE THE CONSOLE ALREADY MAKES FOR PHONES, in its own words: `.mark-sub { display:
   none; }  /* the tabs say it; the mark keeps its name */` — the same three workflow names are on
   the tab strip 100px below this bar, so the mark loses nothing a reader needs and keeps the name
   it is there for. Measured after: mark 392x63 -> 182x42 (its design size), bar right edge 1440
   with `scrollWidth - clientWidth` 0, nothing wider than the viewport, and the name on ONE line.

   SCOPED, NOT GLOBAL — AND THE SCOPE IS AN HONEST ONE. It applies only while `#team-session`
   carries `data-session="person"`, i.e. only in the state this card adds (a live session, so an
   identity is in the bar). A signed-out desk, and any build where `session_identity.js` did not
   load, keeps the base `app.css` rule EXACTLY as it was — the rule cannot half-apply and cannot
   reach a screen that has no identity strip. If the office would rather keep the slogan at desk
   and lose something else instead, this is the one block to change. */
body:has(#team-session[data-session="person"]) .bar .mark-name { white-space: nowrap; }
body:has(#team-session[data-session="person"]) .bar .mark-sub { display: none; }
/* The groups allowed to shrink. Without `min-width: 0` a flex child's min-content width is a floor
   and the shortfall is pushed out of the viewport instead — measured at 1458px on the first
   attempt, which is the same defect one level out. */
body:has(#team-session[data-session="person"]) .bar .bar-meta { min-width: 0; }
body:has(#team-session[data-session="person"]) .bar .bar-tools { min-width: 0; }

/* ---- phones: THE ROLE IS PART OF THE IDENTITY, AND THE SHEET ROW HAS ROOM FOR IT.
   MEASURED DEFECT, read off the render at 390x844: the phone's overflow sheet showed the identity
   row as "Ada Okafor" alone. On the desk the chip carries the role in a second, bordered span, and
   the phone rule above dropped that span to keep the chip narrow — but on a phone the chip lives in
   the overflow SHEET, where the row is the full page width, so there was nothing to be gained and
   the one fact the card is about (this operator's ROLE) was gone from the screen. A person on a
   phone with two tabs open could not tell an admin's session from an employee's.
   The span comes back, as one continuous label with a separator rather than a bordered box — the
   sheet's own row style, and no extra width worth measuring. */
@media (max-width: 720px), (max-width: 900px) and (max-height: 500px) {
  #team-session { max-width: 190px; }
  #team-session .mc-who-face { max-width: none; }
  #team-session .mc-who-sub {
    display: inline; padding-left: 0; border-left: 0; color: var(--ink-2);
  }
  #team-session .mc-who-sub::before { content: "· "; }
  /* The sheet row is wider than the chip's own box, so the chip is allowed the row's width rather
     than being clamped to a bar-sized sliver. */
  .bar-sheet-row #team-session { max-width: 100%; }
}
/* =================================================================================================
   THE FEATURE BLOCKS PANEL (E3.2 / E3.3) — APPENDED ONLY, never inserted mid-file.

   Every selector is namespaced `#blocks-overlay` or `blk-`, so nothing existing can be restyled and
   nothing here can collide with `capability_panel.js`'s `.acc-*` or `access.js`'s `st-`. The only
   shared classes are the console's own `.form-overlay` and `.btn`, reused on purpose so this screen
   belongs to the same shell.
   ================================================================================================= */

#blocks-overlay .blk-head {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: baseline;
  padding: 4px 2px 10px;
  border-bottom: 1px solid var(--line, #2a2f3a);
}

#blocks-overlay .blk-can {
  /* `--muted` is NOT a token: the literal `#8b93a3` was a grey chosen for DARK and was painting on
     the light ground too. `--ink-3` is the themed step for secondary text on both. */
  color: var(--ink-3, #5f656d);
  font-size: 12px;
}

#blocks-overlay .blk-banner {
  margin: 8px 0;
  padding: 8px 10px;
  border: 1px solid var(--warn, #a8763a);
  border-radius: 6px;
  background: rgba(168, 118, 58, 0.10);
}

#blocks-overlay h4 {
  margin: 16px 0 6px;
  font-size: 13px;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  /* `--muted` is NOT a token: the literal `#8b93a3` was a grey chosen for DARK and was painting on
     the light ground too. `--ink-3` is the themed step for secondary text on both. */
  color: var(--ink-3, #5f656d);
}

#blocks-overlay .blk-force,
#blocks-overlay .blk-none {
  margin: 2px 0;
  font-size: 13px;
}

#blocks-overlay .blk-none {
  /* `--muted` is NOT a token: the literal `#8b93a3` was a grey chosen for DARK and was painting on
     the light ground too. `--ink-3` is the themed step for secondary text on both. */
  color: var(--ink-3, #5f656d);
}

#blocks-overlay .blk-cell {
  border-top: 1px solid var(--line, #2a2f3a);
  padding: 8px 0;
}

#blocks-overlay .blk-role {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
  padding: 4px 0;
}

#blocks-overlay .blk-rolehead {
  min-width: 260px;
  font-size: 12px;
  /* `--muted` is NOT a token: the literal `#8b93a3` was a grey chosen for DARK and was painting on
     the light ground too. `--ink-3` is the themed step for secondary text on both. */
  color: var(--ink-3, #5f656d);
}

/* THE STOPPED STATE IS A PARAGRAPH, and it is given the width a paragraph needs — never squeezed
   onto one line with a control, because the reason is the whole point of the state. */
#blocks-overlay .blk-reason {
  flex: 1 1 100%;
  margin: 2px 0;
  font-size: 13px;
}

#blocks-overlay .blk-meta {
  flex: 1 1 100%;
  margin: 0;
  font-size: 11px;
  /* `--muted` is NOT a token: the literal `#8b93a3` was a grey chosen for DARK and was painting on
     the light ground too. `--ink-3` is the themed step for secondary text on both. */
  color: var(--ink-3, #5f656d);
}

#blocks-overlay .blk-note {
  flex: 1 1 100%;
  margin: 0;
  font-size: 12px;
  /* `--muted` is NOT a token: the literal `#8b93a3` was a grey chosen for DARK and was painting on
     the light ground too. `--ink-3` is the themed step for secondary text on both. */
  color: var(--ink-3, #5f656d);
}

#blocks-overlay .blk-said {
  margin-top: 14px;
  font-size: 12px;
  /* `--muted` is NOT a token: the literal `#8b93a3` was a grey chosen for DARK and was painting on
     the light ground too. `--ink-3` is the themed step for secondary text on both. */
  color: var(--ink-3, #5f656d);
}

/* A PHONE GETS THE SAME PAGE, STACKED. The role head stops claiming a desk-sized column, so the
   reason is not pushed off the side of a 390px screen — the trap an `overflow: hidden` ancestor
   makes invisible while every field-level check passes. */
@media (max-width: 720px), (max-width: 900px) and (max-height: 500px) {
  #blocks-overlay .blk-rolehead {
    min-width: 0;
    flex: 1 1 100%;
  }

  #blocks-overlay .blk-role {
    gap: 4px;
  }
}

/* ================================================================================================
   THE SHARING SURFACE (E4.3, per D2/D3) — APPENDED ONLY, every selector namespaced `.case-share*`.

   It dresses the block `web/case_share.js` inserts beside the holder strip and the one-line mark a
   rail row gets when a job has CHANGED HANDS. Nothing here restyles an existing rule: a base rule
   edited for one new screen is how a sibling card's layout breaks without its check noticing.
   ================================================================================================ */

.case-share {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 6px 10px;
  margin: 0 0 10px;
  padding: 8px 12px;
  border: 1px solid var(--line, #d9dee5);
  border-radius: 8px;
  /* THE SURFACE IS THE THEME'S OWN, NOT A LIGHT-MODE FALLBACK.
     `var(--surface-2, #f7f9fb)` named a variable this stylesheet DOES NOT DEFINE — its surfaces are
     `--bg`, `--surface`, `--raised`, `--sunken` — so the fallback was taken in EVERY theme and the
     panel drew `#f7f9fb` (a near-white) with `#3c4753` text. In dark mode that is a pale slab
     sitting on a dark page, measured on the deployed console: a Translations job's "what this job
     offers you" panel and the approval strip below it both read as light-mode screens pasted into a
     dark console.
     `--surface` IS redefined by `[data-theme="dark"]`, so the panel now follows the theme. It is the
     right layer too: this is a panel INSIDE the stage, not a floating card, so it takes the surface
     rather than `--raised`. */
  background: var(--surface, #f7f9fb);
  font-size: 12.5px;
  line-height: 1.45;
  color: var(--ink-2, #3c4753);
}

.case-share-head,
.case-share-holder {
  font-weight: 600;
  color: var(--ink, #1b2430);
}

.case-share[data-severity="warn"] {
  border-color: #d7c07a;
}

.case-share[data-severity="bad"],
.case-share[data-share-read="unknown"],
.case-share[data-share-read="unreadable"] {
  border-color: #c98b84;
}

.case-share-count,
.case-share-with,
.case-share-order,
.case-share-why {
  color: var(--ink-3, #5b6672);
}

/* WHO MAY EDIT IS STATED BESIDE WHO MAY OPEN. A permission the office granted is at least as
   interesting as a share, so it is drawn the same size and weight rather than as a footnote. */
.case-share-edit {
  color: var(--ink-3, #5b6672);
  min-width: 0;
  overflow-wrap: anywhere;
}

/* THE ORDERING RULE IS A SENTENCE, NOT A WALL — it wraps on a narrow desk rather than pushing the
   block wider than the card it sits in. `min-width: 0` is what actually allows the wrap inside a
   flex row: without it a long unbroken token sets the row's floor. */
.case-share-count,
.case-share-with,
.case-share-order,
.case-share-acts,
.case-share-approval,
.case-share-flags {
  min-width: 0;
  overflow-wrap: anywhere;
}

.case-share-acts {
  flex: 1 1 100%;
  margin: 2px 0 0;
  display: flex;
  flex-wrap: wrap;
  gap: 4px 10px;
}

.case-share-act[data-verdict="allowed"] {
  color: #1c6b3a;
}

.case-share-act[data-verdict="refused"],
.case-share-act[data-verdict="not-established"] {
  color: #8a4b46;
}

.case-share-approval {
  flex: 1 1 100%;
  margin: 2px 0 0;
  padding-top: 4px;
  border-top: 1px dashed var(--line, #d9dee5);
  color: var(--ink-3, #5b6672);
}

.case-share-history {
  flex: 1 1 100%;
  margin: 2px 0 0;
}

.case-share-history summary {
  cursor: pointer;
  color: var(--ink-2, #3c4753);
}

.case-share-history ol {
  margin: 6px 0 0;
  padding-left: 18px;
}

.case-share-row {
  margin: 2px 0;
}

.case-share-to {
  font-weight: 600;
}

.case-share-meta {
  margin-left: 6px;
  color: var(--ink-3, #5b6672);
}

/* THE RAIL MARK — one line, never a column, so a 390px row keeps its own layout.
   THE WARN TINT COMES FROM THE THEME, NOT FROM A LITERAL BEIGE. `#efe6d2` on `#6b5527` is a light
   warm chip that reads correctly on a light page and is a pale slab on a dark one — measured in
   dark mode on a Translations job, in the rail beside "Held by". `--warn-soft` / `--warn` are the
   palette's own warning pair and BOTH are redefined by `[data-theme="dark"]`, so the chip follows
   the theme instead of fighting it. The literals remain as fallbacks. */
.case-share-mini {
  display: inline-block;
  margin-left: 6px;
  padding: 0 5px;
  border-radius: 6px;
  background: var(--warn-soft, #efe6d2);
  color: var(--warn, #6b5527);
  font-size: 11px;
  line-height: 16px;
  white-space: nowrap;
}

@media (max-width: 720px), (max-width: 900px) and (max-height: 500px) {
  /* A phone gets a drill-down, so the block is the page's width: it stacks rather than competing for
     a row, and the count/order sentences take the full width instead of forcing a sideways scroll. */
  .case-share {
    flex-direction: column;
    align-items: stretch;
    gap: 3px;
  }

  .case-share-acts {
    flex-direction: column;
    gap: 2px;
  }

  /* THE SEPARATOR IS HIDDEN WHERE THE BLOCK STACKS, so a phone reads one fact per line rather than
     a lone "·" between them. See `case_share.js` for the measurement. */
  .case-share > .case-share-sep,
  .case-share-acts > .case-share-sep {
    display: none;
  }

  .case-share-history ol {
    padding-left: 14px;
  }
}

/* ============================================================ THE SECOND STEP OF A SIGN-IN (E7.5)
   The code step's panel, drawn by `signin_twofa.js` INSIDE the login overlay's own body
   (`#ac-body`), under the login form. APPENDED ONLY, per this board's rule, and every selector is
   namespaced `#stwofa` so no existing rule can be restyled.

   WHY IT EXISTS AT ALL — MEASURED, in the browser: with no rule of its own the panel rendered with
   the browser's defaults, and at 1440px wide the field was 1440px wide (the field is `width: 100%`
   of a panel that had no box), the button OVERLAPPED the field's own border, and at 390px the panel
   bled past the screen's edges. The panel needs the same column, field and input dress the login
   form already uses — and `#stwofa` is a plain block, so nothing here inherits from `.st-login`. */
#stwofa {
  max-width: 30em;
  margin: 0 auto;
  padding: 1.4em 1.2em 0;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  gap: 0.9em;
  align-items: stretch;
}
#stwofa[hidden] { display: none; }
#stwofa .stwofa-lede { margin: 0; font-size: 1.05em; }
#stwofa .stwofa-again { align-self: flex-start; font-size: 0.9em; }
#stwofa .stwofa-until { margin: 0; font-size: 0.85em; opacity: 0.75; }
#stwofa .stwofa-message {
  margin: 0;
  padding: 0.6em 0.8em;
  border-left: 3px solid var(--danger-line, #b4463a);  /* the palette's danger line, not a stray hex */
  background: var(--danger-soft, rgba(180, 70, 58, 0.08));
}
/* A PROMPT IS NOT AN ERROR. `paint()` has one channel for every sentence, and the FIRST thing
   the panel says — "Enter the 6-digit code…" when the box is submitted empty — styled as
   danger, read as failure before anything was tried (seen on the 2026-09-24 screenshots).
   A quiet hint is the honest weight for "nothing entered yet"; real refusals keep the
   danger dress via `data-kind="error"` (set below by the same JS that paints them). */
#stwofa .stwofa-message:not([data-kind="error"]) {
  border-left-color: var(--line);
  background: var(--sunken);
  color: var(--ink-2, inherit);
}
#stwofa .stwofa-message[data-kind="error"] {
  border-left-color: var(--danger-line, #b4463a);
  background: var(--danger-soft, rgba(180, 70, 58, 0.08));
}
#stwofa .stwofa-submit {
  /* the 2FA code COMPLETES the sign-in the primary button above started — same solid-accent
     weight, so the two-step flow reads as one action at one weight, not two weights. */
  align-self: stretch;
  min-height: 36px;
  margin-top: 0.2em;
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
  font-weight: 600;
}
#stwofa .stwofa-submit:hover:not(:disabled) { background: var(--accent-hi); border-color: var(--accent-hi); }
/* THE SUSPENDED PASSWORD FORM IS TAKEN OUT OF PLAY, NOT HIDDEN: the person must be able to see
   which account they typed while they enter the code, and a form that vanished would make the
   screen look like it belonged to nobody. */
#ac-login-form.stwofa-suspended { opacity: 0.45; }

/* ================================================================================================
   [E5.5] THE PHONES: THE LOGIN, SETTINGS, AND THE BLOCKED STATE — APPENDED, never inserted mid-file.

   ONE SITE, TWO RULE SETS, SPLIT BY GEOMETRY ONLY: `max-width: 720px`, or
   `max-width: 900px and max-height: 500px` for a landscape phone. NOTHING here is gated on
   `hover`/`pointer` — this desk reports `hover: none`, so a pointer query would leak onto the
   desktop. Measured before this block was written, on a scratch console at all eight sizes:

     * `#settings-overlay` measured 3,176px tall inside an 844px viewport, its own `overflow-y`
       resolved to `visible`, and the document could not scroll (`scrollTo(0, 500)` moved the page
       by 0px). Its content below the fold was reachable only by a trackpad two-finger swipe — on
       the device this card is about it did not scroll at all.
     * the Settings tabs measured 95px tall across two rows at 390px and 320px.
     * `Sign in` measured 35px tall and the password field 42px, at every phone width.

   WHAT THIS BLOCK DOES. `.form-overlay-body` is the overlay's OWN scroller (`overflow: auto`,
   `flex: 1`) and it already reaches every row — measured at 390x844: 2,895px of content in a
   709px box, `scrollTop` moving a full 500px when driven. So the phone does not need a second
   scroller; what it needs is for the shell to stop being a `position: fixed` box whose height the
   phone's own URL bar and safe areas can cut into. The overlay is anchored to the DOCUMENT on a
   phone (`inset: 0` with `height: auto`), so the page scrolls as one and the body's scroller still
   works on the desk. `position: fixed` on a phone is the trap here, and the console has already
   paid for it twice (see `bar-phone.css` and the `.wfbar` sticky notes).

   THE `@media (min-width: 2600px)` BLOCK IS **NOT** AT THE END OF THIS FILE, and the first draft of
   this comment said it was (it is at line ~4603, ahead of several other cards' appends) — corrected
   by READING the file rather than assuming, which is the same discipline the block's own prose asks
   for. WHAT MAKES THE APPEND SAFE IS SIMPLER AND DOES NOT DEPEND ON WHAT PRECEDES IT: a `@media`
   block does not inherit the query of the block above it, so the `(max-width: 720px)` here is
   evaluated on its own and can never fire on a desk. Nothing below this marker is anything but this
   card's rules, which is asserted by the check.
   ================================================================================================ */

/* ---- the login: the same field set, thumb-sized ----
   MEASURED before: `Sign in` 74x35px, the password field 357x42px. The rule is not a style
   preference — 35px is under the 44px short side every other control on this console is held to,
   and the login is the one screen a person reaches for a phone to use. Inputs go to 16px as well:
   below that iOS zooms the page on focus, which on this screen means the field the person is
   typing into is suddenly off-screen. */
@media (max-width: 720px), (max-width: 900px) and (max-height: 500px) {
    .st-login { margin: 0 auto; padding: 0 1em; }  /* centred, not pushed to the top */
  .st-login-submit { align-self: stretch; min-height: 46px; }
  .st-input { min-height: 46px; font-size: 16px; }
  /* the shell is the page, not a floating box a phone can cut into */
  .form-overlay.st-overlay { position: absolute; min-height: 100%; }
  /* a tab strip is a row of tappable labels, so each one is a target and the strip wraps rather
     than clipping its last word (measured two rows at 390px either way — the difference is that
     every label is now reachable with a thumb) */
  .st-tabs { padding: 0.5em 0.7em 0.3em; gap: 0.3em; }
  .st-tab { min-height: 44px; }
}

/* ---- Settings: a DRILL-DOWN, not a sheet over the rail ----
   THE DECISION THIS CARD HAD TO TAKE, and it is recorded here because the geometry is what
   implements it. Settings REPLACES THE SCREEN on a phone: the whole viewport, and the console's
   own shell (bar, workflow tabs, rail) is not painted behind it. A sheet over the rail was the
   other option and it is wrong for this screen — Settings holds four sections and a row per
   account, and a sheet that must leave room for what it is covering is a sheet whose content is
   squeezed to keep a half-drawn desk visible at its edges. The overlay is the same DOM node on
   both devices; on a phone it owns the screen, and the desk is hidden under it.

   THE DESK IS HIDDEN FOR THE SAME REASON `access-signedout.css` HIDES IT WHEN SIGNED OUT:
   covering is a paint order, not a lock. `access.js` sets the SAME class it already sets for the
   login and clears it in `close()` — no JS is changed by this card, and the class already exists
   for exactly this purpose. The overlay itself is deliberately NOT in this list. */
@media (max-width: 720px), (max-width: 900px) and (max-height: 500px) {
  body.st-locked > header,
  body.st-locked .desk,
  body.st-locked .wfbar,
  body.st-locked #stepper,
  body.st-locked .banner {
    display: none;
  }
  /* the overlay is now the only thing in flow: the bar's own height is gone with it, so the head
     row and the first section start at the top of the screen */
  #settings-overlay.st-overlay { border-radius: 0; max-width: none; padding: 0; }
  #settings-overlay .form-overlay-head { border-radius: 0; }
  #settings-overlay .form-overlay-body { border: 0; border-radius: 0; }
}

/* ================================================================================================
   THE OWNER STRIP'S FULL SENTENCE AT PHONE WIDTH — APPENDED BY E4.3 (t_3fbfb460), never inserted
   mid-file. It overrides ONE rule of E4.1's, in the direction that was MEASURED to be wrong.

   WHAT WAS MEASURED, on the rendered 390x844 capture (`tools/out/e43_share_phone.png`, looked at,
   not asserted on): E4.1's appended rule clamps `.owner-strip .owner-why` to TWO lines, and the
   strip's own reading is longer than that on a job whose holder was resolved from an account —

       the case's own record names 'sara' as its holder (Sara),
       account active — employee, resolved from the holder's…

   It reads as though the sentence had been cut off, which is the one thing a provenance line in
   this console must never look like (R2/R8: a value and the reason it was reached are read
   together, and a reason shown half-way is a reason that cannot be checked). The clamp was added
   for a real symptom — a job whose holder's ACCOUNT IS GONE carries a much longer flag sentence,
   and at 390px it was a wall of text above the step-1 card the operator came to use.

   SO THE WALL IS BOUNDED, NOT THE READING. Three lines, never two, and the tail is ellipsised by
   the clamp rather than the markup; a sentence that still does not fit is a longer reading than a
   phone screen holds rather than a value that was thrown away. This is an APPENDED override on a
   `@media` that already exists, so E4.1's base rules and the desk's own rendering are untouched.

   NOT GATED ON `hover`/`pointer`: this desk reports `hover: none`, so a pointer query would leak
   onto it. The breakpoint is the console's own geometry rule, copied exactly. */
@media (max-width: 720px) {
  .owner-strip .owner-why { -webkit-line-clamp: 3; }
}

/* -- WHAT AN APPROVAL CARRIES AFTER A CHANGE OF HANDS (E4.4, per D3) ------------------------------
   APPENDED, at the very end of the file, and every selector is namespaced `.case-approval*` so no
   existing rule can be reached. The block is drawn by `web/approval_flag.js` after the sharing
   block; it dresses what the SERVER answered (`case_approval.py`), and the only decision here is
   which state is loud: a job whose approval PREDATES its holder must be impossible to miss, and a
   job with nothing to say must be quiet enough not to be read as a warning.

   `:root { zoom: var(--zoom, 1.1) }` scales viewport units, so nothing here is sized in `vw` — the
   phone rule below is the console's own geometry breakpoint (`max-width: 720px`), copied exactly,
   and never a `hover`/`pointer` query: this desk reports `hover: none` and a pointer query would
   leak the phone rules onto it. */
/* THE APPROVAL STRIP FOLLOWS THE THEME. Every colour here was a LITERAL light-mode value —
   `#fbfbfc` background, `#d5d9e0` border, `#4a5260` / `#333a45` text — so in dark mode this panel
   painted a near-white slab with dark grey type, sitting directly above the translations card:
   measured on the deployed console in dark mode, and reported by the operator as a section that
   "looks white and weird".
   A literal colour cannot follow a palette; the console's own tokens can, and `--surface`, `--line`,
   `--ink-2` and `--ink-3` are all redefined by `[data-theme="dark"]`. The literal values stay as
   FALLBACKS so a shell with no tokens still renders, exactly as the `.case-share` panel above.
   The state colours (`--danger` / `--warn` / `--ok`) are the theme's own too: their literals
   (`#b3261e`, `#b26a00`, `#2e7d32`) are dark inks that a dark page swallows.
   The background is NOT stated for every state — the base surface plus the coloured left rule is
   what carries the meaning, and a tinted background per state is a second signal that has to be
   kept in step with the first. */
.case-approval {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 10px;
  margin: 0 0 10px; padding: 8px 10px;
  border: 1px solid var(--line, #d5d9e0);
  /* no decorative left border on the base state: the left bar is this console's severity
     language, and a neutral job wearing a grey bar made every case read as flagged. The
     severity states below colour the bar; a state with nothing to say keeps no bar. */
  border-left: 3px solid transparent; border-radius: 4px;
  background: var(--surface, #fbfbfc); font-size: 12px; line-height: 1.45;
}
.case-approval .case-approval-chip {
  font-weight: 600; letter-spacing: .01em; white-space: nowrap;
}
.case-approval[data-approval-state="approval_given_elsewhere"] {
  border-left-color: var(--danger, #b3261e);
}
.case-approval[data-approval-state="approval_given_elsewhere"] .case-approval-chip {
  color: var(--danger, #b3261e);
}
.case-approval[data-approval-state="unknown"] { border-left-color: var(--warn, #b26a00); }
/* `not_approved` is the ordinary pre-approval state of every job in flight — not a fault and not
   a warning — so it keeps the transparent bar set by the base rule: the chip already names it. */
.case-approval[data-approval-state="not_approved"] { border-left-color: transparent; }
.case-approval[data-approval-state="approval_after_move"] { border-left-color: var(--ok, #2e7d32); }
.case-approval .case-approval-facts { color: var(--ink-2, #4a5260); }
.case-approval .case-approval-stamp { color: var(--ink-2, #4a5260); }
.case-approval .case-approval-move { color: var(--ink-2, #4a5260); }
.case-approval .case-approval-why { flex: 1 1 100%; color: var(--ink-2, #333a45); }
.case-approval .case-approval-material {
  flex: 1 1 100%; color: var(--danger, #b3261e); font-weight: 600;
}
.case-approval .case-approval-flagn {
  color: var(--warn, #b26a00); font-variant-numeric: tabular-nums;
}

@media (max-width: 720px) {
  /* A LONG REASON SENTENCE ON A PHONE, AND THE CLAMP HAS TO BE THE FULL FOUR-PART IDIOM.
     MEASURED, and this was a DEFECT the first version shipped: the block had only
     `-webkit-line-clamp: 3`, which is INERT — a line clamp constrains nothing unless the element is
     a `-webkit-box` with `-webkit-box-orient: vertical` and `overflow: hidden`. The crop of the
     rendered 390x844 block showed the whole 5-line sentence, ~140px of the screen, above the case's
     own card — exactly the wall of text E4.3 hit on the holder strip. The reason is still REACHABLE
     at full length: it is the block's `title`, and the desk shows it in full. */
  .case-approval .case-approval-why {
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }
  .case-approval { font-size: 12px; padding: 8px; }
  /* THE FACTS STACK, so the separator between them must not draw as a line of its own — the same
     defect MEASURED on the holder strip's phone view. */
  .case-approval .case-approval-facts { flex-basis: 100%; }
}

/* ================================================================================================
   [E5.5] THE PHONES: THE LOGIN, SETTINGS, AND THE BLOCKED STATE — APPENDED AGAIN, at the tail.

   ONE SITE, TWO RULE SETS, SPLIT BY GEOMETRY ONLY: `max-width: 720px`, or
   `max-width: 900px and max-height: 500px` for a landscape phone. NOTHING here is gated on
   `hover`/`pointer` — this desk reports `hover: none`, so a pointer query would leak onto the
   desktop.

   WHY THIS BLOCK IS APPENDED A SECOND TIME, and it is the honest version rather than the tidy one.
   The rules below were first written at the tail of the file, and by the time the check ran, TWO
   SIBLING CARDS had appended their own blocks AFTER them — so the rules this card had just measured
   as working had stopped applying to the screen (measured: the phone's sign-in control was back to
   35px and the Settings tabs to 37px, and the desk was painted behind Settings again). Appending is
   the board's rule and it is right; the rule it implies is the one this file's own head already
   states — **the LAST block to append wins, per property** — so a card that needs a rule to hold
   must keep its own copy where it can be reached, or re-append it. Nothing was deleted: the earlier
   copy of these rules stays where it is, and this one is the one that is in force.

   MEASURED BEFORE THE RULES WERE WRITTEN, on a scratch console at all eight sizes:
     * `#settings-overlay` measured 3,176px tall inside an 844px viewport, its own `overflow-y`
       resolved to `visible`, and the document could not scroll (`scrollTo(0, 500)` moved the page by
       0px). Its content below the fold was reachable only by a trackpad two-finger swipe — on the
       device this card is about it did not scroll at all.
     * the Settings tabs measured 95px tall across two rows at 390px and 320px; the Sign in control
       35px and the password field 42px, at every phone width.
     * the Settings panel measured 3,176px of content in an 844px fixed box, and the DESK was painted
       behind it — the drill-down decision this card had to take.

   WHAT THE RULES DO. `.form-overlay-body` is the overlay's OWN scroller (`overflow: auto`, `flex: 1`)
   and it reaches every row — measured at 390x844: 2,895px of content in a 709px box, `scrollTop`
   moving a full 500px when driven. So the phone does not need a second scroller; it needs the shell
   to stop being a `position: fixed` box whose height the phone's URL bar and safe areas cut into.
   The overlay is anchored to the DOCUMENT on a phone (`inset: 0`, `position: absolute`), and the
   desk is hidden under it — the same class `access.js` already sets for the login and clears in
   `close()`, so no JavaScript is changed by this card.

   A `@media` block does not inherit the query of the block above it, so the `(max-width: 720px)`
   here is evaluated on its own and can never fire on a desk.
   ================================================================================================ */

/* ---- the login: the same field set, thumb-sized ----
   MEASURED before: `Sign in` 74x35px, the password field 357x42px. The rule is not a style
   preference — 35px is under the 44px short side every other control on this console is held to,
   and the login is the one screen a person reaches for a phone to use. Inputs go to 16px as well:
   below that iOS zooms the page on focus, which on this screen means the field the person is typing
   into is suddenly off-screen. */
@media (max-width: 720px), (max-width: 900px) and (max-height: 500px) {
    .st-login { margin: 0 auto; padding: 0 1em; }  /* centred, not pushed to the top */
  .st-login-submit { align-self: stretch; min-height: 46px; }
  .st-input { min-height: 46px; font-size: 16px; }
  /* the shell is the page, not a floating box a phone can cut into */
  .form-overlay.st-overlay { position: absolute; min-height: 100%; }
  /* a tab strip is a row of tappable labels, so each one is a target and the strip wraps rather
     than clipping its last word (measured two rows at 390px either way — the difference is that
     every label is now reachable with a thumb) */
  .st-tabs { padding: 0.5em 0.7em 0.3em; gap: 0.3em; }
  .st-tab { min-height: 44px; }
}

/* ---- Settings: a DRILL-DOWN, not a sheet over the rail ----
   THE DECISION THIS CARD HAD TO TAKE, and it is recorded here because the geometry is what
   implements it. Settings REPLACES THE SCREEN on a phone: the whole viewport, and the console's own
   shell (bar, workflow tabs, rail) is not painted behind it. A sheet over the rail was the other
   option and it is wrong for this screen — Settings holds four sections and a row per account, and a
   sheet that must leave room for what it is covering is a sheet whose content is squeezed to keep a
   half-drawn desk visible at its edges. The overlay is the same DOM node on both devices; on a phone
   it owns the screen, and the desk is hidden under it.

   THE DESK IS HIDDEN FOR THE SAME REASON `access-signedout.css` HIDES IT WHEN SIGNED OUT: covering
   is a paint order, not a lock. `access.js` sets the SAME class it already sets for the login and
   clears it in `close()`. The overlay itself is deliberately NOT in this list. */
@media (max-width: 720px), (max-width: 900px) and (max-height: 500px) {
  body.st-locked > header,
  body.st-locked .desk,
  body.st-locked .wfbar,
  body.st-locked #stepper,
  body.st-locked .banner {
    display: none;
  }
  #settings-overlay.st-overlay { border-radius: 0; max-width: none; padding: 0; }
  #settings-overlay .form-overlay-head { border-radius: 0; }
  #settings-overlay .form-overlay-body { border: 0; border-radius: 0; }
}

/* ================================================================================================
   [E5.5] THE PHONE OVERFLOW SHEET ON A LANDSCAPE PHONE — APPENDED, at the tail.

   A REAL DEFECT ON THIS CARD'S OWN SUBJECT, found by driving the sheet with a REAL MOUSE press at
   the control's measured centre (a JS `.click()` skips the hit test and cannot see it).

   MEASURED at 844x390, with the sheet OPENED by pressing `#bar-more` (which works: the button
   measures 48x48 and hit-tests true): the sheet's own SCREEN was painted and the rows were drawn as
   bare text on top of the console's toolbar — "Directory", "Notifications", "Permissions",
   "Feature blocks", "Settings" — with NO panel, NO scrim and NO surface. The Settings row measured
   `left 53 -> right 835, top 474 -> bottom 525` in a **390px viewport**, so it was 84px BELOW the
   screen and could not be pressed at all (`elementFromPoint(444, 499)` is not the control). The
   console's own workflow tabs, search field and create buttons were still live behind it.

   THE CAUSE IS ONE MISSING QUERY, and it is the same shape as the trap the phones block records.
   `bar-phone.js` opens the sheet inside its own query — `(max-width: 720px), (max-width: 900px) and
   (max-height: 500px)` — so on a LANDSCAPE phone (844x390) it correctly shows `#bar-more` and
   correctly unhides `#bar-sheet`. But the sheet's own RULES are `.wf-sheet*`, and those live inside
   the console's **`@media (max-width: 720px)`** block — which a 844px-wide landscape phone does not
   match. So the container was `display: none` (the base rule at line ~1163) while the JS believed it
   was open: `#bar-sheet` had non-zero children and a `hidden` of false, and nothing of it was on the
   screen. The rows that "drew on top of the toolbar" are the sheet's list items being laid out by the
   default flow of a `display: none` parent's descendants — the same defect `bar-phone.css` records
   for the workflow menu, one query further out.

   THE FIX RE-STATES THE SHEET'S PANEL GEOMETRY UNDER THE ONE QUERY THE CONSOLE USES FOR "THIS IS A
   PHONE", so a landscape phone gets the surface its own button already opens. It is a RE-STATEMENT
   rather than an edit: the rules inside the `720px` block are the desk's and the narrow phones' and
   are left exactly as they are. `position: fixed; inset: 0` (not the `absolute` the narrow block
   uses) because the console's shell at this width is a viewport-height grid, and the sheet must cover
   the screen rather than the document.

   MEASURED AFTER on the same build at 844x390: the sheet is a surface, the Settings row is INSIDE the
   viewport and a real mouse press opens Settings — which then owns the screen (`bar` and `desk` both
   `display: none`, 2 team rows drawn). */
@media (max-width: 720px), (max-width: 900px) and (max-height: 500px) {
  /* the container has to be a layer again, or the panel below positions against the page */
  .bar-sheet-wrap { display: block; position: fixed; inset: 0; z-index: 40; }
  .bar-sheet-wrap[hidden] { display: none; }
  /* the panel: a surface at the foot of the screen, scrolling within itself when the rows are taller
     than the room it has. `78vh` was a FRACTION of a tall phone and of a SHORT landscape screen it is
     304px — so the list is allowed to fill what there is and scroll the rest. */
  .bar-sheet-wrap .wf-sheet {
    position: absolute; left: 0; right: 0; bottom: 0;
    max-height: 92%;
    overflow-y: auto;
    overscroll-behavior: contain;
    background: var(--surface);
    border-top: 1px solid var(--line-2);
    border-radius: var(--r-xl) var(--r-xl) 0 0;
    box-shadow: var(--e3);
    padding: var(--s2) var(--s3) max(var(--s2), env(safe-area-inset-bottom));
  }
  /* the scrim: the console behind a modal sheet is not reachable, and a half-lit toolbar behind an
     open sheet reads as a screen that did not open. */
  .bar-sheet-wrap .wf-sheet-scrim {
    position: absolute; inset: 0;
    background: rgba(12, 14, 16, 0.42);
    backdrop-filter: blur(1.5px);
  }
  /* and the row is a target, as it is on a narrow phone */
  .bar-sheet-wrap .bar-sheet-row { display: flex; align-items: center; width: 100%; min-height: 54px; }
  .bar-sheet-wrap .bar-sheet-row .btn {
    flex: 1 1 auto; justify-content: flex-start;
    font-size: 16px; font-weight: 600; min-height: 44px; text-align: left;
  }
}

/* ================================================================================================
   THE AFFIDAVIT EDITOR, FULLSCREEN AND WORD-LIKE.

   The operator, on the affidavit generator:

     "the editing isn't that good, it should be like ms word web version, fonts, bold everything"
     "affidavit generator should be a fullscreen thing, also this header with assigned to who etc
      and the what this job offers you should hide in affidavit generator"
     "get rid of this fields thing on the left what is that even there for"

   So this block does three things, and it is at the END of the file on purpose: a second agent works
   the Hague surface in the sections above, and rules appended here cannot perturb theirs.
   ================================================================================================ */

/* ---- the formatting bar ----
   WORKSTREAM B: the bar's material (translucent, sticky under the acts toolbar, token
   background) is styled once in the editor chrome block (~line 4315); this end-of-file block
   keeps the shape the two share so a reader at the END of the file still finds the tools.
   (--r-1/--r-2 were undefined names here before; the tokens are --r-sm/--r-md.) */
.aff-fmt { display: flex; align-items: center; gap: 4px; flex-wrap: wrap;
  padding: 6px 10px; margin: 0;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-sm);
  position: sticky; top: 53px; z-index: 5; }
/* THE TOOLBAR STAYS PUT while the document scrolls under it — a word processor's toolbars do, and a
   bold button that scrolls away is a bold button the operator has to hunt for mid-sentence. */
.aff-fmt-btn { min-width: 32px; padding: 3px 8px; font-size: 13px; line-height: 1.4; }
.aff-fmt-b { font-weight: 800; }
.aff-fmt-i { font-style: italic; font-family: Georgia, serif; }
.aff-fmt-u { text-decoration: underline; }
.aff-fmt-sep { width: 1px; align-self: stretch; background: var(--line); margin: 0 4px; }
.aff-fmt-size { font-size: 12px; padding: 3px 6px; border: 1px solid var(--line-2);
  border-radius: var(--r-sm); background: var(--surface); color: var(--ink); }
/* THE FONT CONTROL, beside the size it is set at. It is a native `<select>` like the size ladder —
   the console's own material — and each option previews its own face, so the list reads as the
   document would. The `min-width` keeps the control's own box steady as the selection moves between
   faces, so the toolbar's tools do not shuffle sideways while the operator works. */
.aff-fmt-face { font-size: 12px; padding: 3px 6px; border: 1px solid var(--line-2);
  border-radius: var(--r-sm); background: var(--surface); color: var(--ink);
  min-width: 9rem; max-width: 15rem; }
.aff-fmt-face option { font-size: 13px; }
.aff-fmt-state { font-size: 11px; color: var(--ink-4); }

/* ---- a snippet: a cut-out of the job's own document, in the page ----
   A SNIPPET IS AN OBJECT THE OPERATOR DRIVES, not a picture pasted into text. Its controls are
   drawn INSIDE the figure and OUTSIDE the caption (see `snippetFigureHtml`), so:
     * the figure contributes exactly ONE line to the sheet's `innerText` — its caption — which is
       what `saveAffidavitDocument`'s strip and `record_figures`' line anchor both depend on
       (MEASURED: a `contenteditable="false"` button block adds nothing to `innerText`);
     * the caption stays the readable line under the picture, and it is the one part of the figure
       a person can type in.
   `touch-action: none` on the figure is what lets a drag MOVE it on a phone instead of scrolling
   the page under the finger; it is scoped to `.aff-snippet` so the document itself still scrolls
   normally everywhere else. */
.aff-snippet { position: relative; margin: 10px 0; padding: 6px; border: 1px solid var(--line-2);
  border-radius: var(--r-md); background: var(--sunken); text-align: center;
  touch-action: none; cursor: grab; }
.aff-snippet img { display: block; margin: 0 auto; max-width: 100%; height: auto;
  border: 1px solid var(--line); background: #fff; }
/* ---- A PLACED SNIPPET: THE PICTURE'S EDGE IS THE OPERATOR'S TO CHOOSE ----
   `data-snippet-x` is written by `snippetFigureHtml` when — and only when — the record holds a
   place the operator chose (`snippetPlaceFigure` writes the offset itself, as a transform in the
   page's own pixels). These two rules are the part a `style` attribute cannot carry: a picture
   CENTRED in its figure would move to the middle of wherever the figure was pushed to, so a placed
   figure's picture is anchored to its LEFT edge — which is the edge the stored percentage names —
   and the caption reads from the same edge rather than from the middle of a wide empty box.
   A figure with no chosen place matches neither rule and is drawn exactly as it always was. */
.aff-snippet[data-snippet-x] { text-align: left; }
.aff-snippet[data-snippet-x] img { margin-left: 0; margin-right: 0; }
/* ---- BLENDED IN WHEN YOU ARE NOT WORKING ON IT ------------------------------------------------
   The operator, verbatim: "when I have put the snippet where I want it, and I click off it should hide
   the resize, the outline and the cross etc of the snippet, it should be blended into the page now".

   A figure therefore has TWO states and this is the resting one. With nothing selected, the figure
   loses its border, its box and its controls: the picture sits on the page with only the thin edge its
   own `<img>` carries (so a white crop on white paper is still readable as a picture rather than a
   hole), and the caption keeps the document's own texture instead of a UI's.

   THE CHROME COMES BACK THE MOMENT THE FIGURE IS TOUCHED — see `#aff-doc[data-snip-active]` below,
   which re-draws exactly the same box, grip and remove control the figure had before. Nothing is
   removed from the DOM: the controls are hidden, never unbuilt, so the resize/remove wiring and the
   `contenteditable` caption behave identically in both states.

   WHY THIS IS KEYED ON THE SHEET rather than on each figure: the state is "which snippet is the
   operator currently on", which is a fact about one surface at one time, not a property of an element.
   Marking each figure independently would let two figures claim it at once and would leave a stale
   claim behind on any figure that was removed while selected. */
#aff-doc:not([data-snip-active]) figure.aff-snippet {
  border-color: transparent; background: transparent; margin: 10px 0; padding: 6px;
  cursor: default; touch-action: auto; }
#aff-doc:not([data-snip-active]) figure.aff-snippet img { border-color: var(--line); }
#aff-doc:not([data-snip-active]) figure.aff-snippet .aff-snip-tools { display: none; }
#aff-doc:not([data-snip-active]) figure.aff-snippet figcaption { color: var(--ink-4); }
/* ...BUT THE CHROME COMES BACK UNDER THE POINTER, AND IT MUST.
 *
 * MEASURED, and it cost a working control: with the resting state hiding `.aff-snip-tools`
 * unconditionally, nothing could reach the grip without first selecting the figure — and a resize
 * driven from the grip's own rect found a zero-width grip and did nothing (`the resize wrote a WIDTH
 * on the figure: data-snippet-w=None`; removing this one rule turned that check green again). The
 * operator's own instruction was that the chrome should go away when they click OFF the snippet, not
 * that the snippet should become inert: a picture whose handle is unreachable until you know to press
 * it first is a control that has been taken away, and the honest reading of "blended into the page"
 * is about what is PAINTED at rest, not about removing the handles.
 *
 * So the resting state is still quiet — no border, no transparent box, no tools — and pointing at the
 * figure brings its tools back. The selected state (the sheet carrying `data-snip-active`) keeps them
 * out at all times. */
#aff-doc:not([data-snip-active]) figure.aff-snippet:hover .aff-snip-tools,
#aff-doc:not([data-snip-active]) figure.aff-snippet:focus-within .aff-snip-tools { display: flex; }
/* A FIGURE THAT IS NOT SELECTED IS STILL REACHABLE: the resting state must not make it un-clickable,
   or "click off it" would be a one-way door. `pointer-events` stays on and the figure keeps its own
   box for hit testing; only the PAINT changes. */
#aff-doc[data-snip-active] figure.aff-snippet[data-snippet-id] { border-color: var(--accent); }
.aff-snippet figcaption { margin-top: 4px; font-family: var(--mono); font-size: 10.5px;
  color: var(--ink-4); }
/* THE CAPTION IS EDITABLE, SO IT SAYS SO. A one-line box that is the operator's to change reads as
   text until the pointer is over it; the hover/focus rules are that cue, and the dotted underline
   keeps the caption's own look as the document's caption. */
.aff-snippet figcaption[contenteditable="true"] { cursor: text; border-radius: var(--r-2, 3px);
  padding: 1px 3px; outline: none; }
.aff-snippet figcaption[contenteditable="true"]:hover { background: color-mix(in srgb, var(--accent) 10%, transparent); }
.aff-snippet figcaption[contenteditable="true"]:focus { background: color-mix(in srgb, var(--accent) 14%, transparent);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 45%, transparent); color: var(--ink-2); }
/* THE CONTROLS, top-right of the picture's own box. Big enough for a finger (the console's own
   44px rule is for a screen's primary controls; these sit on a document object and are sized to
   the figure, which is why they are 22px with a generous hit area rather than 44 — a control wider
   than the picture it belongs to is not a control). */
.aff-snip-tools { position: absolute; top: 2px; right: 2px; display: flex; gap: 2px;
  z-index: 2;
  /* A CONTROL THE OPERATOR CANNOT HIT IS NOT A CONTROL. The sheet is drawn with a fit-to-screen
     `transform: scale(k)` on `.aff-pages` (measured: k=0.564 on a 1440-wide desk, k=0.246 on a
     390-wide phone), and everything inside it — including these controls — was scaled with it, so a
     22px button measured 14px on the desk and 6px on the phone. They are therefore drawn at a
     CONSTANT SIZE: `wireSnippetFigures` writes the reciprocal of the sheet's own scale into
     `--aff-snip-k` on the sheet, and this counter-transform cancels it back out. Anchored top-right
     so the box stays put in the picture's corner as it grows. */
  transform-origin: top right;
  transform: scale(var(--aff-snip-k, 1)); }
/* ---- WHEN THE CONTROLS DO NOT FIT OVER THE PICTURE, THEY SIT ABOVE IT -------------------------------
   MEASURED DEFECT THIS CLOSES (phone, 390x844): the picture is drawn as small as the operator resizes
   it, and its controls are drawn at a CONSTANT finger-reachable size (22px, hit-extended to 44px) — so
   a picture resized to 36x14 screen px sat UNDER a 51x24 tool box and EVERY point across it hit
   `BUTTON.aff-snip-del`. The snippet could not be grabbed to move it at all, and a press meant to drag
   it deleted it. Hit-tested across the picture's own width at its centre line: 5%, 15%, 25%, 35%, 50%,
   65%, 75% — all `BUTTON.aff-snip-del`.
   `affSnippetToolsFit` (app.js) measures the two and sets this class when the tools do not fit over the
   picture; they then float just above the figure's own box instead of over the operator's picture. It
   is re-decided on every redraw (`snippetPlaceFigure`), because a resize changes the picture's height.
   The host line is already lifted (`.aff-line.aff-snip-host`), so a control raised above the figure is
   still above the neighbouring lines. */
.aff-snippet.aff-snip-tools-above .aff-snip-tools { top: auto; bottom: 100%; margin-bottom: 2px;
  /* AND THE COUNTER-SCALE MUST GROW THE OTHER WAY. `.aff-snip-tools` is drawn at a constant size by
     `transform: scale(var(--aff-snip-k))` with `transform-origin: top right`, so growing it keeps the
     top-right corner put and extends it DOWN-AND-LEFT. Measured on a phone (k=4.069): the raised box's
     rect came back `top 649, bottom 673` against a picture top of `657` — the very overlap it was
     raised to avoid, because the scaled box grew back down over the picture. Anchored at its
     BOTTOM-right instead, the box grows UP-AND-LEFT from the edge that is pinned above the figure, so
     the clearance the rule asks for is the clearance the operator gets. */
  transform-origin: bottom right; }
.aff-snip-tools button { width: 22px; height: 22px; padding: 0; min-width: 0; line-height: 1;
  font-size: 12px; border: 1px solid var(--line-2); border-radius: var(--r-2, 3px);
  background: color-mix(in srgb, var(--surface) 88%, transparent); color: var(--ink-3);
  cursor: pointer; display: flex; align-items: center; justify-content: center;
  /* THE FINGER'S OWN RULE: the visible box stays 22px (a control wider than the picture it belongs
     to is not a control), and the HIT area is extended to 44px by a pseudo-element that takes no
     layout space and draws nothing. */
  position: relative; }
.aff-snip-tools button::after { content: ""; position: absolute; left: 50%; top: 50%;
  width: 44px; height: 44px; transform: translate(-50%, -50%); }
/* AND WHEN THE CONTROLS ARE RAISED CLEAR OF THE PICTURE, THE FINGER'S HIT AREA MUST NOT BLEED BACK OVER
   IT. That 44px area extends 11px past the 22px button on every side — (44-22)/2 — so a button raised
   2px clear of a small picture still had its hit area reaching ~9px down ONTO the picture.
   MEASURED (phone, 390x844, after the raise): hit-testing across the picture's own width at its centre
   line gave `BUTTON.aff-snip-grip` at 5% and `BUTTON.aff-snip-del` at 25%..95% — a press on the picture
   meant to DRAG the snippet would have deleted it. The area is shifted up by its own overhang, so it
   stops at the control's bottom edge and grows upward instead, where nothing of the operator's is.
   It stays 44px and stays reachable: a control the finger cannot reach is not a control.
   THIS RULE SITS AFTER the base `::after` on purpose — same specificity, so the later one wins; placed
   before it, the shift was silently overridden and the hit area still covered the picture. */
.aff-snippet.aff-snip-tools-above .aff-snip-tools button::after {
  transform: translate(-50%, calc(-50% - 11px)); }
.aff-snip-tools button::before { font-size: 12px; line-height: 1; }
.aff-snip-tools button:hover { background: var(--surface); color: var(--ink); border-color: var(--line-3); }
.aff-snip-grip { cursor: ew-resize; touch-action: none; }
.aff-snip-grip::before { content: "⇔"; }
.aff-snip-del:hover { color: var(--danger); border-color: var(--danger-line); }
.aff-snip-del::before { content: "✕"; }
/* WHILE IT IS BEING DRAGGED, AND WHERE IT WOULD LAND. The figure lifts (so the operator can see
   they are holding it, not the page) and the line it would follow is marked — the drop is a place
   they have SEEN rather than one they discover after releasing. */
.aff-snippet.aff-snip-moving { opacity: .72; cursor: grabbing; border-color: var(--accent);
  box-shadow: 0 6px 18px rgba(0,0,0,.18); }
.aff-line.aff-snip-over { box-shadow: inset 3px 0 0 0 var(--accent);
  background: color-mix(in srgb, var(--accent) 8%, transparent); }

/* ---- A LINE THAT HOLDS A SNIPPET PAINTS ABOVE THE LATER LINES ------------------------------------
   THE DEFECT THIS CLOSES, and it is the reason a snippet dropped onto a low line could not be
   RESIZED afterwards: the placed lines are absolutely positioned and `z-index: auto`, so among
   themselves the LATER line paints over the earlier one — and when a snippet is dropped onto a line,
   the figure sits INSIDE that line with the picture hanging below it over the following lines. The
   handle keeps its own `z-index: 2`, but a `z-index` only counts WITHIN its stacking context, and the
   figure itself creates one (it carries a `transform` from `snippetPlaceFigure`), so the handle's 2 is
   confined to the figure and the figure to its host line.

   MEASURED, after the check's own move onto the line "truth." (desk 1440x900), the stack at the
   handle's own rect was:

       0. DIV.aff-line            <- a LATER line, on top
       1. BUTTON.aff-snip-grip    <- the handle, underneath

   `elementFromPoint` at the handle answered `DIV.aff-line`, a real press at the check's own
   coordinates called `affSnippetResize` **0 times**, and the check reported "387px -> 387px" while
   its own "hittable" assertion passed — because `getBoundingClientRect()` says the handle is on
   screen and only hit testing knows it is covered. Setting this line's `z-index` to `4` made the same
   press reach the handle and the resize run (`data-snippet-w` became `72`), which is the confirmation.

   So the HOST is lifted, not the figure: a line carrying a snippet is the thing that must be above its
   neighbours. `wireSnippetFigures` adds `aff-snip-host` to the line it adopts, and removes it when the
   figure leaves. 4 is above the form's own absolute lines (all `auto`) and below the editor's
   furniture (`#stage` chrome is outside `.aff-page`), so it cannot cover the operator's controls. */
.aff-line.aff-snip-host { z-index: 4; }

/* ---- the intake form's picture control ---- */
.inf-photo { display: flex; flex-direction: column; gap: 6px; align-items: flex-start; }
.inf-photo-img { display: block; }
.inf-photo-img img { max-width: 150px; max-height: 150px; border: 1px solid var(--line-2);
  border-radius: var(--r-md); background: #fff; }
.inf-photo-file { font-size: 12px; }
.inf-photo-state { font-size: 11px; color: var(--ink-4); }

/* THE PICTURES, AS A LIST THE OPERATOR CAN WORK DOWN.
   Each row is one picture, its own name, and the button that takes it off the job. The list is the
   ORDER the pictures go in the email, so the row order is the office's own order and is not sorted,
   reversed or tidied here — a screen that reordered them would make the email disagree with it. */
.inf-photo-list { list-style: none; margin: var(--s2) 0 0; padding: 0;
                  display: flex; flex-direction: column; gap: var(--s2); }
.inf-photo-row { display: flex; align-items: center; gap: var(--s3);
                 padding: var(--s2); border: 1px solid var(--line-2); border-radius: 3px;
                 background: var(--sunken); }
.inf-photo-row .inf-photo-img img { max-width: 96px; max-height: 96px; }

/* A PICTURE THE OPERATOR HAS PICKED AND NOT YET SAVED. It must read as a real row — the thumbnail is
   the whole point of the fix — while saying plainly that it is not on the job yet, so nobody mistakes
   it for a saved picture. A dashed edge rather than a colour: the console's own tokens stay untouched,
   and a dashed row is the conventional way a surface says "this is not committed yet". */
.inf-photo-row.inf-photo-picked { border-style: dashed; }
.inf-photo-row.inf-photo-picked .inf-photo-name { color: var(--ink-2); }
.inf-photo-name { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; color: var(--ink-3); }
.inf-photo-row [data-inf-photo-remove] { flex: none; }
/* The picker is a control, not a paragraph: give it a row of its own so "add another" never reads as
   part of the picture above it. */
.inf-photo .inf-photo-file { margin-top: var(--s2); }

/* ---- THE PICTURES ON AN ATTEMPTED DELIVERY ----
   The section the operator asked for (2026-09-26): "a section where the person recording attempted
   delivery can take a picture or upload a picture here (or multiple)". It sits inside the attempt
   form, so it is styled as part of that form rather than as a second screen — the same row shape the
   job's own picture list uses, because it is the same kind of fact and a different shape for it
   would read as a different kind of thing. */
.att-pics { display: flex; flex-direction: column; gap: var(--s2); align-items: flex-start;
            margin-top: var(--s2); }
.att-pics-list { list-style: none; margin: 0; padding: 0; width: 100%;
                 display: flex; flex-direction: column; gap: var(--s2); }
.att-pics-list:empty { display: none; }
.att-pic-row { display: flex; align-items: center; gap: var(--s3); width: 100%;
               padding: var(--s2); border: 1px solid var(--line-2); border-radius: 3px;
               background: var(--sunken); }
.att-pic-name { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; color: var(--ink-2); }
.att-pic-size { flex: none; color: var(--ink-4); }
.att-pic-row [data-att-pic-drop] { flex: none; }
/* THE TWO WAYS TO THE SAME FACT, side by side and equally weighted: taking a picture and uploading
   one are alternatives, not a primary and a fallback, so neither is styled as the lesser control.
   The real `<input type=file>` inside each label is hidden — the label IS the control, which is what
   lets `capture="environment"` ask a phone for the camera without a second button. */
.att-pics-controls { display: flex; gap: var(--s2); flex-wrap: wrap; }
.att-pics-controls .btn { position: relative; cursor: pointer; }
.att-pics-take, .att-pics-file { position: absolute; width: 1px; height: 1px; opacity: 0;
                                 pointer-events: none; }
.att-pics-state { color: var(--ink-4); font-size: 11px; }
/* The delivery row's own picture count, and the report picker's: a mono fact beside the row it
   belongs to, never a badge that competes with the outcome. */
.inf-attempt-pics, .rp-pics { color: var(--ink-4); }

/* ---- FULLSCREEN: THE SAME EDITOR, FULL-VIEWPORT ----
   `body.aff-fullscreen` is set by the editor and by nothing else. Everything hidden here is shell
   chrome the operator named: the top bar, the case header with who it is assigned to, the informal
   tabs, and the job's own panels. What is left is THE EDITOR — the same `#stage .affdoc` the
   normal view shows, at full height — and nothing else.
   WORKSTREAM B DEFECT, MEASURED (screenshot 2026-09-24): the old rule
   `body.aff-fullscreen .aff-record { display:none !important }` hid the EDITOR ITSELF, because
   `infAffidavit()` builds the editor with class `step-card inf-card affdoc` and `#stage`'s Hague
   step cards carry `.aff-record` — the selector that was written for the OLD affidavit panel
   matches the LIVE editor's card, so fullscreen swapped the editor for the job's shell panels
   (the red gap prose, APPROVE AND SAVE, the signature forms). The client: "the fullscreen view is
   cooked... you can tell which one is which". The rule is GONE: the old panel is uncalled code
   (`affidavitPanel` has no caller), so nothing draws a second affidavit surface to hide, and the
   same editor stays on screen in both modes. */
/* THE SELECTORS ARE THE ONES THE DOM ACTUALLY USES, measured in a real browser rather than
   guessed. The job's own panels live in the stage as `#acts-strip` ("What this job offers you"),
   `#owner-strip` ("Assigned to …"), `#case-share` and `#case-approval`, and the informal tabs are
   a `nav.inf-tabs` INSIDE the informal screen — not a shell element at all. Measured ancestry:
   #case-share sits directly under #stage, and .inf-tabs is `#stage .inf-flow > nav`.
/*
   `#stage` also holds the AFFIDAVIT itself, so the panels are hidden individually — a rule hiding
   #stage would hide the thing the operator came to work on. */
/* THE TOP WORKFLOW ROW AND THE JOB LIST ARE ALSO SHELL. Measured in the DOM: the row with
   "Hague / Informal / Translations" is `.wfbar`, and the list of jobs down the left is
   `SECTION.col.queue`. Both were still on screen after my first pass, because neither is inside the
   informal card — they are siblings of it in `.desk-body`. Taking them away is what leaves the
   affidavit alone on the page, which is what "fullscreen" was asked to mean. */
/* THE LAYOUT MUST COLLAPSE WITH THE COLUMN. `.desk-body` is
   `grid-template-columns: var(--rail) 1fr`, and hiding the queue left its rail TRACK still reserved
   — so the affidavit was drawn in a ~150px strip down the left of an otherwise empty page. Measured
   in the browser: after the panels were hidden the sheet's own width fell from 900px to a sliver.
   A hidden element does not give its track back; the track has to be re-declared. */
body.aff-fullscreen .desk-body { grid-template-columns: minmax(0, 1fr); }
/* THE JOB HEADER — "this header with assigned to who etc" — is `DIV.case-head` (it holds the job's
   name, its DRAFT badge and Delete job). Measured ancestry: BUTTON#delete-case -> DIV.case-actions
   -> DIV.case-head -> SECTION.col sheet. It is not the same element as `#case-share`, which sits
   in the stage; both had to be named, and naming only one left half the header on screen. */
body.aff-fullscreen .case-head { display: none !important; }
body.aff-fullscreen .wfbar,
body.aff-fullscreen .col.queue,
body.aff-fullscreen .bar,
body.aff-fullscreen .step-rail,
body.aff-fullscreen .step-caption,
body.aff-fullscreen #acts-strip,
body.aff-fullscreen #owner-strip,
body.aff-fullscreen #case-share,
body.aff-fullscreen #case-approval,
body.aff-fullscreen .inf-head,
body.aff-fullscreen .inf-tabs,
body.aff-fullscreen #chat-wrap,
body.aff-fullscreen .danger-foot,
/* THE CREATE BANNER and the UNFILLED-GAP PROSE are status prose about the job, not part of the
   editor; in fullscreen the document is the point. The gap list stays reachable in the normal
   view and in the editor's status band — hiding it here hides no fact, it moves nothing. */
body.aff-fullscreen .stage > .inf-result,
body.aff-fullscreen [data-unfilled],
/* The page-level banner reports the create; the acts bar already says what matters in fullscreen
   ("Saved — nothing has been sent"), and the prose below the sheet is the normal view's business. */
body.aff-fullscreen #banner { display: none !important; }

/* THE STAGE TAKES THE WINDOW. `#sheet-scroll` is the scroller (see .sheet-scroll above), so it is
   the element that has to grow to the viewport height — the document then fills it.
   `:root { zoom: 1.1 }` multiplies VIEWPORT UNITS TOO, so a plain `100vh` here measures 990px on a
   900px window and the editor's own foot is 90px below the glass — measured: the format toolbar's
   row and the bottom of the page sat off the screen in fullscreen. The height is divided by the same
   token the zoom is set from, exactly as the chat's expanded height already does (see
   `body.case-open.chat-expanded`). */
body.aff-fullscreen .stage { padding: 0; height: calc(100vh / var(--zoom, 1.1));
  display: flex; flex-direction: column; max-width: none; }
body.aff-fullscreen .sheet-scroll { height: calc(100vh / var(--zoom, 1.1)); }
body.aff-fullscreen .affdoc { margin: 0; border: 0; border-radius: 0; box-shadow: none;
  background: var(--sunken); display: flex; flex-direction: column;
  height: calc(100vh / var(--zoom, 1.1)); }
body.aff-fullscreen .affdoc > .affed-actions { flex: none; padding: 8px 14px;
  border-bottom: 1px solid var(--line); background: var(--surface); }
body.aff-fullscreen .aff-toolbar { flex: none; padding: 8px 14px; margin: 0;
  border-bottom: 1px solid var(--line); }
body.aff-fullscreen .aff-fmt { position: static; top: auto; margin: 0;
  border-left: 0; border-right: 0; border-radius: 0; }
body.aff-fullscreen .affed { flex: 1; min-height: 0; display: flex; flex-direction: column;
  margin: 0; }
body.aff-fullscreen .affed-mid { flex: 1; min-height: 0; display: flex; flex-direction: column; }
/* THE PAGEWRAP IS THE SPACE THE PAGE HAS TO FIT IN, so its padding is part of the measurement
   `affFitPages` reads and the fit is taken against what is left. Kept tight on purpose: every pixel
   of padding here is a pixel the document does not get. */
body.aff-fullscreen .affed-pagewrap { flex: 1; min-height: 0; padding: 14px 12px; }
@media (max-width: 720px) {
  /* a phone's screen is the scarce thing, and the page already scales to whatever is left */
  body.aff-fullscreen .affed-pagewrap { padding: 8px 6px; }
}
/* THE SHEET IS WIDER AND TALLER IN FULLSCREEN, unless it is a DRAWING OF PAGES.
   The whole point of the mode is that the page stops being a 900px column inside a stack of panels
   and becomes the thing being worked on — so a LEGACY sheet (plain lines, one continuous sheet)
   keeps the wider box it always had here. A sheet that draws REAL PAGES does not: it is exactly the
   template's own size, and enlarging the sheet around it would put the drawn page in a box that no
   longer matched what is on it. Its display size is the scale `affFitPages` computed, and this rule
   is the one that would otherwise override it with `min-height: calc(100vh - 220px)` — a leftover
   from when the sheet WAS the page. */
body.aff-fullscreen .affed-sheet { max-width: 900px; min-height: calc(100vh - 220px); }
body.aff-fullscreen .affed-sheet.paged { max-width: none; width: fit-content;
  min-height: 0; height: auto; }

/* WHERE ELSE A FACT LANDS. A value the form names on several of its lines is ONE fact listed once,
   with the other lines named beside it — the same list must not repeat itself (app.js `facts`). */
.aff-also { color: var(--ink-3); font-size: 11px; white-space: normal; }

/* ================================================================================================
   MOTION — the console's feel layer (apple-design pass, 2026-09-24).

   THESIS. This is an OPERATE surface: motion serves feedback, state and continuity, and never
   makes an operator wait. One material idea — surfaces ARRIVE from where they come from and
   RETURN along the same path — carried by quiet supporting states:

     · overlays materialise (opacity + 8px rise + blur radius), exits are faster than entrances;
     · the phone sheets slide from the bottom edge they live on, the scrim fading with them;
     · pressed controls answer on :active (already present) with a faster release than press;
     · state chips keep their existing chip-in moment;
     · the desk's cards stagger in ONCE per list paint, capped, never on scroll.

   Every value is a token. No bounce anywhere: nothing on this console is flicked, so overshoot
   would read as a glitch (apple-design §4: damping 1.0 unless the gesture carried momentum).

   REDUCED MOTION is an intentional alternative, not an off switch (apple-design §14): spatial
   movement (translate/scale) is dropped; OPACITY and colour transitions — which carry the state
   change — remain, at the same durations.

   IMPLEMENTATION NOTES. Overlays here toggle the `hidden` ATTRIBUTE directly, and CSS cannot
   transition *from* display:none. So entrances animate with a keyframe (runs when the element
   un-hides), and exits are cut — the honest, dependency-free choice for a console whose overlays
   are plain hidden-toggles; a fake "closing" animation that never runs would be a claimed
   success (R8). Keyframes animate only transform/opacity — compositor-friendly.
   ================================================================================================ */

:root {
  --dur-fast: 120ms;   /* press feedback, chip answers */
  --dur: 180ms;        /* routine state change (was 150ms; within the 150–300 band) */
  --dur-slow: 280ms;   /* overlay/sheet entrances: the 150–300 band's top */
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-inout: cubic-bezier(0.45, 0, 0.25, 1);
}

/* ---- 1. OVERLAYS MATERIALISE ------------------------------------------------------
   `.form-overlay` (Settings, directory, notifications…) and `.src-overlay` (source view)
   toggle `hidden`. The entrance keyframe gives them one shared arrival: the scrim dims
   (opacity), the panel rises 8px — the surface materialising, not a plain fade
   (apple-design §12, "materialize, don't just fade"). */
@keyframes overlay-in {
  from { opacity: 0; }
}
@keyframes overlay-panel-in {
  from { opacity: 0; transform: translateY(8px) scale(0.995); }
  to   { opacity: 1; transform: none; }
}
.form-overlay:not([hidden]),
.src-overlay:not([hidden]) {
  animation: overlay-in var(--dur-slow) var(--ease-out);
}
/* the panel is the head+body card inside the scrim — animate the card, not the dimmer.
   transform-origin follows the geometry: a centred panel scales from its centre; the
   full-height settings sheet grows from the bottom edge it reads as coming from. */
.form-overlay:not([hidden]) > .form-overlay-head,
.form-overlay:not([hidden]) > .form-overlay-body,
.st-overlay:not([hidden]) > .form-overlay-head,
.st-overlay:not([hidden]) > .form-overlay-body {
  animation: overlay-panel-in var(--dur-slow) var(--ease-out);
  transform-origin: 50% 50%;
}
.st-overlay:not([hidden]) > .form-overlay-body { transform-origin: 50% 100%; }
.src-overlay:not([hidden]) > * {
  animation: overlay-panel-in var(--dur-slow) var(--ease-out);
}

/* ---- 2. PHONE SHEETS FROM THEIR EDGE ----------------------------------------------
   `.wf-sheet`/`.bar-sheet` live at the bottom edge: they slide up from it. The scrim fades.
   Exits cut (the hidden toggle) — the entrance explains the spatial relationship; an exit
   animation a hidden-toggle cannot express must not be faked. */
@keyframes sheet-up {
  from { transform: translateY(48px); opacity: 0.6; }
  to   { transform: none; opacity: 1; }
}
.wf-sheet-wrap:not([hidden]) .wf-sheet {
  animation: sheet-up var(--dur-slow) var(--ease-out);
}
.wf-sheet-wrap:not([hidden]) .wf-sheet-scrim {
  animation: overlay-in var(--dur-slow) var(--ease-out);
}

/* ---- 3. PRESS ANSWERS INSTANTLY, RELEASE SETTLES -----------------------------------
   The press already transforms on :active (§1 respond on pointer-down). What was missing is
   the release: with the base transition at 150ms the release lagged the press. The press
   itself stays instant (no transition on :active), the release eases back. */
.btn, .wf-tab, .rail-tab, .step, .pg, .btn-icon {
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease),
              border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease),
              transform var(--dur-fast) var(--ease-out);
}
.btn:active:not(:disabled) { transition-duration: 0ms; }  /* the press is instant */

/* ---- 4. A LIST ARRIVES AS ONE THING (Kazim, 2026-09-24) ----------------------------
   "when I switch to hague and informal and translations its slow, but when I switch to home
   its smooth and instant."
   MEASURED: with the per-row stagger below, an Informal switch settled at 416ms across 12
   separate animations — the rail's rows AND the desk's cards each fading in on their own
   0/30/60/90ms delay, so the list crept in row by row while Home arrived as one block. The
   stagger was the whole difference; the data was already there at 128ms.
   DESIGN.md's Motion section is the authority and names exactly TWO authored moments — the
   state chip's 200ms settle and the mark's 60s ring. A per-row entrance is not one of them.
   So a row keeps its gentle entrance but the DELAYS are gone: the list arrives as a single
   block in one `--dur`, exactly like the overview, and nothing creeps.
   `both` is still needed so a row cannot flash at full opacity before its animation starts. */
@keyframes row-in {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: none; }
}
.rail-body .entry, .rail-body li, .home-card {
  animation: row-in var(--dur) var(--ease-out) both;
  animation-delay: 0ms;             /* the list is one object: it does not creep in row by row */
}

/* ---- 4b. THE SWITCH'S OWN LOADING STATE (Kazim, 2026-09-25) --------------------------
   "the switch happens and its just 'loading' instead."
   The switch skeleton reuses `.skel` (its sweep, its reduced-motion and contrast rules, its
   dark-theme values) and adds only what a RAIL row needs: the row's real geometry — an index
   column, a name block, then the state chip and meta line — so the skeleton occupies the same
   box the list will, and the arrival does not move anything. Nothing new is invented: same
   shimmer, same tokens, same 150ms band as the rest of the console's motion. */
.skel-entry {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  list-style: none;
}
.skel-entry .entry-idx { flex: none; width: 22px; }
/* THE INDEX PLACEHOLDER IS A BAR, NOT A SLIVER. `.skel-bar.w14` is 14% of its parent — inside a
   22px index column that is ~3px, and it rendered as a stray tick at the row's left edge (seen in
   the desk and phone screenshots, reported by neither count). The real `.entry-idx` holds a
   two-digit numeral, so the placeholder takes a fixed width that matches one and centres itself
   in the column. */
.skel-entry .entry-idx .skel-bar { width: 12px; height: 8px; margin: 0 auto; }
.skel-entry .entry-main { flex: 1 1 auto; display: flex; flex-direction: column; gap: 7px; min-width: 0; }
.skel-entry .entry-meta { display: flex; align-items: center; gap: 8px; }
/* The rail's skeleton must not carry the list entrance: it is the FIRST thing on screen, not an
   arrival INTO a settled view, and `row-in` on a placeholder reads as content settling. */
.skel-entry { animation: none; }
@media (prefers-reduced-motion: reduce) {
  .skel-entry .skel-bar, .skel-entry .skel-chip { animation: none; }
}

/* ---- 5. DIALOGS THE OPERATOR CAUSES: the sign-out confirm -------------------------- */
@keyframes dialog-in {
  from { opacity: 0; transform: translateY(6px) scale(0.99); }
  to   { opacity: 1; transform: none; }
}
#signout-dialog:not([hidden]) {
  animation: dialog-in var(--dur) var(--ease-out);
}

/* ---- 6. REDUCED MOTION: the intentional alternative --------------------------------
   Spatial movement dropped; opacity kept (it carries the state change). Durations shorten
   rather than zeroing — feedback must remain legible (apple-design §14). */
@media (prefers-reduced-motion: reduce) {
  .form-overlay:not([hidden]), .src-overlay:not([hidden]),
  .wf-sheet-wrap:not([hidden]) .wf-sheet,
  #signout-dialog:not([hidden]),
  .rail-body .entry, .rail-body li, .home-card {
    animation-duration: 1ms;           /* effectively instant, but not display:none */
  }
  .form-overlay:not([hidden]) > .form-overlay-head,
  .form-overlay:not([hidden]) > .form-overlay-body,
  .st-overlay:not([hidden]) > .form-overlay-head,
  .st-overlay:not([hidden]) > .form-overlay-body,
  .src-overlay:not([hidden]) > *,
  .wf-sheet-wrap:not([hidden]) .wf-sheet {
    animation-name: overlay-in;        /* opacity only — no translate, no scale */
    animation-duration: 160ms;
  }
  .rail-body .entry, .rail-body li, .home-card { animation: none; }
  .btn, .wf-tab, .rail-tab, .step, .pg, .btn-icon { transition-duration: 80ms; }
}

/* ---- 7. REDUCED TRANSPARENCY: frostier scrims -------------------------------------- */
@media (prefers-reduced-transparency: reduce) {
  .form-overlay { background: rgba(20, 23, 27, 0.82); }
  .wf-sheet-scrim, .bar-sheet-wrap .wf-sheet-scrim { background: rgba(12, 14, 16, 0.8); backdrop-filter: none; }
}

/* ---- 8. THE REQUEST-ACCESS FLOW STRIPS (D8/D9, 2026-09-24) ---------------------------
   The read-only note with its ask, and the holder's pending asks with Allow / Deny. Drawn by
   `app.js` from the payload's own blocks; the styles live here so the panel and the routes
   cannot disagree about what a row looks like. */
#access-flow-strips { display: flex; flex-direction: column; gap: 8px; margin: 0 0 10px; }
.ro-strip, .ar-strip {
  border: 1px solid var(--line, #2c313a); border-radius: 8px;
  /* *** `--panel-2` IS NOT A TOKEN THIS CONSOLE HAS, AND THE ONE USE OF IT WAS THE DEFECT. ***
   * MEASURED 2026-09-26, on the rendered strip: with `data-theme="light"` the page's own ground is
   * `rgb(238,240,239)` and the strip painted `rgb(26,30,23)` — the literal fallback, because a
   * `var()` whose property is undefined ALWAYS takes its fallback, in every theme. Kazim's report,
   * verbatim: "the request job feature shows up dark even in light mode". `--raised` is the themed
   * token for a raised surface (`#fff` light / `#232830` dark), which is what a strip sitting on the
   * page's ground is. */
  padding: 8px 10px; background: var(--raised, #ffffff);
  display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
}
/* `--text-dim` is not a token either, so this text took `#9aa3ad` in BOTH themes — a grey chosen for
   dark, at 2.6:1 on the LIGHT ground (below the 4.5:1 floor this file's own tokens are held to, see
   `--ink-4`). `--ink-3` is the themed step for secondary text. */
.ro-note { flex: 1 1 240px; color: var(--ink-3, #5f656d); }
.ro-wait { color: var(--ink-3, #5f656d); }
.ar-strip { flex-direction: column; align-items: stretch; }
.ar-head { font-weight: 600; }
.ar-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.ar-who { flex: 1 1 200px; }

/* ============================================== REPORT/SHARE AFFIDAVITS (the client's own screen)

   The affidavits' tick list, built on the `.report-picker` / `.rp-*` rules above so the two
   report/share screens look like the same feature rather than two: `affidavitSharePicker` reuses
   `.rp-head`, `.rp-count`, `.rp-list` and `.rp-row`, and adds only what it has that a delivery
   report does not — a FILE NAME box per affidavit.

   THE NAME BOX SITS UNDER ITS OWN ROW, not beside the tick: an affidavit's name is long (a court
   form's own label), and a row that tried to hold the tick, the name and the file name on one line
   pushed the file name off a 390px phone. Measured by looking at it at both widths. */
.affshare-picker { border: 1px solid var(--line); border-radius: 6px; padding: var(--s2); margin: var(--s2) 0; }
.affshare-list { margin: 6px 0 0; }
.affshare-list .rp-row { padding: 8px 0; }
.affshare-id { color: var(--ink-3); font-size: 11px; }
/* A LEGACY AFFIDAVIT IS NAMED, and the mark says why it is one record per job. */
.affshare-legacy { border-left: 3px solid var(--warn-line, var(--line)); padding-left: 8px; }
.affshare-name { display: flex; flex-direction: column; gap: 2px; margin: 6px 0 0 22px; }
.affshare-name .inf-label { font-size: 11px; }
.affshare-name .inf-input { width: 100%; max-width: 34rem; }
/* A BOX FOR AN AFFIDAVIT THAT IS NOT TICKED IS SHUT, not hidden: it shows the operator that the name
   belongs to that row, and re-ticking brings the typing straight back. */
.affshare-name .inf-input:disabled { opacity: .55; }
@media (max-width: 640px) {
  .affshare-name { margin-left: 0; }
  #affshare-block .inf-actions .btn, #affshare-now, #affshare-back { width: 100%; }
}


/* ================================================================================================
   WHO HOLDS THIS JOB HAS ONE PLACE ON THE ROW (2026-09-24) — APPENDED, never inserted mid-file.

   Kazim, from a screenshot of his own rail: *"the names are all on the left except one job has a
   name to the right"*. Traced, not guessed: `.entry-meta` is a WRAPPING FLEX LINE and
   `.entry-sub` carries `flex: 1 1 62%`, so the holder chip's side was ARITHMETIC rather than a
   rule. Measured on his own two rows, at the desk's 319px meta width:

       "B own job"  / Bob     64 + 8 + 202 + 8 + 36  = 318  <= 319   -> fits: chip rides line 1
       "A own job"  / alice   64 + 8 + 202 + 8 + 50  = 332   > 319   -> wraps: chip sits alone, LEFT

   Two names fourteen pixels apart put the same fact on opposite sides of the row. The same
   arithmetic flipped Hague's rows (a long destination pushing the chip right on one row and off
   the line on the next), which is why the report covered Hague as well as Informal.

   THE FIX IS ONE RULE, not a second layout: an auto left-margin on the chip absorbs whatever free
   space the line has left, so the chip is pinned to the RIGHT EDGE of whichever line it lands on
   — beside the destination when there is room for it, on its own line (still right) when there is
   not. Nothing else moves: measured against the deployed rail the row height is UNCHANGED
   (69/98px at 1440 before and after) and the destination is clipped no more than before (10px at
   1440, 70px at 1120 — both pre-existing, neither made worse). The chip's own left offset becomes
   a single value at every width tested (1440 / 1120 / 980 / 860 / 760 / phone), against up to
   five distinct offsets before.

   WHY NOT A GRID WITH THE DESTINATION ON ITS OWN LINE. That was built and measured too: it pins
   the chip on both axes but CLIPS the destination worse (28px at 1440, 88px at 1120) or costs a
   line on every row (~92px where a short row was 69px). The destination names the country that
   drives the legal-aid lookup and `DESIGN.md` describes this row as *"a state chip with its file
   count and destination"*, so re-cutting those two apart to park a chip is the wrong trade.

   Scoped to the three containers the chip is drawn into (`web/owner.js` writes the rail row, the
   phone's card list and the desk's centre view, all through the same `swapChip`), and to
   `.owner-mini` alone.

   THE CHANGED-HANDS MARK, corrected by a later measurement. The mark (`web/case_share.js`,
   `.case-share-mini`) is appended into the same container AFTER the chip, so with the chip pinned
   right by `margin-left: auto` the mark lands OUTSIDE it and the chip is pushed 41px in. Measuring
   with a mark on EVERY row hides this — the whole column shifts together and still reads as one
   value. On a MIXED list (one job in the list changed hands, which is the ordinary case) the marked
   rows put the chip at 41px and the unmarked at 0, which is the same defect this block exists to
   fix, one row down. `order` below settles it: the mark is drawn immediately BEFORE the chip, the
   chip keeps `margin-left: auto`, and the chip's right gap is 0 on every row. Measured with the
   mark on every other row: row height unchanged (98px marked and unmarked), nothing overflows, the
   chip's top is identical (33px). The mark still carries its title/tooltip and its value.
   ================================================================================================ */

#rail-body .entry-meta > .owner-mini,
#rail-body .home-card-meta > .owner-mini,
#stage .home-card-meta > .owner-mini {
  margin-left: auto;
}

/* ================================================================================================
   THE CHANGED-HANDS MARK GOES BESIDE THE CHIP, NOT OUTSIDE IT (2026-09-24) - APPENDED, never
   inserted mid-file.

   `.case-share-mini` (web/case_share.js) is appended into the same meta container as the holder
   chip, so it is the row's LAST item and `margin-left: auto` on the chip pushes the chip in by the
   mark's width on exactly the rows that carry one. On a mixed list - one job in the list changed
   hands - that is the same defect as the one this file already fixes: the same fact in two places
   depending on the row. Measured on the deployed rail with the mark on every other row: chip right
   gap 41px on marked rows, 0 on unmarked.

   `order` settles it with no DOM change and no height cost. The mark is ordered 1 and the chip 2,
   so the mark paints immediately to the chip's left while the chip keeps its `margin-left: auto`
   and stays flush right. Marked and unmarked rows then share one chip column (right gap 0) and one
   row height (98px at the desk), nothing overflows, and the mark keeps its own title/tooltip.

   Scoped to the same three containers as the rule above, and to these two classes alone.
   ================================================================================================ */

#rail-body .entry-meta > .case-share-mini,
#rail-body .home-card-meta > .case-share-mini,
#stage .home-card-meta > .case-share-mini {
  order: 1;
}

#rail-body .entry-meta > .owner-mini,
#rail-body .home-card-meta > .owner-mini,
#stage .home-card-meta > .owner-mini {
  order: 2;
}

/* ================================================================================================
   THE GREY BOX AROUND ACCOUNT SETTINGS IS GONE (2026-09-25). APPENDED at the tail — never inserted
   mid-file — and scoped to `#settings-overlay.st-own-view`, so nothing else in the console moves.

   Kazim: «Also in account settings there is a gray coloured outline of a box around the whole
   thing, get rid of that its ugly». FOUND BY MEASURING, NOT BY GUESSING. The panel was rendered at
   the operator's own 2541x1285 and every element between the panel root and `.st-account` was walked
   for a visible border / outline / box-shadow, then the FRAME ITSELF was read back in PIXELS — a
   scan of the capture for a column/row that is grey down most of its run (which is what a box edge
   is and what a field's own border is not). MEASURED, before this rule:

     * `#account-card.as-card` — `border: 0.909091px solid rgb(203,209,207)`, box
       x 743..1798, y 117..1131 — A FULL RECTANGLE around the whole account form. A pixel scan of
       `tools/out/access-fixes/account-before.png` found its left edge running x=743 and its right
       edge x=1798 for 0.97 of the panel's height, and its top edge y=117 for 0.92 of the panel's
       width. That is the box the operator is looking at.
     * `#cred-card.cred-card` — the same `border: 0.909091px solid rgb(203,209,207)`, a second
       rectangle around the password / sign-in-name half, and it reads as one box with the first.
     * `#settings-overlay .form-overlay-body` — `border: 0.909091px solid rgb(223,227,226)` on all
       four sides. That one is the SHELL's own edge, and it STAYS: the panel must keep its frame and
       its scrim (Kazim's page-sized landscape pop-up), or the fix undoes the shape he asked for.

   WHY THESE TWO RULES EXIST AND WHAT THEY ARE: `account_settings.js` and `account_credentials.js`
   state the hairline on `.as-card` / `.cred-card` for the MODAL they were written as — "on a dark
   ground a card with no edge reads as a hole rather than a surface" — and each panel already
   overrides its own modal dress for the inline mount (`#account-overlay.as-inline .as-card
   { box-shadow: none; padding: 0; border-radius: 0 }` — note the border the inline mount did NOT
   clear). Inside Settings there is no ground to separate from: the card sits on the shell's own
   slab, so its frame is a box drawn around content rather than a card's edge, which is exactly the
   complaint. Only the FRAME goes.

   THE FILL GOES WITH THE FRAME, AND IT IS THE SAME DEFECT IN DARK (Kazim, 2026-09-25: «in dark mode
   as you can see the screenshot, it has this weird different coloured box around the body and
   everything in account settings, look at the light mode (also a screenshot), it doesn't have that,
   so can you make it the same style as light mode, just in dark, remove the gray box thing»).

   MEASURED with `getComputedStyle` on both themes, the pane open, the card's fill against the body
   slab it sits on:

     light   card rgb(255,255,255)  on slab rgb(251,252,251)  -> delta [4,3,4],  luminance gap  3.28
     dark    card rgb(35,40,48)     on slab rgb(28,32,36)     -> delta [7,8,12], luminance gap  8.08

   The fill is `--raised` — `#ffffff` in light, `#232830` in dark — a MODAL's surface, on top of a
   body that is already `--surface`. In light the two are three points apart and read as one page,
   which is why no one ever saw it; in dark the card is 2.5x further from its ground AND shifts
   BLUE (the +12 on the blue channel against +7/+8 on red/green), so the rectangle is plainly
   visible. That is precisely the asymmetry the operator photographed.

   SO THE FILL BECOMES `transparent`: the pane's own surface shows through and the account form
   reads as ONE page in both themes — the same style, not a dark-only exception. The fields and
   buttons inside KEEP their own `--raised` fill and their own border, which is what still makes a
   control read as a control (light: `#fff` fields on the slab, dark: `#232830` fields on the slab,
   each carried by its border) — the same relationship in both themes, and it is the relationship
   the light theme already had and the operator approved.

   WHAT IS DELIBERATELY NOT TOUCHED — these are the controls and the stated rules a person needs,
   and removing them is not what «a box around the whole thing» means:
     * the fields and buttons inside the form (`.inf-input`, `.btn`) keep their own borders;
     * `#account-twofa`, `.as-state`, `#cred-password-section` and `.cred-sec` keep theirs: those are
       SECTION frames grouping one subject, not a box around the whole panel;
     * `.as-no` / the `.ts-panel` / `.rc-panel` keep theirs — `ts-own`'s is a real state panel and
       `rc-own`'s is a DASHED border, a different mark stating a different fact.
   The rule is scoped to the account view's own class and to these two card elements alone, so no
   other overlay, and no field, moves by a pixel.
   ================================================================================================ */

#settings-overlay.st-own-view #account-card.as-card,
#settings-overlay.st-own-view #cred-card.cred-card {
  border: 0;
  background: transparent;
}

/* ====================================================== THE JOB'S OWN PROCESS, ON EVERY SCREEN

   The operator's report: "after putting the document in and naming the job and document it just
   goes to This Job page you literally did nothing else." The creation step produced a record and no
   process. This is the process, drawn above whichever panel is showing: the job's steps in the
   order they are walked, a tick only where the record carries the evidence, and ONE next act with a
   button that goes to it.

   It is a STRIP, not a rail that gates: nothing here is disabled, no step is blocked behind
   another, and the tabs stay the operator's (the old rail was the thing that gated, and it was
   removed for saying "Approve" and "Send" at a form). This only SAYS where the job is and what is
   next; the operator can ignore it and click any tab. */

.inf-progress {
  border: 1px solid var(--line); border-radius: 4px; padding: var(--s3);
  margin: 0 0 var(--s3); background: var(--sunken, rgba(0,0,0,.03)); min-width: 0;
}
.inf-prog-head { display: flex; gap: var(--s2); align-items: baseline; justify-content: space-between;
                 margin: 0 0 var(--s2); flex-wrap: wrap; }
.inf-prog-count { font-weight: 620; font-size: 0.82em; }
.inf-prog-quiet { color: var(--ink-4); font-size: 0.76em; }
.inf-prog-steps { list-style: none; margin: 0 0 var(--s2); padding: 0;
                  display: flex; flex-direction: column; gap: 2px; }
.inf-prog-step { display: flex; gap: var(--s2); align-items: baseline; flex-wrap: wrap;
                 padding: 3px 6px; border-radius: 3px; color: var(--ink-3); font-size: 0.86em; }
.inf-prog-step.done { color: var(--ink-2); }
.inf-prog-step.here { background: var(--surface); color: var(--ink); font-weight: 600;
                      box-shadow: inset 2px 0 0 var(--accent, #2f6f4f); }
.inf-prog-n { width: 1.4em; text-align: center; color: var(--ink-4); font-size: 0.9em;
              flex: none; }
.inf-prog-step.done .inf-prog-n { color: var(--accent, #2f6f4f); }
.inf-prog-title { min-width: 0; overflow-wrap: anywhere; }
.inf-prog-now { font-size: 0.72em; text-transform: uppercase; letter-spacing: .04em;
                color: var(--accent, #2f6f4f); border: 1px solid currentColor; border-radius: 2px;
                padding: 0 3px; }
.inf-prog-next { display: flex; gap: var(--s3); align-items: center; justify-content: space-between;
                 flex-wrap: wrap; padding-top: var(--s2); border-top: 1px solid var(--line-2); }
.inf-prog-what { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1 1 22em; }
.inf-prog-what b { font-size: 0.9em; }
.inf-prog-what span { color: var(--ink-3); font-size: 0.82em; overflow-wrap: anywhere; }
.inf-prog-done { margin: var(--s2) 0 0; }
@media (max-width: 640px) {
  .inf-prog-next { flex-direction: column; align-items: stretch; }
  .inf-prog-next .btn { width: 100%; }
}
.inf-prog-filled { color: var(--ink-4); font-size: 0.76em; }
/* THE NEXT STEP IS MARKED, not called "here": "here" is the screen the operator is on, and the next
   step is a statement about the JOB. Two different facts, two different marks. */
.inf-prog-step.next { color: var(--ink-2); }
.inf-prog-step.next .inf-prog-n { color: var(--accent, #2f6f4f); font-weight: 700; }

/* ================================================================================================
   THE FIRST PAINT IS NOT A CASE (Kazim, 2026-09-25) — APPENDED at the tail, never inserted mid-file.

   Kazim, on a phone: *"when I have not been on the tab and then I go back to it, doesn't matter what
   part of the mission control I am on it still gets this 'Cause list empty' for a second or 2."*

   FOUND BY RENDERING THE FIRST PAINT, NOT BY READING THE CODE. `#case-id`="no case selected" and
   `#case-name`="Cause list is empty" are LITERAL markup in index.html, and `.case-head` is hidden
   only by `body.on-home` — a class **app.js adds**. app.js is ~950KB, so on a phone there is a real
   window in which the browser has painted index.html and app.js has not yet run. Reproduced exactly
   by serving index.html with the app.js tag stripped: the back chevron, "no case selected", "Cause
   list is empty", the 1-5 step rail and "Agent activity" — the screenshot Kazim sent.

   "Doesn't matter what part I am on" is the tell: this is not a failed fetch or a bad hash, it is
   EVERY fresh load, phone and desk alike.

   THE FIX IS IN THE MARKUP'S OWN DEFAULT STATE, not in a render path. The page ships `mc-booting` on
   <html> (set at parse, before any stylesheet or script), and this block removes the case chrome
   while it stands. That is the honest rendering: the console has not read anything yet, so it must
   not claim a case whose name is a placeholder. app.js drops `mc-booting` as its first act, so every
   state after that is app.js's to draw, exactly as before.

   WHY THE MARKUP TEXT IS NOT REWORDED: `verify_idle_tab_recovers.py` uses "Cause list is empty" as
   the marker of the UN-RENDERED state (E15 and `verify_case_race.py` rely on that meaning), so the
   string stays. What changes is that the un-rendered state no longer SHOWS it as a case title. */
html.mc-booting .case-head,
html.mc-booting .stepper,
html.mc-booting .step-caption,
html.mc-booting .chat-wrap { display: none; }
/* the stage holds nothing yet either, and an empty stage beside empty chrome is what made the shell
   read as a broken case */
html.mc-booting .sheet-scroll { overflow-y: hidden; }

/* ------------------------------------------------------------------------------------------------
   THE BOOT NOTE — the only thing the console says between the first paint and app.js's first act.

   It is visible ONLY while `html.mc-booting` stands (see the block above). At rest both lines are
   hidden and it takes no space, so no render path ever sees it. While booting it replaces the
   placeholder case title with the truth, and `mc-boot-stalled` — added by index.html's failsafe when
   app.js has not arrived in 8s — swaps the quiet line for a sentence that says the load did not
   finish. R8: a stage that cannot resolve must SAY so; it must not dress itself as an empty case.

   Tokens first, no new colours: it reads at the console's own quiet weight. Motion is a fade only,
   and it is inside the one authored moment (a state arriving), not a new one. */
html.mc-booting .boot-note { display: block; }
.boot-note { display: none; }
.boot-note-loading { display: inline; }
.boot-note-stalled { display: none; }
html.mc-boot-stalled .boot-note-loading { display: none; }
html.mc-boot-stalled .boot-note-stalled { display: inline; }

.boot-note {
  margin: 0;
  padding: 22px 4px 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--ink-2, #6b7280);
  animation: boot-note-in 260ms cubic-bezier(.22, .61, .36, 1) both;
}
.boot-note em { font-style: normal; }
/* `--warn-ink` is not a token; the literal fell to `#92400e` in both themes. `--warn` is the
   themed one (`#8a5a0b` light / `#e0b467` dark) and it is the same warning colour the rest of this
   file's warning chrome uses. */
html.mc-boot-stalled .boot-note { color: var(--warn, #8a5a0b); }
@keyframes boot-note-in {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .boot-note { animation: none; }
}

/* ================================================================================================
   SETTINGS · TEMPLATES (W2) — THE FIFTH SECTION'S OWN LAYOUT

   APPENDED, NEVER INSERTED (the board's rule for this file: several sessions append to it, so a
   change to a rule ABOVE this block is a lost write rather than an edit). Every selector is
   namespaced `tl-`, so nothing here can restyle a sibling's section, and the section's own styles
   are deliberately small: the shell's `st-*` classes dress the pane and the console's own
   `.st-note` / `.pset-note` / `.st-banner` classes dress the prose, so this block states only the
   layout the panel itself invents.

   WHY IT EXISTS AT ALL, and it is the same measurement the Directory and Roles panels record: a
   panel drawn with the console's own classes and NO rules of its own reads as ONE RUN of text —
   the refusal jammed against the note above it. The spacing here is what keeps two facts apart.
   ================================================================================================ */
#settings-overlay .tl-panel { display: block; }
#settings-overlay .tl-panel .st-note { margin: 0 0 10px; color: var(--ink-3, #666); }
/* THE REFUSAL IS THE SERVER'S SENTENCE, AND IT WRAPS WHEREVER IT MUST: an affidavit form's name can
   be long and a refusal names the capability, so `anywhere` is the only rule that keeps a long
   token inside a 390px panel. `.st-banner` already carries the theme's own colours (light and dark),
   so nothing about colour is invented here. */
#settings-overlay .tl-refusal { overflow-wrap: anywhere; white-space: pre-wrap; }

/* AN EMPTY NOTICE DRAWS NOTHING.
   MEASURED DEFECT, seen only by looking at the rendered panel: `#tl-notice` reuses `.st-banner`,
   which always paints `border-left: 3px solid var(--fault)` and a sunken background. With no text in
   it — the state the panel opens in — that is a 1267x21.5px grey strip carrying a red vertical bar
   and nothing else: it reads as a broken or truncated error banner, which is a worse lie than no
   banner at all. `:empty` is the honest test: the panel sets the text content itself, so the element
   is empty exactly when there is nothing to say. */
#settings-overlay .tl-notice:empty {
  display: none;
}
#settings-overlay .tl-manager { display: block; margin-top: 10px; }
#settings-overlay .tl-manager-note { overflow-wrap: anywhere; }
/* THE MANAGER'S OWN LIST, FOR THE ACTS THAT LAND IN IT: a template row is drag-able when the work
   that draws it arrives, so the cursor and the row's own box are stated now rather than later. */
#settings-overlay .tl-manager .tl-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  border: 1px solid var(--line, #ddd); border-radius: 6px; padding: 8px 10px; margin-bottom: 6px; }
#settings-overlay .tl-manager .tl-row .tl-name { flex: 1 1 12em; min-width: 0;
  overflow-wrap: anywhere; }
@media (max-width: 720px), (max-width: 900px) and (max-height: 500px) {
  #settings-overlay .tl-manager .tl-row { align-items: flex-start; }
}

/* --- the blanket block's own banner kind ---------------------------------------------------------
   Operator, 2026-09-26: «when a blanket block is applied it grays out the blocked tabs which is
   good, but I can still click on the tabs even though they are grayed out ... and also make it
   someone cant even change the url».

   THE GREY TAB NOW ALSO STATES ITS OWN REASON, and that reason travels on `#banner` — the console
   already has exactly one transient line for a server sentence, and a second surface would be a
   second answer to one question. So this is the banner's FOURTH kind, not a new component. It is
   `--warn`, NOT `--danger`: a blanket block is a POLICY the operator set, not a fault. Painting a
   deliberate block in the fault colour trains a reader to treat a decision as a bug — and the
   console's own semantic token for «a reading that needs a human decision» is `--warn` (DESIGN.md).
   Appended at the end so it wins on equal specificity without an `!important`. */
.banner[data-kind="blocked"] {
  background: var(--warn-soft);
  color: var(--warn);
  border-bottom-color: var(--warn-line);
}
@media (prefers-contrast: more) {
  .banner[data-kind="blocked"] {
    background: var(--surface);
    color: var(--ink);
    border-bottom-color: var(--ink-2);
  }
}

/* ================================================================================================
   SETTINGS — THE SHELL'S OWN REFINEMENT PASS (append-only; nothing above is rewritten)
   ================================================================================================
   Operator, 2026-09-26: «go through all of settings menu/s and make it clean it up, no dev notes
   etc ... refine it ... but go through everything else too to get rid of clutter and notes».

   THE THREE THINGS BELOW AND WHY EACH IS HERE. All three are corrections to the ORIGINAL shell
   rules further up this file, which is why they are appended rather than edited: `app.css` is
   shared with sibling sessions and append-only by agreement.

   1. MUTED TEXT WAS SET WITH `opacity`, WHICH IS NOT A COLOUR. The shell's own notes, keys,
      facts and refusals all read `opacity: 0.7–0.85`. On the light palette a mid-grey ink over a
      white ground survives that; on the DARK palette the same `opacity` composites the ink toward
      a near-black surface and the line loses roughly a third of its contrast — the metadata of the
      Team cards and every refusal paragraph read as disabled text. `--ink-3` is DESIGN.md's own
      token for «metadata» and is redefined for the dark palette precisely so a muted line stays
      legible in both; the fix is to state the colour instead of fading it. Opacity is also wrong
      for a second reason: it fades the BORDER of a chip or the bar of a refusal along with the
      words, so a 1px `--line` becomes a sub-threshold hairline on the one theme that needs it most.

   2. THE PANES HAD NO SHARED VERTICAL RHYTHM. Every section is a different module with its own
      margins, so the gap under a heading was 0.4em in the shell, 18px in the roles panel and 10px
      in the directory — three answers to one question. This block sets ONE rhythm for the shell's
      own children (heading → note → body), in `em` so it follows the reader's text size.

   3. THE SELECTED TAB WAS TOO QUIET. Measured off the render: `[aria-selected="true"]` differed
      from its neighbours by a 1px hairline and a `--raised` ground that is ~2% from `--surface` on
      the light palette — the strip read as five plain text links, and the one piece of state a
      tab strip exists to carry was the hardest thing to see in the pane. The selected tab now
      carries WEIGHT as well as ground, which is the honest way to say «you are here» without
      adding a colour that means nothing. (This is the same fix as the roles panel's grant chips:
      state by weight and ground, never by colour alone.)

   NO MOTION IS ADDED. DESIGN.md's Motion section names exactly two authored moments and neither is
   here; a settings pane is a place you look at, not a thing you watch. The `.st-tab` rules below
   therefore inherit the console's own `.btn`/`:active` behaviour and add only `:focus-visible`, so
   a keyboard user can see which tab has the caret. */

/* ---- 1. muted text stated as a colour, not faded ------------------------------------------------ */
.st-note,
.st-panel-note {
  color: var(--ink-3);
  opacity: 1;
}

.st-field-label,
.st-field > span {
  color: var(--ink-3);
  opacity: 1;
}

.st-team-key,
.st-team-facts {
  color: var(--ink-3);
  opacity: 1;
}

/* A REFUSAL IS THE SERVER'S SENTENCE ABOUT AN ACT, AND IT IS THE ONE THING ON A LOCKED CARD THE
   READER MUST BE ABLE TO READ. It was `opacity: .85`, which on the dark palette faded it to
   near-invisible against `--sunken`. Stated in ink, at the secondary weight. */
.st-refusal-sentence,
.st-team-note {
  color: var(--ink-2);
  opacity: 1;
}

/* ---- 2. one vertical rhythm for the shell's own children --------------------------------------- */
.st-section > .st-h,
.st-section > h3,
.st-section > h4 {
  margin: 0 0 0.45em;
  letter-spacing: -0.005em;
}

.st-section > .st-note,
.st-section > .st-panel-note {
  margin: 0 0 1.1em;
  max-width: 78ch;
}

/* The last block in a pane carries no trailing margin: the pane's own padding is the gap. Without
   this the shell double-counts its bottom padding against the last card's margin and every pane
   ends on a visibly larger gap than it began with. */
.st-section > *:last-child,
.st-team-row > *:last-child,
.st-team-row .st-refusal-sentence:last-child {
  margin-bottom: 0;
}

/* ---- 3. the tab strip: state by weight and ground ---------------------------------------------- */
.st-tab {
  font-weight: 500;
  color: var(--ink-2);
  border-radius: 7px 7px 0 0;
  /* The strip's tabs must not jump by a pixel when one gains weight, so the selected state changes
     WEIGHT only, never size: the row's baseline is fixed by the unselected tabs. */
}

.st-tab[aria-selected="true"] {
  color: var(--ink);
  font-weight: 640;
  background: var(--raised, transparent);
  border-color: var(--line, currentColor);
  border-bottom-color: transparent;
  /* A selected tab sits ON the strip's rule, so it must not draw its own shadow onto it — the
     border-bottom colour above is what cuts the line under the tab. */
}

.st-tab:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* THE PERSON'S OWN ACCOUNT ENTRY is not a section and must not compete with one: same treatment,
   set one step quieter, and it keeps its own class (a check counts `.st-tab`s). */
.st-own-entry {
  font-weight: 500;
  color: var(--ink-2);
}

.st-own-entry[data-selected="true"] {
  color: var(--ink);
  font-weight: 640;
  background: var(--raised, transparent);
}

.st-own-entry:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* ---- the shell's headings and the banner ------------------------------------------------------- */
.st-banner,
.st-section .st-h {
  color: var(--ink);
}

/* THE SHELL'S OWN TITLE BAR, if a heading is drawn beside the strip: it is the pane's name, so it
   carries primary ink while everything under it steps back. */
.st-panel-title {
  font-size: 1em;
  font-weight: 640;
  letter-spacing: -0.005em;
  color: var(--ink);
}

.st-panel-note {
  max-width: 78ch;
}

/* ---- honest states read as quiet, not as a wall ------------------------------------------------ */
/* A pane that cannot be drawn, or an office with no entries, states ONE sentence. It is not an
   error and must not shout: `--ink-3` on the pane's own ground, no border, no fill. The roles
   panel's refusal banner is the exception and keeps its `--danger` edge, because a refusal is a
   fact the reader must not miss. */
.sd-empty,
.sd-refusal {
  color: var(--ink-3);
  margin: 0.6em 0 0;
}

/* ---- contrast, transparency and motion --------------------------------------------------------- */
@media (prefers-contrast: more) {
  .st-note,
  .st-field-label,
  .st-team-key,
  .st-team-facts,
  .st-refusal-sentence,
  .st-team-note,
  .st-panel-note,
  .sd-empty,
  .sd-refusal {
    color: var(--ink);
  }

  .st-tab[aria-selected="true"],
  .st-own-entry[data-selected="true"] {
    border-color: var(--ink-2);
  }
}

/* The strip is opaque chrome on an opaque panel, so `prefers-reduced-transparency` has nothing to
   act on here; the rule is stated so a later change that makes it translucent has a place to land. */
@media (prefers-reduced-transparency: reduce) {
  .st-tabs {
    backdrop-filter: none;
  }
}

/* Nothing in this block animates, so `prefers-reduced-motion` changes nothing about it. The one
   place the strip could have moved — a sliding indicator under the selected tab — is deliberately
   NOT built: DESIGN.md's Motion section names exactly two authored moments, and a settings tab
   strip is not one of them. */


/* ==========================================================================================
   [DELIVERY LINK] THE SIGNED-OUT RECORDING PAGE HAS NO CASE LIST, SO ITS TRACK MUST NOT
   RESERVE SPACE — APPENDED, at the tail (Kazim, 2026-09-28).

   `linkHideChrome()` hides `.col.queue` on a token page — but the track it sat in is a fixed
   `var(--rail)` of the `.desk-body` grid, and `hidden` on a grid ITEM does not collapse its TRACK.
   MEASURED at 1440x900 on the real link page before this rule: the job card, the recorder strip and
   the whole Attempted Delivery form were laid out in a ~420px column with ~1000px of dead space
   beside them, because the empty rail's track was still reserved. The form a process server fills in
   — on a desktop, from an email — should use the page.

   SCOPED to `body.link-mode` (set by the token bootstrap in `app.js`), so the operator's own screen
   keeps its rail and its two columns exactly as they are.

   `overflow-wrap:anywhere` is here for the same class of reason the email needed it: this page can
   carry a long unbroken token or URL, and an unbreakable token must not push a phone sideways.
   ========================================================================================== */
body.link-mode .desk-body { grid-template-columns: minmax(0, 1fr); }
body.link-mode .col.queue { display: none; }
body.link-mode .sheet { border-left: 0; }
body.link-mode #stage,
body.link-mode #sheet-scroll { overflow-wrap: anywhere; }

/* ================================================================================================
   THE ARCHIVE (2026-09-28) — appended, and namespaced `.ar-*` so no existing rule is touched.

   The feature: a finished job is marked complete and filed away, so the tab holds only current
   work while the filed cases stay durable for the 7 years the client stated.

   THE SHAPE OF IT IS A CORRECTION (Kazim, 2026-09-28, verbatim):

     «have a mark as complete button not this clutter that you see in the screenshot. Also don't have
      archive button up here, have it where I circled in red in the screenshot, as like a part of the
      tab … also for the search bar have it rounded and in the center of the archive at the top»

   The first build drew a CARD at the foot of the case carrying a state chip, a FINISHED/STATE fact
   table and a sentence — «this clutter». The facts it stated were already on the screen: the case's
   own state chip says where the job is, and its destination and category are on the card the
   operator came from. What remains is the ACT, as one button, beside the case's Delete; the one
   thing that must still be SAID (a read that did not establish the answer, or a disagreement) is a
   line of prose and not a table.

   Dress: the console's ordinary surface language (DESIGN.md) — `--raised`, hairline `--line`,
   `--r-*` radii, mono for records, and one accent used only for meaning. EVERY token named here is
   defined in this stylesheet's `:root`/dark pair; none is invented and none carries a fallback
   literal, which is the defect that once painted a light-mode panel dark (`--panel-2`).
   ================================================================================================ */

/* -------------------------------------------------- the act, in the case's own action row */
/* `.case-actions` is `flex-wrap: wrap` and holds the case's state chip and Delete. The act is a
   member of that row, so it needs no margin of its own — the row's `gap` spaces it. */
.ar-acts { display: inline-flex; align-items: center; gap: var(--s2); flex-wrap: wrap; }
.ar-acts .btn { white-space: nowrap; }
/* --------------------------------------------------- THE ARCHIVE ACT IS DELETE'S TWIN, IN GREEN
   Kazim, 2026-09-29: «make the archive button the same style as the delete job button just green
   instead of red.»

   SAME CUT AS DELETE, which is `.btn danger sm` in `.case-actions`: a neutral surface at rest with
   the colour arriving in the WORD and the hover, not a filled block. `.btn danger` is
   `color: var(--danger); background: var(--raised); border-color: var(--line-2)`; this is that
   declaration with `--ok` in the place of `--danger`, so the two buttons differ in exactly the one
   token the operator asked to differ.

   THIS REPLACES `btn primary ok-solid`, WHICH FILLED THE BUTTON GREEN. That dress belonged to a
   row where the act stood alone; beside Delete it made the archive read as the loudest thing on
   the screen and the two acts as unequal weights, when they are the two acts the operator takes on
   a job. The `ar-done` class is KEPT because this module and its checks select it by name. */
.ar-acts .ar-done {
  order: 0;
  color: var(--ok);
  background: var(--raised);
  border-color: var(--line-2);
}
.ar-acts .ar-done:hover:not(:disabled) {
  background: var(--ok-soft); border-color: var(--ok-line); color: var(--ok);
}
.ar-acts .ar-done .glyph-svg { margin-right: 6px; }
/* -------------------------------------------------- a filed card's own acts, and why they are seen */

/* ON THE TAB THE CARD'S ACTS ARE HOVER-ONLY (`.home-card-acts { opacity: 0 }` until `:hover` /
   `:focus-within`, app.css:1804). That convention is right where the pencil and bin are a SHORTCUT to
   work the operator can open by clicking the card — the card itself is the door.

   IN THE ARCHIVE IT IS WRONG, and Kazim said so twice, by name: «there should be a restore button with
   the edit icon on the card, aswell as a delete button». Restore is not a shortcut to something the
   card already offers — it IS the act of this screen, the one reason a filed case is shown here at
   all. A control the operator has asked for twice and cannot see until they happen to move the mouse
   over a card is not a control they were given.

   SO THEY REST AT FULL OPACITY, HERE ONLY, so the tab keeps its own convention. Scoped to
   `#archive-list`, which is the filed list's own container, so nothing outside the archive changes. */
#archive-list .home-card-acts { opacity: 1; }
/* AND THEY CARRY THEIR NAME: `decorateFiledCards` sets each button's `title` and `aria-label` to the
   card's own name and act ("Restore Second Filed to the tab"), so a hover or a screen reader says
   what the glyph means. The glyph is what Kazim asked for («a restore button with the edit icon»);
   the words ride on it rather than being drawn beside it, which is how the tab's own cards read. */
#archive-list .home-card-acts .btn-icon { width: 30px; height: 30px; }

/* THE ANSWER TO THE ACT lands under the row it was pressed in, right-aligned with it so it reads as
   a reply to that button and not as a new statement about the case. */
.case-actions .ar-said {
  flex: 1 0 100%;
  margin: 6px 0 0;
  text-align: right;
  font-size: 12.5px;
  color: var(--ink-2);
}
.case-actions .ar-said[data-kind="busy"] { color: var(--ink-3); }
.case-actions .ar-said[data-kind="ok"] { color: var(--ok); }
.case-actions .ar-said[data-kind="refused"] { color: var(--danger); }

/* the statement that is NOT an act — a read that did not establish the answer, or a disagreement */
.ar-foot-note {
  margin: 18px 0 0;
  padding: 10px 12px;
  background: var(--sunken);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--ink-2);
}
.ar-foot-note[data-kind="disagreement"] { border-color: var(--danger-line); color: var(--danger); }

/* -------------------------------------------------- the entry in the tab, where he circled it */
/* THE TAB'S OWN HEADER ROW. `renderHome` draws `.wf-intro` — the tab's name and its blurb — and this
   entry is dressed onto that row so it reads as PART OF THE TAB rather than as chrome bolted to the
   strip above it (which is where it was, and where he said it must not be). The intro keeps its own
   margins; the wrapper is only a two-column row. */
.wf-home-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--s4);
}
/* the blurb's own `margin-bottom` would push the row taller than its tallest child, so the wrapper
   owns the spacing and the intro contributes none */
.wf-home-head .wf-intro { margin-bottom: 0; }
.wf-home-head + * { margin-top: var(--s5); }

.ar-entry {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 14px;
  font: inherit;
  font-size: 13px;
  font-weight: 620;
  color: var(--ink-2);
  background: var(--raised);
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  cursor: pointer;
  box-shadow: var(--e1);
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease),
              border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.ar-entry:hover { background: var(--hover); border-color: var(--line-2); color: var(--ink); }
/* THE PRESS IS ACKNOWLEDGED ON THE WAY DOWN, not on release — the moment lag appears the control
   stops feeling direct. Same displacement the console's own buttons use, so it belongs to the
   family rather than being a new behaviour. */
.ar-entry:active { transform: translateY(1px); box-shadow: none; }
.ar-entry:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.ar-entry .ar-entry-n { color: var(--ink-4); font-size: 11px; font-weight: 600; }
.ar-entry[data-count="0"] .ar-entry-n { display: none; }

/* -------------------------------------------------- the pop-up */
.ar-overlay .form-overlay-body { padding: 0; }
body.ar-open-mode { overflow: hidden; }

/* THE LIST PANE AND THE CASE PANE occupy the same body, one at a time. `hidden` is the switch, so
   the case screen is genuinely absent while the list is up rather than merely covered. */
#ar-list-pane { display: flex; flex-direction: column; min-height: 0; height: 100%; }
.ar-detail {
  display: flex;
  flex-direction: column;
  min-height: 0;
  height: 100%;
  background: var(--bg);
}
.ar-detail[hidden] { display: none; }
/* THE CASE SCREEN TAKES THE WHOLE POP-UP — «it should be fullscreen within the pop up, like the
   case/job like opens within this pop up, and takes its whole space». The console's own `.col.sheet`
   is MOVED in here (not copied), so it arrives with its real width. It is laid out as the single
   column of this pane, with its own `.sheet-scroll` inside it doing the scrolling. */
.ar-detail > .col.sheet {
  flex: 1 1 auto;
  min-height: 0;
  border-left: 0;
  background: var(--surface);
}
.ar-detail .sheet-scroll { flex: 1 1 auto; min-height: 0; }
/* the pop-up's own body must not scroll while the case pane is up, or the case would scroll inside
   a scrolling box and the header would drift away from the screen it belongs to */
.ar-overlay.ar-in-detail .form-overlay-body { overflow: hidden; }

/* -------------------------------------------------- the search — rounded, centred at the top */
.ar-search-row {
  display: flex;
  justify-content: center;
  padding: 4px 0 2px;
}
.ar-search {
  position: relative;
  display: flex;
  align-items: center;
  width: 100%;
  max-width: 420px;
}
.ar-search .glyph-svg {
  position: absolute;
  left: 14px;
  color: var(--ink-4);
  pointer-events: none;
}
.ar-search-in {
  width: 100%;
  font: inherit;
  font-size: 13.5px;
  padding: 10px 16px 10px 38px;
  color: var(--ink);
  background: var(--sunken);
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease),
              box-shadow var(--dur) var(--ease);
}
.ar-search-in::placeholder { color: var(--ink-4); }
.ar-search-in:focus {
  outline: none;
  background: var(--raised);
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
/* the browser's own clear affordance on a search input is drawn in the browser's colours, which do
   not follow this console's theme; it is hidden and the field is cleared by selecting the text */
.ar-search-in::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }

/* -------------------------------------------------- the filed list — the tab's own cards */
.ar-view { padding: 16px 18px 22px; }
.ar-list { padding: 12px 0 0; }
/* THE CARDS HERE ARE `cardSections`'s OWN — the same function the tab's home view calls — so the
   grid, the section headings and the cards arrive already dressed and nothing is restyled. This row
   only narrows the grid: the pop-up is a column, and a 280px minimum is the card size the tab uses. */
.ar-list .home-section:last-child { margin-bottom: 0; }
.ar-list .empty-line { margin: 0; }

.ar-note {
  margin: 12px 0 0;
  font-size: 12.5px;
  line-height: 1.6;
  color: var(--ink-2);
}
.ar-note[data-kind="refused"] { color: var(--danger); }

/* -------------------------------------------------- phone */
@media (max-width: 720px) {
  /* THE ROW STACKS. On a phone the case header is already tight — the state chip is capped at 46vw
     — so the act takes its own line rather than squeezing the case title. */
  .ar-acts { flex: 1 0 100%; justify-content: flex-start; }
  .case-actions .ar-said { text-align: left; }
  .ar-detail > .col.sheet { padding: 0; }
  .ar-search { max-width: none; }
  .ar-view { padding: 12px 14px 18px; }
}

/* A PREFERENCE FOR LESS MOTION IS ANSWERED, not ignored: the press and the entrance become instant
   rather than springy, and nothing here relies on movement to be understood. */
@media (prefers-reduced-motion: reduce) {
  .ar-entry, .ar-search-in { transition: none; }
  .ar-entry:active { transform: none; }
}

/* ==================================================================================================
   THE DELIVERY LINK'S OWN PAGE: IT HAS TO SCROLL ON A PHONE, AND IT HAS NOTHING TO CANCEL BACK TO
   (Kazim, 2026-09-28)

   (a) UNSCROLLABLE ON MOBILE. `html, body { overflow: hidden; }` is the desktop app shell, and the
   release that lets a PHONE scroll the document is scoped to the two views the console has:
   `html:has(body.on-home)` and `html:has(body.case-open)`. A delivery link's page is NEITHER — the
   token bootstrap draws the Attempted-Delivery form on a page that carries neither class — so
   `overflow: hidden` stood and the process server could not scroll at all. MEASURED on the link page
   before this block: document scrollHeight 1,300+ against a 844px phone viewport with
   `html`'s overflow-y computing to `hidden`, so the bottom of the form (the pictures and Save) was
   unreachable.

   The fix is the SAME release the other phone views get, scoped to the link page's own class rather
   than invented here: X pinned shut (`overflow-x: clip`, or `zoom: 1.1` lets the page be dragged a
   few px sideways) and Y the scroller, with the height chain released so the document scrolls.

   SCOPED to `body.link-mode` ONLY. The operator's own two-column screen keeps `overflow: hidden`
   exactly as it was: this block can be read and deleted without touching any other view.

   (b) NO CANCEL. The console's Cancel clears the edit state and returns to the job view. A link page
   has no job view to return to — the token opens this one screen — so the button is not emitted at
   all (see `infAttemptForm` in app.js, where it is now conditional on `window.mcLinkMode`). Nothing
   to style here; stated so the absence is not mistaken for a missing rule.
   ================================================================================================== */
@media (max-width: 900px) {
  html:has(body.link-mode) {
    overflow-x: clip;
    overflow-y: auto;
    height: auto;
    overscroll-behavior-y: contain;
  }
  body.link-mode {
    overflow: visible;
    height: auto;
    min-height: 100%;
    display: block;
  }
  /* the sticky shell must not become its own overflow context, or the bar resolves against a box
     that never scrolls (the same reason recorded for the other two phone views) */
  body.link-mode .desk,
  body.link-mode .sheet,
  body.link-mode #sheet-scroll {
    overflow: visible;
    height: auto;
    min-height: 0;
  }
  body.link-mode #stage { overflow: visible; min-height: 0; }
}

/* ================================================================================================
   THE VIEWING TAB (Kazim, 2026-09-28): "Also it should have a viewing tab for it so we can view it
   before approving, editing or rejecting it."

   APPENDED, never woven in, so a check that pins another surface cannot be disturbed by this one and
   two cards editing this file at once cannot collide. Everything here is scoped to the viewing tab's
   own card (`#att-view-card`) or its own classes, so nothing above changes meaning.

   WHAT IT HAS TO BE, and this is the whole reason the sheet holds more than one rule: a page read at
   1440px on a desk and at 390px on a phone. The facts are a definition list (`inf-kv`, already
   two-column with `minmax` on the label), and two things break a phone's width if they are not
   handled here — a long unbroken token (an attempt id, a record id, a case-relative file path) and a
   picture whose natural width is larger than the screen. Both are stated rather than hoped for.
   ================================================================================================ */

/* THE CARD'S OWN FACTS LIST. `inf-kv` already does the two-column layout; this only guarantees that
   nothing inside it can push the card wider than its container. */
#att-view-card .att-view-facts,
#att-view-card .att-view-facts > dt,
#att-view-card .att-view-facts > dd { min-width: 0; overflow-wrap: anywhere; }
#att-view-card .att-view-facts { margin: var(--s1) 0 0; }

/* THE STANDING LINE. `inf-flag` is the same class the job page's unapproved badge uses, so the two
   surfaces say the same thing in the same shape; the counting state gets the accent rather than the
   warning colour, because "it counts" is not a caution. */
#att-view-card .inf-view-counts { color: var(--accent, #2f6f4f); border-color: var(--accent, #2f6f4f); }
#att-view-card #att-view-standing { display: flex; flex-wrap: wrap; align-items: baseline;
  gap: var(--s2); min-width: 0; }
#att-view-card #att-view-standing .inf-flag { flex: 0 0 auto; }

/* THE DELIVERY'S OWN ID, AND EVERY UNBROKEN TOKEN ON THE PAGE. An id is one unbroken word by
   construction (`att-<case>-0001`), and `att-photo-head .mono` carries a case-relative path; on a
   390px phone either one, left unbroken, drags the whole card sideways and the page can be dragged.
   `overflow-wrap: anywhere` is the rule that stops it, and it is on the token's own element rather
   than on the card so a long path wraps inside its own row instead of stretching the grid. */
#att-view-card #att-view-id,
#att-view-card .att-photo-head .mono,
#att-view-card .att-quiet,
#att-view-card .inf-list li,
#att-view-card [data-att-view-why] { overflow-wrap: anywhere; min-width: 0; }

/* THE PICTURES. `att-photo-img img` already caps at `max-width: 100%`; this adds the two things a
   phone needs from a viewer — a tap target that is legibly large and a block that cannot outgrow its
   list item. Lazy loading is set on the img itself by the renderer, so a delivery with several
   pictures does not fetch them all before the person has read the first fact. */
#att-view-card .att-photos { width: 100%; }
#att-view-card .att-photo { min-width: 0; }
#att-view-card .att-photo-img img { max-width: 100%; height: auto; }
#att-view-card .att-photo-missing { min-width: 0; overflow-wrap: anywhere; }

/* ON A PHONE THE LABEL SITS ABOVE ITS VALUE, and it is the same rule the sheet already applies to
   `inf-kv` elsewhere; restated for this card so the tab does not depend on a media query far up the
   file that another card may move. One column, always, under 560px. */
@media (max-width: 560px) {
  #att-view-card .att-view-facts { grid-template-columns: 1fr; }
  #att-view-card .att-view-facts > dt { margin-top: var(--s1); }
  #att-view-card .att-photo-head { display: block; }
  #att-view-card .att-photo-head .mono { display: block; margin-top: 2px; }
  /* THE WAY BACK AND THE NOTE STACK rather than sitting side by side on a 390px screen, where the
     note would be squeezed into a column a few characters wide. */
  #att-view-card .inf-actions { flex-direction: column; align-items: stretch; }
  #att-view-card .inf-actions .btn { text-align: center; }
}

/* THE STANDING LINE MUST NOT PUSH THE CARD WIDER THAN A PHONE. Measured at 390x844: the badge
   ("counts by the act that recorded it — no separate approval needed") is a flex item with
   `flex: 0 0 auto`, so it refused to shrink or wrap and its right edge landed past the viewport —
   `document.querySelectorAll('*')` flagged `.inf-flag.inf-view-counts` crossing the edge. A flex
   item that may not shrink is a flex item that decides the width of the page. */
#att-view-card #att-view-standing { min-width: 0; }
#att-view-card #att-view-standing .inf-flag { flex: 0 1 auto; min-width: 0; overflow-wrap: anywhere;
  white-space: normal; }
@media (max-width: 560px) {
  /* One column: the badge on its own line, the reason under it — the shape the phone already uses
     for every other label/value pair on this card. */
  #att-view-card #att-view-standing { display: block; }
  #att-view-card #att-view-standing .inf-flag { display: inline-block; margin-bottom: 2px; }
}


/* ==================================================================================================
 * THE READING'S PICTURES, FOLDED, AND THE DECISIONS AFTER THE FACTS (Kazim, 2026-09-28)
 *
 * "for the images within it, it should be expandable, like it lists the images and it can expand it,
 *  but keep a set size, it should get the real ratio of the image but on this web keep like a set
 *  size so its not too big, similar to the email."
 *
 * THE SIZE IS THE EMAIL'S OWN CEILING, deliberately — `web/mail.py` boxes every picture at
 * `max-width:300px` with `height:auto`, which is what the operator named as the reference. The same
 * two properties in the same order: the BOX has a ceiling, the IMAGE keeps its own aspect ratio.
 * Nothing is stretched to fill a slot and nothing is cropped to a ratio, so a tall picture is tall
 * and a wide one is wide, and neither runs the length of the page.
 *
 * APPEND-ONLY, at the tail: the `.att-photo` rules further up were written for an unfolded list and
 * are left exactly as they are. These are scoped to the viewing card so the job page's own attempt
 * rows, which also carry `.att-photo`, are untouched.
 * ================================================================================================== */

#att-view-card .att-photo-fold { min-width: 0; }

/* THE ONE LINE PER PICTURE — what you read before deciding to open it. */
#att-view-card .att-photo-head {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
  min-width: 0; cursor: pointer; padding: 2px 0;
}
#att-view-card .att-photo-head::-webkit-details-marker { color: var(--ink-3); }
#att-view-card .att-photo-head .att-photo-name { flex: 1 1 140px; min-width: 0; overflow-wrap: anywhere; }
#att-view-card .att-photo-head .att-photo-bytes { flex: 0 0 auto; }
#att-view-card .att-photo-missing-inline { flex: 0 0 auto; font-size: 11px; color: var(--warn); }

/* THE EMAIL'S CEILING: a fixed box, the image in its own ratio inside it. */
#att-view-card .att-photo-body { margin-top: 6px; min-width: 0; }
#att-view-card .att-photo-img { display: block; margin-top: 2px; max-width: 100%; }
#att-view-card .att-photo-img img {
  display: block;
  max-width: 300px;          /* the email's own ceiling (mail.py `_PIC_BOX`) */
  max-height: 300px;         /* a tall picture is bounded too, so the page stays readable */
  width: auto;
  height: auto;              /* THE REAL RATIO — never stretched, never cropped */
  border-radius: var(--r-sm);
  border: 1px solid var(--line);
  /* `--card` IS NOT A TOKEN THIS STYLESHEET DEFINES, and an undefined custom property with no
     fallback is INVALID AT COMPUTED-VALUE TIME — so the declaration was dropped and the element
     inherited its ground instead. MEASURED by `verify_theme_tokens.py` part A, which named these
     two rules by line. The page render is a white sheet in both themes (`.aff-snippet img` and
     `.inf-photo-img img` already set it), so it takes `--raised`, the token for a card surface. */
  background: var(--raised);
}

/* THE DECISIONS, AFTER EVERYTHING THEY ARE A DECISION ABOUT. */
#att-view-card .att-view-acts { margin-top: var(--s2); }
#att-view-card .att-view-acts .btn.danger { color: var(--warn); }

@media (max-width: 560px) {
  /* A phone gets the same ceiling, bounded by the screen it is on. */
  #att-view-card .att-photo-img img { max-width: min(300px, 100%); }
  #att-view-card .att-view-acts { display: flex; flex-wrap: wrap; gap: 8px; }
}



/* ==================================================================================================
 * THE READING, REDESIGNED (Kazim, 2026-09-28): "The viewing and the buttons within it look trash,
 * use /impeccable to redesign it and use /apple-design to make it feel nice."
 *
 * WHAT WAS WRONG, concretely — not a taste complaint, three measurable faults:
 *
 *   1. EVERYTHING WAS A DISCLOSURE. The reading arrived as a stack of five `<details>` all open, so
 *      the first thing on screen was a row of triangle glyphs — chrome the operator had to look past
 *      to read four facts. A `summary` is a control; a delivery is not made of controls.
 *   2. NO HIERARCHY. The standing, the id and the facts all carried the same weight, so the one thing
 *      that decides whether the delivery COUNTS — the approval gate — was a sentence in a paragraph
 *      of small print, and the delivery's own id (a 40-character token) sat where a title should be.
 *   3. THE BUTTONS WERE A ROW OF EQUALS. Approve, Reject, Edit, Delete and Back were five identical
 *      small buttons. The destructive ones were as easy to hit as the way back, and nothing said
 *      which was the primary act.
 *
 * WHAT THIS IS NOW, and the reasoning, in the order a reader meets it:
 *
 *   THE HEADER states the thing the page is about: what this delivery is, and — as a CHIP with a dot,
 *   the console's own `inf-flag` language kept — whether it counts. The approval gate is the most
 *   consequential fact on the page and it is now the first thing on it, in the accent or the warning
 *   tone, with the record's own reason directly under it.
 *
 *   THE FACTS are ONE list, not two disclosures. Nine label/value rows, grouped by hairlines rather
 *   than separated by chrome, in the console's existing `inf-kv` shape: the labels a fixed column at
 *   the desk so the values line up and the eye can scan them, one column on a phone.
 *
 *   THE COUNTS are CHIPS that jump to the section they count. They answer "is there anything else
 *   here?" before you scroll, and they do something when pressed — a count that is only decoration
 *   teaches a reader to ignore it.
 *
 *   THE PICTURES are a GALLERY of folds, one line per picture. See the block further up for the size
 *   rule (the email's own 300px ceiling, the image in its real ratio).
 *
 *   THE DECISIONS are a FOOTER, deliberately: the primary act first (Approve), the destructive acts
 *   separated and quiet, the way back OUT of the flow as a ghost. Reading a delivery is what a person
 *   does BEFORE deciding, and the layout now says so.
 *
 * APPEND-ONLY, at the tail, scoped to `#att-view-card` and the `rv-` prefix, so nothing above changes
 * meaning and the job page's rows (which share `.att-photo`, `.att-photos`, `.inf-list`) are untouched.
 * ================================================================================================== */

#att-view-card.rv {
  /* The reading is its own surface: a card that sits ON the sheet rather than a step in it. */
  padding: 0;
  overflow: hidden;
  background: var(--raised);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--e2);
}

/* ---- THE HEADER --------------------------------------------------------------------------------- */
#att-view-card .rv-hero {
  display: flex; align-items: flex-start; justify-content: space-between; gap: var(--s3);
  padding: var(--s4) var(--s5) var(--s3);
  /* NO BLUE BAND (Kazim, 2026-09-28). His words: «remove the gradient but otherwise its really nice»
   * and then, when the blue came back across the console: «I told you to remove the blue ish gradient
   * from the attempt deliveries from the process server and then use that design, WITHOUT the blue
   * gradient».
   *
   * THE FIRST CUT OF THIS GOT IT WRONG, and this comment stood over the mistake. The band was
   * `linear-gradient(180deg, var(--accent-soft), var(--raised))`; I removed the gradient but flattened
   * to `var(--accent-soft)` — the ACCENT end — and left blue on the card Kazim had named as the design
   * to copy. Worse, it made blue the header token: the block at the foot of this file that propagates
   * this design read the blue as the reading's own idiom («The reading's header is `--accent-soft`»)
   * and put a blue top on the workflow step cards and on the process server's page. One wrong token
   * became the house style.
   *
   * SO THE BAND IS NEUTRAL. `--sunken` is a real surface token declared in both themes (#f2f4f3 light,
   * #191d21 dark) — it is the console's own "recessed/quieter surface", needs no new token, and the
   * accent stays spent on MEANING (a chip, a current step, a live state) rather than on a decorative
   * top edge. The band still does its structural job: the header is its own ground, closed by the
   * `--line` hairline, so the card keeps its top edge and still reads as a card.
   *
   * ONE TOKEN, ONE LINE, so this is a decision that can be flipped without touching anything else. */
  background: var(--sunken);
  border-bottom: 1px solid var(--line);
  min-width: 0;
}
#att-view-card .rv-hero-main { min-width: 0; }
#att-view-card .rv-hero-line {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--s2);
  min-width: 0;
}
#att-view-card .rv-hero-t {
  margin: 0; font-size: 19px; line-height: 1.2; font-weight: 650;
  letter-spacing: -0.015em; color: var(--ink);
}
#att-view-card .rv-hero-why {
  margin: 6px 0 0; font-size: 13px; line-height: 1.45;
  color: var(--ink-3); max-width: 62ch; overflow-wrap: anywhere;
}
/* THE ID IS METADATA, NOT A TITLE. It was set at the heading's own size in the old layout, which made
   a 40-character token the loudest thing on the page. Mono, small, quiet, and it wraps rather than
   dragging the card sideways on a phone. */
#att-view-card .rv-hero-id {
  flex: 0 0 auto; font-size: 11px; color: var(--ink-4);
  background: var(--sunken); border: 1px solid var(--line);
  border-radius: var(--r-sm); padding: 3px 7px;
  max-width: 42%; overflow-wrap: anywhere;
}

/* ---- THE STANDING CHIP -------------------------------------------------------------------------- */
#att-view-card .rv-chip {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12px; font-weight: 600; line-height: 1.3;
  padding: 4px 10px 4px 8px; border-radius: var(--r-pill);
  border: 1px solid transparent; white-space: normal; min-width: 0;
  overflow-wrap: anywhere; text-align: left;
}
#att-view-card .rv-chip-dot {
  flex: 0 0 auto; width: 7px; height: 7px; border-radius: 50%;
  background: currentColor; box-shadow: 0 0 0 3px color-mix(in srgb, currentColor 18%, transparent);
}
#att-view-card .rv-chip-ok   { color: var(--ok);     background: var(--ok-soft);     border-color: var(--ok-line); }
#att-view-card .rv-chip-warn { color: var(--warn);   background: var(--warn-soft);   border-color: var(--warn-line); }

/* ---- THE FACTS ---------------------------------------------------------------------------------- */
#att-view-card .rv-facts {
  margin: 0; padding: var(--s2) 0 0;
  display: block;                       /* one list of rows, each row its own grid */
}
#att-view-card .rv-fact {
  display: grid; grid-template-columns: minmax(132px, 168px) 1fr;
  align-items: baseline; gap: var(--s3);
  padding: 9px var(--s5);
  min-width: 0;
}
/* A HAIRLINE BETWEEN ROWS, not a box around each: the row is the unit, and the line is what makes a
   nine-row list scannable without nine pieces of chrome. */
#att-view-card .rv-fact + .rv-fact { border-top: 1px solid var(--line); }
#att-view-card .rv-fact dt {
  font-size: 12px; font-weight: 600; color: var(--ink-4);
  letter-spacing: 0.01em; text-transform: none;
  min-width: 0; overflow-wrap: anywhere;
}
#att-view-card .rv-fact dd {
  margin: 0; font-size: 14px; line-height: 1.5; color: var(--ink);
  min-width: 0; overflow-wrap: anywhere;
}

/* ---- THE COUNT CHIPS ---------------------------------------------------------------------------- */
#att-view-card .rv-strip {
  display: flex; flex-wrap: wrap; gap: var(--s2);
  padding: var(--s3) var(--s5) var(--s4);
  border-top: 1px solid var(--line);
  background: var(--sunken);
}
#att-view-card .rv-jump {
  display: inline-flex; align-items: baseline; gap: 6px;
  font: inherit; cursor: pointer;
  padding: 5px 11px; border-radius: var(--r-pill);
  background: var(--raised); border: 1px solid var(--line-2);
  color: var(--ink-2);
  transition: background 120ms ease-out, border-color 120ms ease-out, color 120ms ease-out;
}
#att-view-card .rv-jump:hover { background: var(--hover); border-color: var(--line-3); color: var(--ink); }
/* FEEDBACK ON THE PRESS, NOT ON THE RELEASE — the pointer-down rule. A chip that only reacts once the
   click lands feels dead under the finger. */
#att-view-card .rv-jump:active { transform: translateY(0.5px); }
#att-view-card .rv-jump-n { font-size: 14px; font-weight: 650; color: var(--ink); font-variant-numeric: tabular-nums; }
#att-view-card .rv-jump-l { font-size: 12px; color: var(--ink-4); }

/* ---- THE SECTIONS ------------------------------------------------------------------------------- */
#att-view-card .rv-sec { border-top: 1px solid var(--line); padding: var(--s4) var(--s5) var(--s3); min-width: 0; }
#att-view-card .rv-sec-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--s3); flex-wrap: wrap; margin-bottom: var(--s2); min-width: 0;
}
#att-view-card .rv-sec-t {
  margin: 0; font-size: 13px; font-weight: 650; color: var(--ink-2);
  letter-spacing: 0.02em; text-transform: uppercase;
}
#att-view-card .rv-sec-m { font-size: 12px; color: var(--ink-4); min-width: 0; overflow-wrap: anywhere; }
#att-view-card .rv-none { margin: 0; }
#att-view-card .rv-foot {
  margin: var(--s2) 0 0; font-size: 12px; line-height: 1.5; color: var(--ink-4);
  max-width: 72ch;
}

/* A JUMP LANDS: the section it landed on draws the eye once and lets go. Not a scroll animation —
   the scroll is the browser's own — this is only so the destination is unmistakable. */
#att-view-card .rv-sec.rv-landed { animation: rvLand 700ms ease-out 1; }
@keyframes rvLand {
  0%   { background: var(--accent-soft); }
  100% { background: transparent; }
}
@media (prefers-reduced-motion: reduce) {
  /* Reduced motion is a gentler equivalent, never nothing: the scroll stops being smooth (handled in
     the renderer) and the ring becomes a static tint that simply goes away. */
  #att-view-card .rv-sec.rv-landed { animation: none; background: var(--accent-soft); }
  #att-view-card .rv-jump, #att-view-card .rv-back { transition: none; }
}

/* ---- THE DECISIONS ------------------------------------------------------------------------------ */
#att-view-card .rv-decide {
  border-top: 1px solid var(--line-2);
  background: var(--sunken);
  padding: var(--s3) var(--s5) var(--s4);
}
#att-view-card .rv-decide-why {
  font-size: 12px; line-height: 1.5; color: var(--ink-4);
  margin-bottom: var(--s3); max-width: 78ch; overflow-wrap: anywhere;
}
#att-view-card .rv-decide-acts { display: flex; flex-wrap: wrap; gap: var(--s2); align-items: center; }
/* THE WAY OUT SITS APART — it is navigation, not a decision about the delivery, and it is pushed away
   from the buttons that change the record so a mis-aimed click cannot land on one. */
#att-view-card .rv-back { margin-left: auto; }
#att-view-card .rv-decide-acts .btn { height: 34px; padding: 0 14px; font-size: 14px; }

/* ---- THE PICTURES, IN THE GALLERY SHAPE ---------------------------------------------------------- */
#att-view-card .rv-gallery { list-style: none; margin: 0; padding: 0; }
#att-view-card .rv-gallery > .att-photo {
  border: 1px solid var(--line); border-radius: var(--r-md);
  /* `--card` is not defined here either — see the note on `.att-photo-img img` above. `--raised`
     is the token the gallery's own photos and the card's sheet ground already use. */
  background: var(--raised); padding: 8px 10px;
}
#att-view-card .rv-gallery > .att-photo + .att-photo { margin-top: var(--s2); }
#att-view-card .rv-gallery .att-photo-head { padding: 2px 0; }
#att-view-card .rv-gallery .att-photo-fold[open] .att-photo-head { margin-bottom: 2px; }
#att-view-card .rv-gallery .att-photo-body { padding-left: 2px; }

/* ---- THE CORRECTIONS TRACE ---------------------------------------------------------------------- */
#att-view-card .rv-trace { list-style: none; margin: 0; padding: 0; counter-reset: rvstep; }
#att-view-card .rv-trace > li {
  position: relative; padding: 0 0 0 22px; margin: 0 0 var(--s3);
  font-size: 13px; line-height: 1.5; color: var(--ink-2);
}
/* A TRACE, DRAWN AS ONE: a dot per step and a spine joining them, so a delivery corrected twice reads
   as a sequence rather than as two more paragraphs of small print. */
#att-view-card .rv-trace > li::before {
  content: ""; position: absolute; left: 4px; top: 6px;
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft);
}
#att-view-card .rv-trace > li + li::after {
  content: ""; position: absolute; left: 7px; top: -8px; bottom: 100%;
  width: 1px; height: calc(100% + 14px); background: var(--accent-line);
}
#att-view-card .rv-flags { list-style: none; margin: 0; padding: 0; }
#att-view-card .rv-flags > li {
  font-size: 13px; line-height: 1.5; color: var(--ink-2);
  padding: 6px 0 6px 18px; position: relative;
}
#att-view-card .rv-flags > li::before {
  content: ""; position: absolute; left: 3px; top: 12px;
  width: 6px; height: 6px; border-radius: 1px;
  background: var(--warn); transform: rotate(45deg);
}
#att-view-card .rv-flags > li + li { border-top: 1px solid var(--line); }

/* ---- AT THE DESK THE READING GETS A COMFORTABLE MEASURE ------------------------------------------ */
@media (min-width: 900px) {
  #att-view-card .rv-fact { grid-template-columns: minmax(150px, 190px) 1fr; }
  #att-view-card .rv-hero { padding: var(--s5) var(--s6) var(--s4); }
  #att-view-card .rv-fact, #att-view-card .rv-sec, #att-view-card .rv-strip,
  #att-view-card .rv-decide { padding-left: var(--s6); padding-right: var(--s6); }
}

/* ---- ON A PHONE ---------------------------------------------------------------------------------- */
@media (max-width: 560px) {
  #att-view-card .rv-hero { flex-direction: column; gap: var(--s2); padding: var(--s4); }
  #att-view-card .rv-hero-id { max-width: 100%; flex: 0 1 auto; }
  #att-view-card .rv-hero-t { font-size: 17px; }
  /* One column: the label ABOVE its value, the shape every other label/value pair on a phone uses. */
  #att-view-card .rv-fact { grid-template-columns: 1fr; gap: 2px; padding: 9px var(--s4); }
  #att-view-card .rv-fact dt { margin-bottom: 1px; }
  #att-view-card .rv-fact, #att-view-card .rv-sec, #att-view-card .rv-strip,
  #att-view-card .rv-decide { padding-left: var(--s4); padding-right: var(--s4); }
  #att-view-card .rv-decide-acts { flex-direction: column; align-items: stretch; }
  #att-view-card .rv-decide-acts .btn { text-align: center; width: 100%; }
  /* THE WAY OUT KEEPS ITS OWN ROW and its own margin reset, or the auto margin would push it. */
  #att-view-card .rv-back { margin-left: 0; margin-top: var(--s1); }
  #att-view-card .rv-gallery > .att-photo { padding: 8px 9px; }
  #att-view-card .rv-jump { flex: 1 1 auto; justify-content: center; }
}


/* ==================================================================================================
 * THE VIEW BUTTON WAS GREY (Kazim, 2026-09-28): "so the View button is grayed it doesn't look as
 * bright as Report deliveries."
 *
 * THE CAUSE, MEASURED, and it is not a colour: `.inf-attempt-unapproved { opacity: .66 }` is set on
 * the whole `<li>`. The View button is INSIDE that row, so it rendered at 66% against the same `btn`
 * class as "Report deliveries" — which is NOT inside a greyed row. Two buttons with identical classes,
 * identical background, identical border, differing only by what their ancestor did to them.
 *
 * THE FIX IS THE MECHANISM THAT WAS ALREADY HERE. The row already re-states its own colours for the
 * unapproved state (`.inf-attempt-unapproved[data-ok="served"] { border-left-color: var(--ink-4) }`),
 * i.e. the design already intends the state to be expressed by TONE, not by fading the row. Opacity is
 * the one tool that cannot be undone from inside — no descendant can raise it — so the row stops
 * fading itself and states its state with the tint it already declares.
 *
 * WHAT THE STATE STILL SAYS, unchanged: the row is still tinted, its served-edge is still the muted
 * ink rather than the accent, and the `inf-unapproved-flag` badge still says in words that the
 * delivery does not count. NOTHING about the state is weakened — only the button stops inheriting it,
 * because the button is the way to READ the delivery, and reading it is how a person decides.
 *
 * APPEND-ONLY at the tail: the `.inf-attempt-unapproved` rule above is left exactly as it is, so a
 * check that pins it still finds it. This only re-states the row's own colour and takes the fade off
 * the act cluster.
 * ================================================================================================== */

/* THE ROW STATES ITS STATE IN COLOUR INSTEAD OF BY FADING EVERYTHING IT CONTAINS. */
.inf-attempt.inf-attempt-unapproved { background: var(--sunken); }

/* THE FADE COMES OFF THE ACT CLUSTER — the one part of the row that is a CONTROL. The row's own text
   keeps the muted tone it had, which is what made the state readable in the first place. */
.inf-attempt-unapproved > .inf-attempt-acts { opacity: 1; }

/* AND THE VIEW BUTTON IS EXACTLY THE PAGE'S OWN BUTTON, at full strength, on every row: the same
   `btn` metrics as "Report deliveries" whether the delivery counts or not. Stated rather than
   inherited, so a later tint on a future row state cannot fade it again by accident.

   THE GEOMETRY IS COPIED FROM `.btn`, AND THE FIRST CUT COPIED IT FROM SOMEWHERE ELSE. It read
   `font-size: 14px; font-weight: 500` while its own comment said "the same `btn` metrics as Report
   deliveries" — but `.btn` (line 213) is `font-size: 13px; font-weight: 560`. Measured 4x-zoomed,
   side by side: the View button's glyphs were LIGHTER and thinner than Report deliveries', which is
   exactly the "greyed, doesn't look as bright" Kazim reported, and it was the whole of what was left
   of the defect after the opacity cause above was fixed. A control is not the place to invent metrics:
   these are `.btn`'s own numbers, so the two buttons are one button drawn twice. */
.inf-attempt .inf-attempt-acts .btn[data-att-view] {
  opacity: 1;
  /* the same geometry the sibling buttons carry — `.btn`'s, not a lookalike */
  height: 32px; padding: 0 12px; font-size: 13px; font-weight: 560;
}


/* ---- THE ACTION ROW, SECOND PASS: SEPARATE WHAT CHANGES THE RECORD FROM WHAT DOES NOT ------------
 *
 * MEASURED on the rendered card: three buttons at 34px (37 device px at the console's 1.1 zoom —
 * 34 x 1.1 = 37.4, so the geometry the rule asks for IS what is drawn) sitting in one flat row with
 * the ghost "Back to the job page" pushed right by an auto margin. That reads as four peers of
 * differing importance rather than as two groups, and "Delete" — the one irreversible act — sits
 * next to "Edit" where a mis-aimed press can reach it.
 *
 * `bindAttemptDecisions` owns these controls, so the SPLIT IS DRAWN FROM THE ACT ITSELF rather than
 * from a class the renderer has to remember to add: the destructive acts already carry `.danger`
 * (the console's own convention, and what the reading's check asserts), so `.danger` is what sets the
 * confirming group apart. Nothing about behaviour changes — only the space between the groups.
 */
#att-view-card .rv-decide-acts { row-gap: var(--s3); }
#att-view-card .rv-decide-acts .btn.danger { margin-left: var(--s3); }
#att-view-card .rv-decide-acts .btn.primary + .btn.danger { margin-left: 0; }
#att-view-card .rv-decide-acts .rv-back { margin-left: var(--s5); }

/* A DESTRUCTIVE ACT IS QUIET UNTIL IT IS MEANT. On this card Delete is `btn danger`, which paints a
   red word on a neutral surface; at rest it stays neutral and the red arrives on hover, so the reader
   scanning for "Approve" is not pulled at by a red button they almost certainly do not want. */
#att-view-card .rv-decide-acts .btn.danger {
  color: var(--ink-2);
  border-color: var(--line-2);
  background: var(--raised);
}
#att-view-card .rv-decide-acts .btn.danger:hover:not(:disabled) {
  color: var(--danger); background: var(--danger-soft); border-color: var(--danger-line);
}

@media (max-width: 560px) {
  /* One column on a phone: every margin reset, or the auto/offset margins push buttons off the edge
     of a 390px screen. */
  #att-view-card .rv-decide-acts .btn.danger,
  #att-view-card .rv-decide-acts .rv-back { margin-left: 0; }
}

/* ---------------------------------------------------------------- ARCHIVE: the act is GREEN
   Kazim, 2026-09-28: «change the mark as complete button instead as archive and make it a green
   button. functions the same just change what it says and looks.» ADDED, never rewritten — this
   file is edited by other sessions and the last writer wins with no merge, so a new scoped block
   cannot take a rule away from anyone.

   The act files a job away and is the console's own mark of settled work, so it wears `--ok`, the
   token this design already reserves for exactly that (`app.css:7`: "green for settled work").

   THE LABEL FOLLOWS THE THEME, like `.btn.primary`, and it is NOT one white word. MEASURED on the
   rendered button: this token is `--ok #1f6b45` in light and lightens to `#6fc094` in dark, where a
   white label would sit at 2.18:1 — unreadable, and this project has already been bitten by a
   light-theme-only reading. Following `--bg` measures 5.65:1 light and 8.10:1 dark on the real page,
   both above AA for a 13px label. */
.btn.ok-solid {
  background: var(--ok); border-color: var(--ok); color: var(--bg);
  box-shadow: var(--e1), inset 0 1px 0 rgba(255,255,255,0.10);
}
.btn.ok-solid:hover:not(:disabled) { background: var(--ok-hi); border-color: var(--ok-hi); }
.btn.ok-solid:disabled { background: var(--sunken); border-color: var(--line); color: var(--ink-4); box-shadow: none; }

/* ONE STEP FURTHER OUT IN EACH THEME, declared per theme so the hover is a darkening on a light
   page and a lightening on a dark one — the same direction `.primary`/`.danger-solid` take. */
:root { --ok-hi: #175535; }
[data-theme="dark"], [data-theme="system"][data-prefers="dark"] { --ok-hi: #8ad0ab; }


/* ==================================================================================================
 * ONE LANGUAGE, EVERYWHERE — the reading's design, applied to the rest of the console
 * ==================================================================================================
 *
 * THE ASK (Kazim): one page was right, and the rest of the console should be brought to it. The page
 * is the ATTEMPTED-DELIVERY READING — `#att-view-card`, the `rv-` family, further up this file under
 * "THE READING, REDESIGNED". Its grammar, in the order it draws itself:
 *
 *   1. THE CARD is a surface that SITS ON the sheet rather than content poured onto it: `--raised`
 *      ground, a `--line` hairline, `--r-lg`, `--e2`. Everything below happens inside that frame.
 *   2. THE HEAD IS A BAND — `--accent-soft`, closed by a hairline — at the card's own type size (19px/
 *      650 title, a 13px `--ink-3` line of *why* under it, the record id as an 11px mono chip). The
 *      band is what makes a card recognisable at a glance from across the desk.
 *   3. THE FACTS ARE ROWS, separated by ONE hairline each and never boxed: a 12px `--ink-4` label, a
 *      14px `--ink` value, 9px of vertical rhythm. Nine rows stay scannable without nine pieces of
 *      chrome.
 *   4. THE SECTIONS are separated by that same hairline and headed by a 13px/650 UPPERCASE `--ink-2`
 *      title, with 12px `--ink-4` meta on the right.
 *   5. THE STRIP AND THE FOOTER are `--sunken` bands closed by a hairline: counts in one, the acts
 *      that change the record in the other.
 *   6. THE CHIPS ARE PILLS at 11px or above — a tinted ground, a matching border, the tone in the ink.
 *      Colour is meaning, never decoration.
 *
 * WHY THIS IS APPENDED AND NOT AN EDIT. This file has several writers and no merge; the last writer
 * wins. A rule changed in place is a rule taken from whoever wrote it. So the rules below are
 * APPENDED, and the ones they supersede are left exactly where they are — a check that pins one of
 * them still finds it. Where a rule above is genuinely WRONG (an undefined token, a size under the
 * 11px floor) the fix is made IN PLACE and named in its comment, so it is auditable.
 *
 * WHAT DID NOT CHANGE, on purpose, and it is most of it: no wording, no id, no class, no `data-`
 * attribute, no element, no order, no behaviour. This block sets spacing, radii, hairlines, grounds,
 * elevation, type size and weight, and the chip / badge / section-head treatment. Nothing here can
 * make the console do anything it did not already do.
 *
 * NO GRADIENT (the standing instruction: a gradient reads as chrome, not as design). Every band here
 * is a FLAT token. The stylesheet's only gradient is the loading skeleton's sweep, which is functional.
 *
 * TOKENS ONLY. Every custom property used below is declared in `:root` and in the dark block at the
 * top of this file. `--card`, `--panel`, `--fg`, `--sans`, `--surface-2` and `--line-1` are NOT
 * declared and are therefore never used: a `var()` naming an undeclared property with no fallback is
 * invalid at computed-value time and silently inherits, and with a dark fallback it paints dark in
 * BOTH themes — the defect `verify_theme_tokens.py` exists to catch, and it had caught two of them in
 * the reading itself (`.att-photo-img img`, `.rv-gallery > .att-photo`), now fixed in place above.
 *
 * SCOPE, AND WHY IT IS BY OBJECT RATHER THAN BY ANCESTOR. MEASURED first, through `getComputedStyle`
 * on the rendered page: Settings, Notifications and the Archive are `body`-level overlays — they are
 * NOT inside `#stage` — while the workflow home, the case list, the Hague steps, the informal job, the
 * affidavit and the translations step are. The workflow tab strip, the rail's column heading and the
 * case header's status chip are likewise outside `#stage`. So the classes below are scoped by the
 * OBJECT they name (`.home-card` is one card wherever it is drawn — the tabs' lists and the archive's
 * are the same markup, measured), except where a class family exists in more than one pane in two
 * different shapes, which is stated at the rule.
 *
 * MEDIA QUERIES ADD NO SPECIFICITY, so an unscoped rule would silently override a phone rule written
 * earlier in this file. Everything that touches a property a phone block also sets — padding, gaps,
 * the head's type size — is inside `@media (min-width: 721px)` and mirrored in the phone block at the
 * foot of this section. Radii, hairlines, grounds and chip metrics are safe unscoped: no phone rule
 * competes for them.
 * ================================================================================================== */


/* ------------------------------------------------------------------ A. THE CARD — the shell
 * MEASURED before this block: `#stage .step-card` computed `border: 0px none`, `border-radius: 0px`,
 * `background: rgba(0, 0, 0, 0)`. The Hague and Translations steps, and the informal job's card, were
 * bare content laid straight onto the sheet with no edge between the step and the pane around it.
 * Only the reading had a frame — which is most of why it reads as designed and the rest does not.
 *
 * `.rv` IS EXCLUDED: the reading already carries this exact frame in its own block, and re-stating it
 * would make one card depend on two places. `.affdoc` IS EXCLUDED for a different reason: the
 * affidavit editor already draws its own frame (`.affed`, hairline + `--r-lg` + a `--bg` sheet), so a
 * second one around it would be a box drawn twice. The affidavit's language arrives through its heads,
 * its field boxes and its chips instead — sections B, C and L.
 */
#stage .step-card:not(.rv):not(.affdoc) {
  background: var(--raised);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--e2);
  min-width: 0;
}
/* THE INNER MEASURE IS A MEDIA QUERY because `.inf-card { padding: var(--s3) }` is set by the phone
   block above, and an unscoped padding here would take the phone its own tighter gutter away. */
@media (min-width: 721px) {
  #stage .step-card:not(.rv):not(.affdoc) { padding: var(--s4) var(--s5) var(--s5); }
}

/* THE CARD'S TITLE WEARS THE READING'S TITLE. `#att-view-card .rv-hero-t` is 19px/650/-0.015em with a
   13px/1.45 `--ink-3` line under it. The step cards' and the informal card's titles were 15px/680 on
   the desk and 17px on a phone, with the explanatory line at `--ink-2` — two titles for one role. */
@media (min-width: 721px) {
  #stage .step-card-head h3,
  #stage .inf-head h3 {
    font-size: 19px; line-height: 1.2; font-weight: 650; letter-spacing: -0.015em;
  }
  #stage .step-card-head p { font-size: 13px; line-height: 1.45; max-width: 68ch; }
}
@media (max-width: 720px) {
  /* The phone keeps the size its own block already set (17px) and takes only the weight and line, so
     nothing a phone has today shrinks. */
  #stage .inf-head h3 {
    font-size: 17px; line-height: 1.2; font-weight: 650; letter-spacing: -0.015em;
  }
}
#stage .step-card-head p { color: var(--ink-3); }

/* THE HEAD IS A BAND. The reading's header is a NEUTRAL `--sunken` band closed by a `--line` hairline
   (app.css:8364 — it is NOT the accent; an earlier cut of the reading left blue there and this block
   copied it, which put a blue top on every step card), and it is the card's loudest single marker of
   "this is a card". It reaches the card's own edges by pulling the head out of the shell's padding by
   exactly that padding. NOTHING HERE USES `overflow: hidden`: an absolutely-positioned child (a
   snippet's own tools, a picker popup) must never be clipped by a frame it merely lives inside — the
   same trap `verify_affidavit_snippet_blend.py` was written for.

   THE READING AND THE AFFIDAVIT EDITOR ARE EXCLUDED by the same `:not()` pair as the shell above: the
   reading's head is its own `.rv-hero`, and the editor's `.affed` already draws a header strip. A band
   put on either of them would be a second header on one card. */
@media (min-width: 721px) {
  #stage .step-card:not(.rv):not(.affdoc) > .step-card-head:first-child,
  #stage .step-card:not(.rv):not(.affdoc) > .inf-head:first-child,
  #stage .step-card:not(.rv):not(.affdoc) > .inf-section:first-child > .inf-head:first-child {
    margin: calc(-1 * var(--s4)) calc(-1 * var(--s5)) var(--s3);
    padding: var(--s4) var(--s5) var(--s3);
    background: var(--sunken);
    border-bottom: 1px solid var(--line);
    border-radius: calc(var(--r-lg) - 1px) calc(var(--r-lg) - 1px) 0 0;
  }
}

/* THE ACT ROWS ARE HAIRLINE-SEPARATED, like the reading's facts: one line, then space. That is the
   whole of the reading's grammar for "one thing ends, the next begins". */
#stage .inf-actions,
#stage .stage-btnrow,
#stage .intake-actions,
#stage .inf-next { border-top: 1px solid var(--line); }
@media (min-width: 721px) {
  #stage .inf-actions,
  #stage .stage-btnrow,
  #stage .intake-actions,
  #stage .inf-next { padding-top: var(--s3); }
}


/* ------------------------------------------------------------------ B. THE SECTION HEADS
 * ONE treatment for "this names a section", taken from `#att-view-card .rv-sec-t`: 13px, 650,
 * uppercase, 0.02em, `--ink-2`, with the section's meta at 12px `--ink-4` on the right.
 *
 * THE CONSOLE HAD SIX. MEASURED, every one of them naming a whole section of a card or a pane:
 * 12px/680/0.1em/`--ink-3` (`.pset-head h2`, `.col-head h2`, `.home-section-head h2`), 12px/680/
 * 0.07em/`--ink-3` (`.aff-head`), 11px/680/0.1em/`--ink-3` (`.tr-block h4`, `.ap-head`), 11px/660/
 * 0.09em/`--ink-3` (`.notif-head`), and nothing at all for `.inf-col h4`. Six sizes and four colours
 * for one role is the incoherence the operator can see without being able to name it.
 *
 * WHAT IS DELIBERATELY NOT IN THIS LIST: `.aff-field-label`, `.att-k`, `.instruct-label`,
 * `.att-panel h3` and `.informal-blocked h4`. Those are not section heads — they are micro-labels on
 * a single field or inside a warning box, and DESIGN.md reserves 11px for exactly that. The reading
 * agrees: its own field labels (`.rv-fact dt`) are 12px, NOT uppercase, and stay that way (section D).
 *
 * ONE EXCEPTION: the Settings section heads. They take the same ink and weight but are NOT uppercased
 * — they are sentences naming an area of the office's work, not column labels, and a sentence shouted
 * in caps is a different thing from a sentence. Handled in section H.
 */
.pset-head h2,
.col-head h2,
.home-section-head h2,
.aff-head,
.tr-block h4,
.ap-head,
.notif-head,
.inf-col h4 {
  color: var(--ink-2);
  font-weight: 650;
  text-transform: uppercase;
  letter-spacing: 0.02em;
}
/* The size and the tracking are two properties a phone block also sets on the home headings, so they
   are stated per width. A phone keeps the step down its own block already gave it. */
@media (min-width: 721px) {
  .pset-head h2,
  .col-head h2,
  .home-section-head h2,
  .aff-head,
  .tr-block h4,
  .ap-head,
  .notif-head,
  .inf-col h4 { font-size: 13px; }
  .inf-col h4 { margin: var(--s3) 0 var(--s2); }
}
@media (max-width: 720px) {
  .pset-head h2,
  .col-head h2,
  .home-section-head h2,
  .aff-head,
  .tr-block h4,
  .ap-head,
  .notif-head,
  .inf-col h4 { font-size: 12px; }
}
/* THE META ON THE RIGHT OF A HEAD, in the reading's own voice: 12px `--ink-4`, allowed to shrink
   rather than push the head's title off the row. */
.pset-counts,
.inf-count,
.home-count,
.inf-prog-quiet { font-size: 12px; color: var(--ink-4); min-width: 0; overflow-wrap: anywhere; }
/* `.tr-count` draws its own separator dot before the count. The reading separates a head's meta with
   space, not a glyph, so the dot goes. It is `content`, not text: nothing readable is lost. */
.tr-count::before { content: none; }


/* ------------------------------------------------------------------ C. THE CHIPS
 * THE READING'S CHIP, exactly: `--r-pill`, a 1px border, a tinted ground, the tone carried by the ink,
 * at 12px/600 for a count and 11px for metadata. Applied to every badge in the console so one chip
 * reads the same wherever it appears.
 *
 * TWO REAL DEFECTS ARE FIXED HERE, both against the console's own stated rules:
 *
 *   1. THE 11px FLOOR. "11px is reserved for uppercase tracked micro-labels; everything functional is
 *      12px or above" (DESIGN.md) — the same floor `.home-kind`'s own comment states in so many words
 *      ("11px, NOT 10px: the floor", after an audit measured 10px as below the legibility floor).
 *      NINETEEN rules in this stylesheet set a font-size under 11px; the ones below are the chips —
 *      `.att-tag`, `.aff-flag`, `.st-chip`, `.nf-kind`, `.ep-kind`, `.disp-chip`, `.map-chip` and the
 *      metadata keys — every one of them a badge a person has to read. They are 11px now. The pure
 *      glyph carets (`.conf-caret`, `.cand-ladder > summary .caret`) are left exactly as they are:
 *      they are arrow shapes, not text.
 *
 *   2. SIX RADII FOR ONE OBJECT. A chip is a pill in the reading and through most of this file, but
 *      `.st-chip` was 10px, `.ep-kind` 3px and `.inf-flag` 2px — the same kind of badge was a pill on
 *      one screen and a small rectangle on the next. They are all `--r-pill` now.
 */
.att-tag,
.aff-flag,
.st-chip,
.nf-kind,
.ep-kind,
.disp-chip,
.map-chip,
.inf-flag,
.tr-tag,
.pset-tag,
.owner-chip,
.home-kind,
.att-k,
.st-step-n,
.disp-hist-act,
.st-move-act,
.affed-title,
.affed-card h4,
.affed-group > summary,
.aff-snippet figcaption { font-size: 11px; letter-spacing: 0.06em; }

.att-tag,
.aff-flag,
.st-chip,
.nf-kind,
.ep-kind,
.disp-chip,
.map-chip,
.inf-flag,
.tr-tag,
.pset-tag,
.owner-chip,
.home-kind { border-radius: var(--r-pill); padding: 1px 8px; }

/* A CHIP WITH NO TONE SITS ON THE SUNKEN SURFACE BEHIND A HAIRLINE — the reading's own recipe for its
   quiet chip (`#att-view-card .rv-hero-id`: `--sunken`, `--line`, 11px). */
.aff-flag.aff-flag-quiet,
.att-tag.quiet,
.tr-tag.quiet,
.pset-tag.meta,
.nf-kind { color: var(--ink-3); background: var(--sunken); border-color: var(--line); }

/* THE STATUS CHIP IS THE CONSOLE'S MOST-SEEN BADGE, on the case header, on every rail row and on every
   card. It already used the tone tokens; what it did not have was the reading's weight and line, and
   at a 44vw cap on a phone it needs to be able to wrap rather than clip. */
.state {
  font-size: 11px;
  font-weight: 620;
  letter-spacing: 0.06em;
  border-radius: var(--r-pill);
  padding: 2px 9px;
  min-width: 0;
  overflow-wrap: anywhere;
}
/* THE ONE CHIP THAT WAS GREY ON GREY. `.st-chip-unknown` set `--line-3` for its border and nothing
   else, while every other state in that family carries its tone's ground and line. An "unknown" state
   is the amber "confirm this" language this console already owns, so it takes the amber trio. */
.st-chip-unknown {
  color: var(--warn);
  background: var(--warn-soft);
  border-color: var(--warn-line);
}


/* ------------------------------------------------------------------ D. THE FACT ROWS AND THE LISTS
 * THE READING'S ROW, verbatim: one hairline above each row after the first, a 12px/600 `--ink-4`
 * label, a 14px/1.5 `--ink` value, 9px of vertical rhythm. The label column is a fixed measure at the
 * desk so a column of values lines up and the eye can scan it, one column on a phone.
 *
 * The informal job's key/value list was already this shape (`.inf-kv`), which is the one place the two
 * languages had already met. The reading's numbers are now its numbers too.
 */
.inf-kv > dt {
  font-size: 12px; font-weight: 600; color: var(--ink-4); letter-spacing: 0.01em;
}
.inf-kv > dd { font-size: 14px; line-height: 1.5; color: var(--ink); }
@media (min-width: 721px) {
  .inf-kv { gap: 9px var(--s3); }
}
/* A LIST OF RECORDS IS SEPARATED BY ONE SOLID HAIRLINE, never a dashed one. The reading's row rule is
   `1px solid var(--line)`; `.inf-list li` was `1px dashed`. A dashed rule reads as "provisional" in
   this console — it is what `.inf-drop` and a not-yet-committed row use on purpose — so a list of
   settled records was saying something about itself that was not true. Solid, same token. */
.inf-list li { border-bottom: 1px solid var(--line); padding: 6px 0; }
.inf-list li:last-child { border-bottom: 0; }
/* THE READING'S OWN TWO LISTS ARE PUT BACK EXACTLY AS THEY WERE, and this is not a formality: the
   rule above TIES on specificity with `#att-view-card .rv-trace > li` (1,2,1 each) and sits LATER in
   the file, so without this it would win and strip the 22px left padding the trace's dot and spine
   are drawn into — and add a hairline under every step and every flag, where the reading deliberately
   has none (its flags already carry `border-TOP` between rows, and a line after the last row is a
   line the reference does not draw). MEASURED as a 3.65% pixel diff on the reference card before this
   restore; the trace and flag geometry is the design Kazim named, so it is pinned here verbatim. */
#att-view-card .rv-trace > li { border-bottom: 0; padding: 0 0 0 22px; }
#att-view-card .rv-flags > li { border-bottom: 0; padding: 6px 0 6px 18px; }
/* A NOTE UNDER A THING IS THE READING'S FOOT: 12px/1.5 `--ink-4`, capped at a readable measure so a
   long sentence does not run the width of a wide card or a page-sized panel. */
.rv-foot,
.pset-note,
.inf-quiet,
.notif-head { color: var(--ink-4); }
@media (min-width: 721px) {
  .pset-note { max-width: 78ch; overflow-wrap: anywhere; }
}


/* ------------------------------------------------------------------ E. THE BANDS
 * THE READING'S STRIP AND FOOTER: a `--sunken` band closed by a hairline, for a panel of counts and
 * for the acts that change the record.
 *
 * The informal job's progress panel was a `--sunken` panel at a 4px radius — a radius nothing else in
 * the console uses — and the share block was a bare 3px box. Both stand beside the card, so both take
 * the card's own radius and the reading's band treatment.
 */
.inf-progress {
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--sunken);
  min-width: 0;
}
.inf-scriptbar {
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--sunken);
  min-width: 0;
}
.inf-share {
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--raised);
  min-width: 0;
}
/* THE STEP YOU ARE ON IS LIT LIKE THE CURRENT THING ELSEWHERE. The reading marks the current item with
   the accent on the accent's own soft ground (`.rv-chip`, `.rv-trace > li::before`), so the row you
   are standing on is accent ink on `--accent-soft` rather than a plain surface with an inset bar —
   which also removes the one hand-rolled `inset` shadow in the informal flow. */
.inf-prog-step.here {
  background: var(--accent-soft);
  color: var(--ink);
  box-shadow: none;
}


/* ------------------------------------------------------------------ F. THE TAB STRIPS
 * ONE tab drawn twice in this console: the `.wf-tabs` workflow strip (a sunken track whose current
 * cell is raised on `--raised` with a `--line-2` border and `--e1`) and the Settings strip. The
 * informal job's three tabs were a third drawing of it, at a 3px radius with no elevation.
 *
 * The informal tabs keep their distinct SHAPE — a label with its own note underneath, standing on a
 * hairline — because that shape carries information the workflow strip does not (each tab states what
 * it currently holds). What they take is the family's dress: the radius, and `--e1` on the current
 * cell, so "where am I" reads the same way on both strips.
 */
.inf-tab {
  border-radius: var(--r-sm) var(--r-sm) 0 0;
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease),
              color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.inf-tab.on { border-color: var(--line-2); background: var(--raised); box-shadow: var(--e1); }
.inf-tab-note { font-size: 11px; }
/* The rail's step cells take the same elevation on the cell that is current, so the case's step rail
   and the workflow's tab strip read as one control family. */
.step.on { box-shadow: var(--e1); }
.st-tab { border-radius: var(--r-sm) var(--r-sm) 0 0; }
.st-tab[aria-selected="true"] { box-shadow: var(--e1); }


/* ------------------------------------------------------------------ G. THE CASE HEADER AND THE CASE CARDS
 * The case header was already close — a hairline under it, a 20px title, the record id in mono. It
 * takes the reading's title numbers: the console's own 20px step, at the reading's 650 weight and
 * tracking rather than 680/-0.025em.
 */
.case-head h1 {
  font-size: 20px;
  font-weight: 650;
  letter-spacing: -0.015em;
  line-height: 1.2;
}
/* THE CASE CARD — the same object on the home page, on a workflow's list and inside the archive
   (MEASURED: `#archive-list .home-card` is the same markup), so it is scoped by the class and not by
   an ancestor. The rail's own rows (`.entry`) are an INDEX inside one column and stay flush: a card
   inside a card is not what the reading does, and a hairline-separated index row is already the right
   object. */
.home-card {
  border-radius: var(--r-lg);
  background: var(--raised);
  box-shadow: var(--e1);
}
.home-card:hover { box-shadow: var(--e2); }
/* The inner measure: at the desk no other rule competes for `.home-card`'s padding (the phone's own
   block sets it below 721px), so this is stated in the desk range and mirrored on the phone in M. */
@media (min-width: 721px) {
  .home-card { padding: var(--s4); gap: var(--s3); }
}
/* THE OVERVIEW'S CARDS keep their ground and take the same radius, so the home page and a workflow's
   list are visibly the same object. */
.ovw-card { border-radius: var(--r-lg); }
.ovw-card-head { background: var(--sunken); border-bottom: 1px solid var(--line); }
/* AN EMPTY STATE IS A QUIET LINE, not a centred paragraph. */
.home-empty,
.ar-note { color: var(--ink-3); }


/* ------------------------------------------------------------------ H. SETTINGS
 * The Settings pane is a page-sized surface, and its parts are the same objects as everywhere else: a
 * card, a band, a chip, a tab strip. What it was drawing was an 8px card, a 6px panel, a 10px chip and
 * `.st-chip` at 0.8em — four numbers for three objects. MEASURED on the rendered pane:
 * `.st-team-row` r=8px on `--surface`, `.st-panel` r=6px on `--raised`, `.st-chip` r=10px.
 */
.st-team-rows { gap: var(--s3); }
.st-team-row {
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--raised);
  box-shadow: var(--e1);
}
/* A PANEL INSIDE A CARD IS THE SUNKEN ONE — the reading's own `--sunken` strip inside a `--raised`
   card — so the two levels are told apart rather than both being the same ground. */
.st-panel {
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--sunken);
}
.st-panel-title { font-weight: 650; letter-spacing: -0.005em; }
/* THE SETTINGS SECTION HEAD takes section B's ink and weight and is deliberately NOT uppercased: it
   names an area of the office's work in a sentence, and a sentence in caps is a different thing. */
.st-section > .st-h,
.st-section > h3,
.st-section > h4 { color: var(--ink-2); font-weight: 650; }
.st-banner { letter-spacing: -0.005em; }


/* ------------------------------------------------------------------ I. NOTIFICATIONS
 * A notification row IS a card. It was drawing a 10px radius on a `color-mix()` ground — a translucent
 * raised surface — while every other card in the console is `--raised` at `--r-lg`. A translucent card
 * sitting on a card is not a material this console has anywhere else, so the row is stated plainly.
 */
.nf-item {
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--raised);
  padding: var(--s3) var(--s4);
  margin-bottom: var(--s2);
}
.nf-item:hover { border-color: var(--line-3); }
/* READ IS A TONE, NOT A FADE. `.nf-item.read { opacity: .62 }` fades the WHOLE row including its
   controls — the exact mechanism that made the reading's View button look greyed (see "THE VIEW
   BUTTON WAS GREY" above, where the console already concluded that opacity is the one tool that
   cannot be undone from inside). A read row says so with its ink and its mark instead, and its acts
   stay at full strength. NOTHING about the state is weakened: the row is still tinted, the mark is
   still quiet, and the head still lightens. */
.nf-item.read { opacity: 1; }
.nf-item.read .nf-head { color: var(--ink-3); font-weight: 400; }
.nf-item.read .nf-kind { color: var(--ink-4); background: var(--sunken); border-color: var(--line); }
/* A ROW'S HEAD IS A TITLE, at a size the scale actually has rather than 13.5px, which is a step
   between two steps. Its body is the next step down. */
.nf-head { font-size: 14px; line-height: 1.5; color: var(--ink); }
.nf-text { font-size: 13px; line-height: 1.5; color: var(--ink-2); }
.nf-when { font-size: 12px; color: var(--ink-4); }
/* THE COMPOSED MESSAGE IS PROSE A CLIENT READS, capped at a readable measure. */
@media (min-width: 721px) {
  .notif-text { max-width: 78ch; }
}


/* ------------------------------------------------------------------ J. THE ARCHIVE
 * The archive pop-up IS the console's other case list — its rows are the same `.home-card` the tabs
 * draw, so the list itself needs nothing and inherits section G. What needed stating was the frame
 * around the list, and the search field's fit with the card language.
 *
 * THE SEARCH FIELD IS A PILL AND STAYS ONE. `verify_case_archive_ui.py` asserts the search is rounded
 * (its radius at least half its height, measured) and centred at the top of the pane; this block
 * changes neither. It touches only the grounds and the focus ring.
 */
.ar-view { background: var(--surface); }
@media (min-width: 721px) {
  .ar-view { padding: var(--s4) var(--s5) var(--s5); }
}
.ar-search-in {
  background: var(--sunken);
  border: 1px solid var(--line);
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease),
              box-shadow var(--dur) var(--ease);
}
.ar-search-in:focus { background: var(--raised); border-color: var(--accent); box-shadow: var(--ring); }
.ar-entry { box-shadow: var(--e1); }
.ar-list { padding: var(--s3) 0 0; }


/* ------------------------------------------------------------------ K. THE TRANSLATIONS AND REQUIREMENTS PANELS
 * `.tr-box` and `.tr-block` are the reading's own `.rv-sec` shape: a headed panel. They were an 8px
 * radius on `--raised` with an 11px uppercase head; the head is section B's now, and the panel takes
 * the card's radius and the card's hairline. A document row inside a panel is a hairline-separated row
 * (section D) and its tag is a chip (section C).
 *
 * `.tr-block` IS DECLARED TWICE in this stylesheet — once at `--r-md` on `--raised`, and again further
 * down at `--r-lg` on `--surface` — so the later one was winning and the earlier was dead text. It is
 * stated once here, so which panel is which is legible from one place.
 */
.tr-box,
.tr-block {
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--raised);
  min-width: 0;
}
.tr-block .tr-block { background: var(--sunken); border-radius: var(--r-md); }
.tr-table th {
  font-size: 11px; letter-spacing: 0.06em; color: var(--ink-3);
  border-bottom: 1px solid var(--line);
}
.tr-table td { border-bottom: 1px solid var(--line); }


/* ------------------------------------------------------------------ L. THE AFFIDAVIT SURFACES
 * THE AFFIDAVIT ALREADY HAS ITS CARD (`#stage .step-card.affdoc`, excluded from A, whose frame is
 * `.affed`). What it takes from the reading is the language of the parts inside:
 *
 *   * A FIELD IS A SMALL CARD — the reading's `.att-photo` is `--line` + `--r-md` + `--raised`, and
 *     `.aff-field` was already that shape. It keeps it; what was inconsistent was the quiet label and
 *     the heading sizes, both fixed by B and D.
 *   * THE SHEET'S OWN TYPE IS THE DOCUMENT'S AND IS NEVER TOUCHED by this block. `.aff-line`,
 *     `.affed-sheet`, `.fbox` and the print path are deliberately left alone: a document that will be
 *     signed and filed does not take the console's type scale, and changing it would change what
 *     prints. `verify_affidavit_sheet_pages.py` measures those lines — correctly — as unchanged.
 */
.aff-field { border-radius: var(--r-md); }
.aff-field-label { font-size: 11px; letter-spacing: 0.06em; color: var(--ink-4); }
.aff-factlist li { color: var(--ink-2); }
.affdoc-run-label { color: var(--ink-4); letter-spacing: 0.06em; }
.affdoc-run { border-bottom: 1px solid var(--line); }
.affdoc-page { border-radius: var(--r-md); }


/* ------------------------------------------------------------------ M. THE PHONE
 * THE PHONE KEEPS ITS OWN LAYOUT. Nothing in this block changes a phone's STRUCTURE: the drill-down,
 * the 44px targets, the single column, the fixed header and the sheet-picker are all deliberate and
 * all untouched. What a phone takes is the same LANGUAGE at the phone's own sizes — the card's radius
 * and hairline, the banded head, the section heads, the chips at the 11px floor.
 *
 * `.wf-sheet` is the phone's step picker and already wears the console's own material (a surface, a
 * `--line-2` top hairline, `--r-xl`, `--e3`). It is left exactly as it is: it is a sheet, not a card,
 * and the radius family already says so.
 */
@media (max-width: 720px) {
  #stage .step-card:not(.rv):not(.affdoc) { padding: var(--s3); }
  /* THE BAND REACHES THE EDGES OF THE PHONE'S CARD TOO, at the phone's own gutter. */
  #stage .step-card:not(.rv):not(.affdoc) > .step-card-head:first-child,
  #stage .step-card:not(.rv):not(.affdoc) > .inf-head:first-child,
  #stage .step-card:not(.rv):not(.affdoc) > .inf-section:first-child > .inf-head:first-child {
    margin: calc(-1 * var(--s3)) calc(-1 * var(--s3)) var(--s2);
    padding: var(--s3);
    background: var(--sunken);
    border-bottom: 1px solid var(--line);
    border-radius: calc(var(--r-lg) - 1px) calc(var(--r-lg) - 1px) 0 0;
  }
  #stage .inf-actions,
  #stage .stage-btnrow,
  #stage .intake-actions,
  #stage .inf-next { padding-top: var(--s2); }
  .nf-item { padding: var(--s3); }
  .st-team-row { border-radius: var(--r-md); }
  .st-panel { border-radius: var(--r-sm); }
  .tr-box,
  .tr-block { border-radius: var(--r-md); }
  .home-card { padding: var(--s3); }
}


/* ------------------------------------------------------------------ N. MOTION
 * This block adds no keyframe, no animation and no transform: the only movement it can produce is a
 * state arriving on pointer-down or focus, which is a colour change. The console's own reduced-motion
 * contract already collapses every transition duration to nothing; it is restated here, beside the two
 * controls this block gave a transition to, so the guard sits with the thing it guards. */
@media (prefers-reduced-motion: reduce) {
  .inf-tab,
  .ar-search-in { transition: none; }
}


/* ==================================================================================================
 * THE SERVER'S OWN PAGE — `/record/<token>`, DRESSED AS THE PRODUCT'S OWN DOCUMENT (2026-09-28).
 *
 * WHAT THIS IS. The delivery-recording link is what an OUTSIDE process server is emailed: one job, one
 * form, a Save, and no console around any of it. The page is drawn by the SAME `infAttemptForm` an
 * operator's Attempted Delivery screen is (`app.js:6323`), and until now it wore that screen's dressing
 * with the console's chrome merely hidden — so it read as a console screen with the furniture taken
 * away, rather than as a page built for the person filling it in.
 *
 * WHAT IT WEARS NOW. The READING's own language, which is this product's design system in this file:
 * `#att-view-card` (app.css:8348) and the `rv-` family. A hero band naming the job, sections divided by
 * ONE full-bleed hairline with a 13px/650 uppercase `--ink-2` head and its meta at 12px `--ink-4`
 * (`rv-sec`/`rv-sec-t`/`rv-sec-m`), fact ROWS for what is only READ (`rv-fact`), the act in a `--sunken`
 * footer band (`rv-decide`), and FLAT tokens throughout — NO GRADIENT anywhere, which is the standing
 * instruction for this product (`app.css:8728`).
 *
 * WHAT IT DELIBERATELY DOES NOT TOUCH. Not a word, not an id, not a data attribute, not a class, and
 * not one line of behaviour. The form still posts to `/record/<token>` for a new visit and to
 * `/record/<token>/correction` for a correction; the strips are still the elements the guards read
 * (`#link-recorded`, `#att-result-box`, `[data-link-open]`, `#att-save`, `#att-cancel`); and EVERY
 * selector is scoped to `body.link-mode`, the class `tokenBoot` sets (`app.js:19133`), so the
 * operator's own Attempted Delivery screen — the same functions, the same ids — is untouched by this
 * block and can be read and deleted without it.
 *
 * APPENDED AT THE TAIL, never woven in: this file is edited by other sessions and the last writer wins
 * with no merge, so a scoped block at the end cannot take a rule away from anyone.
 *
 * TOKENS: only ones this stylesheet declares (`:root` and its dark pair, app.css:10). NOT `--panel` /
 * `--fg`, which are declared nowhere here and would silently paint hard-coded dark values.
 * ================================================================================================== */

/* ------------------------------------------------- A. THE SHELL — NO CONSOLE, ONE MEASURED COLUMN
 *
 * THE BAR GOES. What is left on it after `linkHideChrome` is a theme button and an account chip: two
 * pieces of the office's furniture that a stranger cannot act on, drawn as a bare 62px strip above the
 * form. (Everything else on it — the Home mark, Notifications, the queue count — is already withdrawn.)
 * A page that opens with an empty console strip does not read as a page.
 *
 * THE MEASURE IS CAPPED. MEASURED at 1440x900 before this block: the form was a 1386px-wide card, i.e.
 * a line of prose and a set of controls stretched the full width of a desktop, with the label of every
 * field an inch from its neighbour. The reading caps its own measure; so does this page (880px,
 * centred), because a form is read as much as it is filled in.
 */
body.link-mode { background: var(--bg); }
body.link-mode .bar { display: none; }
body.link-mode .desk { background: var(--bg); }
body.link-mode .sheet {
  /* THE MEASURE IS THE READING'S OWN, AND THE GUTTER IS OUTSIDE IT: a content column of 880px, plus
     `--s4` on each side, so the measure is 880 at every width and the cards never touch an edge.
     MEASURED gap this closes: between 721px and 880px the sheet IS the viewport, so a horizontal
     padding of 0 put the card flush against both edges — fine at 1440 (where the cap applies and the
     page supplies the margin) and fine on a phone (the block below adds its gutter), and wrong on
     every width in between. `box-sizing: border-box` is global (`app.css:122`), so the cap includes
     the padding. */
  max-width: calc(880px + 2 * var(--s4)); width: 100%; margin: 0 auto;
  background: transparent; border-left: 0;
  padding: var(--s5) var(--s4) var(--s6);
}
/* The stage owns no horizontal inset at the desk: the page header card and the form card then share
   ONE left and right edge, which is what makes them read as one page rather than as two blocks. The
   phone gets its gutter back in H. */
body.link-mode #stage { padding: var(--s4) 0 0; }

/* ------------------------------------------------- B. THE PAGE HEADER — WHICH JOB THIS IS
 *
 * The one question a process server has before recording anything is "is this the right matter", and
 * the case head already answers it from the TOKEN's own facts: `#case-id` carries
 * `link_job_number`, `#case-name` the job's name (`tokenBoot`). It is dressed as the reading's hero —
 * a NEUTRAL `--sunken` band closed by a `--line` hairline, NOT the accent — because it is this page's
 * heading. Kazim's instruction is explicit that this page carries no blue: "remove the blue ish
 * gradient from the attempt deliveries from the process server". An earlier cut of the reading used
 * `--accent-soft` here and this block copied it; the accent is now spent NOWHERE on this page's
 * chrome, so it can mean something when it does appear.
 *
 * IT IS A CARD OF ITS OWN, not a strip: `--raised`, the card's hairline and `--r-lg`, `--e1`.
 *
 * AND IT DOES NOT STICK. `.case-head` is `position: sticky; top: 0` (app.css:452) because on the console
 * it is a compact strip above a step rail. MEASURED on a phone: the link page releases the document's
 * scroll chain (`body.link-mode`, app.css:8145), so the sticky card came to rest OVER the form — a card
 * floating on top of the thing it names, with the fields running underneath it. This page's header is a
 * card in the flow: it scrolls with the page it heads.
 */
body.link-mode .case-head {
  position: static;
  display: flex; flex-direction: column; align-items: flex-start; gap: var(--s1);
  padding: var(--s5) var(--s5) var(--s4);
  background: var(--sunken);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--e1);
  min-width: 0;
}
body.link-mode .case-head .case-title { min-width: 0; width: 100%; }
/* THE RECORD ID IS METADATA, NOT A TITLE — the reading's own rule (`rv-hero-id`, app.css:8391): a mono
   chip, an 11px word on a surface of its own, and it WRAPS rather than dragging the card sideways. */
body.link-mode .case-id {
  display: inline-block; margin: 0 0 var(--s2); max-width: 100%;
  font-family: var(--mono); font-size: 11px; font-weight: 500; letter-spacing: 0.01em;
  color: var(--ink-4); background: var(--raised); border: 1px solid var(--line);
  border-radius: var(--r-sm); padding: 3px 7px;
  overflow-wrap: anywhere;
}

/* ------------------------------------------------- C. THE CARD, ITS TITLE BAND AND ITS SECTIONS
 *
 * The card keeps the console's own frame — `--raised`, a `--line` hairline, `--r-lg`, `--e2` — because
 * it IS the card the rest of the product draws. What changes is the inside, which is now the reading's
 * grammar:
 *
 *   * a TITLE BAND on `--sunken`, closed by a hairline, naming the thing being filled in;
 *   * a SECTION per group: ONE full-bleed hairline, a 13px/650 uppercase `--ink-2` head with its meta
 *     at 12px `--ink-4` — not the five bordered `<details>` rectangles this was;
 *   * the act in a `--sunken` FOOTER band.
 *
 * THE CARD'S OWN PADDING GOES TO ZERO and each band states its own, because a hairline that stops
 * short of the card's edge reads as a box rather than as a division. The `:not(.rv):not(.affdoc)`
 * shape is copied from the frame rules at app.css:8767 so this block WINS over them by specificity,
 * not by luck of ordering.
 */
body.link-mode #stage .step-card:not(.rv):not(.affdoc) { padding: 0; }
body.link-mode #stage .step-card:not(.rv):not(.affdoc) > .inf-head:first-child {
  margin: 0; padding: var(--s4) var(--s5) var(--s3);
  background: var(--sunken);
  border-bottom: 1px solid var(--line);
  border-radius: calc(var(--r-lg) - 1px) calc(var(--r-lg) - 1px) 0 0;
}
/* THE FORM'S NAME IS A SECTION HEAD, NOT A SECOND TITLE. The page's title is the job's, above; this
   names the thing being filled in, at the reading's section-head weight (`rv-sec-t`, app.css:8465). */
body.link-mode #stage .inf-head h3 {
  font-size: 13px; font-weight: 650; letter-spacing: 0.02em;
  text-transform: uppercase; color: var(--ink-2);
}
body.link-mode #stage .inf-head .inf-count { font-size: 12px; color: var(--ink-4); }

/* THE INTRODUCTORY LINE — "what is being asked", in the reading's own voice (`rv-hero-why`): 13px/1.45
   `--ink-3`, capped at a readable measure. It is the one sentence on the page allowed to sound like a
   person explaining the job. */
body.link-mode #stage .inf-head + .pset-note {
  margin: 0; padding: var(--s4) var(--s5) var(--s2);
  font-size: 13px; line-height: 1.45; color: var(--ink-3); max-width: 72ch;
}
/* THE EDITING FORM SAYS SO LOUDER — on the warning trio — because the one thing a person must not miss
   is that this Save is a CORRECTION of a delivery already on the record, and that the earlier reading
   is kept. */
body.link-mode #stage .inf-head + .pset-note.inf-contested {
  max-width: none; padding-bottom: var(--s4); color: var(--ink-2);
  font-size: 13px;                       /* stated, or the <b> inside it drops below the 11px floor */
  background: var(--warn-soft); border-bottom: 1px solid var(--warn-line);
}
body.link-mode #stage .inf-head + .pset-note.inf-contested b { color: var(--ink); font-size: inherit; }

/* ONE SECTION = ONE HAIRLINE + A HEAD. */
body.link-mode #stage .inf-group {
  display: block; margin: 0; padding: var(--s4) var(--s5) var(--s3);
  border: 0; border-top: 1px solid var(--line); border-radius: 0;
  background: transparent; min-width: 0;
}
body.link-mode #stage .inf-group > summary {
  align-items: baseline; gap: var(--s2); margin-bottom: var(--s2); min-width: 0;
}
/* The fold's caret is KEPT and quietened: these sections really do open and close, and a control the
   person cannot see is not one they were given. It is the glyph it always was — only its colour and
   size are stated, so a later change to the marker is a change in one place. */
body.link-mode #stage .inf-group > summary::before {
  font-size: 11px; line-height: 1.4; color: var(--ink-4);
}
body.link-mode #stage .inf-group-title {
  font-size: 13px; font-weight: 650; letter-spacing: 0.02em;
  text-transform: uppercase; color: var(--ink-2);
}
/* THE SECTION'S META IS A NOTE IN THE PERSON'S OWN VOICE, not the machine's: right-aligned in the
   reading's `rv-sec-m` numbers (12px `--ink-4`), in the UI face, because "shown for checking — not
   editable here" is a sentence to a person and not a record read off the store. */
body.link-mode #stage .inf-group > summary .pset-note {
  margin-left: auto; font-family: var(--ui); font-size: 12px;
  color: var(--ink-4); text-align: right;
}

/* ------------------------------------------------- D. WHAT IS READ vs WHAT IS FILLED IN
 *
 * THE JOB'S FACTS ARE ROWS — the reading's own fact grammar (`rv-fact`, app.css:8418): a 12px/600
 * `--ink-4` label in a fixed measure, its value at 14px `--ink`, one hairline between rows. Nothing in
 * that section is a control; the job's identity is shown to be CHECKED. A row says that; a two-column
 * grid of empty boxes said "fill me in".
 */
body.link-mode #stage .inf-facts { display: block; margin: 0; padding: 0; }
body.link-mode #stage .inf-facts > .inf-field {
  display: grid; grid-template-columns: minmax(140px, 190px) 1fr;
  align-items: baseline; gap: var(--s3); padding: 9px 0; min-width: 0;
}
body.link-mode #stage .inf-facts > .inf-field + .inf-field { border-top: 1px solid var(--line); }
body.link-mode #stage .inf-facts .inf-label {
  margin: 0; font-size: 12px; font-weight: 600; letter-spacing: 0.01em; color: var(--ink-4);
}
body.link-mode #stage .inf-facts .inf-control {
  font-size: 14px; line-height: 1.5; color: var(--ink); overflow-wrap: anywhere;
}

/* THE FIELDS ARE THE FORM. `.inf-input` was a 3px-radius box with 4px of padding at the inherited size
   — a control sized for a dense operator screen. Here it is the product's own field: `--r-md`, 9px of
   padding, the `--ring` on focus (the same focus the console's own intake box wears, app.css:614), and
   a label at the reading's 12px/600 `--ink-4`.
 *
 * THE FIELD'S SIZE IS STATED PER WIDTH, AND ONLY AT THE DESK. Below 721px the stylesheet already lifts
 * every input to 16px so iOS does not zoom the page on focus (app.css:2589); that rule is left to win,
 * because a form that jumps under a finger is worse than a field one step larger than the desk's. */
body.link-mode #stage .inf-label {
  display: block; margin: 0 0 6px;
  font-size: 12px; font-weight: 600; letter-spacing: 0.01em; color: var(--ink-4);
}
body.link-mode #stage .inf-group > .inf-field { margin: var(--s4) 0 0; }
body.link-mode #stage .inf-group > .inf-grid { gap: var(--s4) var(--s5); }
body.link-mode #stage .inf-input {
  width: 100%; max-width: 100%; padding: 9px 12px;
  color: var(--ink); background: var(--raised);
  border: 1px solid var(--line-2); border-radius: var(--r-md);
  transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
body.link-mode #stage .inf-input:focus-visible {
  outline: none; border-color: var(--accent); box-shadow: var(--ring);
}
@media (min-width: 721px) {
  body.link-mode #stage .inf-input { font-size: 14px; }
}
/* THE FIELD'S OWN NOTE stays quiet and stays ONE step below the label it explains. */
body.link-mode #stage .inf-quiet { font-size: 12px; line-height: 1.5; color: var(--ink-4); }
/* A NOTE INSIDE A SECTION IS A SENTENCE TO THE PERSON, not the machine's voice — mono is reserved on
   this page for values that ARE records (the strip's dates and ids). */
body.link-mode #stage .inf-group .pset-note { font-family: var(--ui); }
/* a chosen picture is the same object as a field: the field's radius on the field's hairline */
body.link-mode #stage .att-pic-row { border-radius: var(--r-md); border-color: var(--line); }

/* ------------------------------------------------- E. THE ACT — A FOOTER BAND
 *
 * `.inf-actions` is the reading's `rv-decide` (app.css:8491): a `--sunken` band closed by a `--line-2`
 * hairline, with the primary act FIRST and the explanation UNDER it — not squeezed into the gap beside
 * the buttons, where a sentence that has to be read becomes the last thing anyone reads.
 *
 * THE TARGET IS 36px AND THE LABEL IS 14px. This button is the whole point of the page, and it was the
 * console's own 32px/13px `btn` — the size of a control on an operator's dense screen. The footer
 * band's own bottom corners belong to the card, because this band is its foot while no result is
 * drawn; `:has()` is used here exactly as the phone's shell rules already use it (app.css:1906). */
body.link-mode #stage .inf-actions {
  margin: 0; padding: var(--s4) var(--s5);
  background: var(--sunken); border-top: 1px solid var(--line-2);
  gap: var(--s2) var(--s3); align-items: center;
}
body.link-mode #stage .inf-actions .btn { height: 36px; padding: 0 16px; font-size: 14px; }
body.link-mode #stage .inf-actions #att-save { font-weight: 620; min-width: 132px; }
body.link-mode #stage .inf-actions .inf-quiet { flex: 1 0 100%; margin: 0; }
body.link-mode #stage .inf-card > .inf-actions:has(+ #att-result-box[hidden]) {
  border-radius: 0 0 calc(var(--r-lg) - 1px) calc(var(--r-lg) - 1px);
}

/* ------------------------------------------------- F. WHAT HAPPENED — THE RESULT LINE
 *
 * The save's own answer, banded in the product's own "settled work" green (`--ok`, app.css:7) and on
 * `--danger-soft` when a write was refused. It is the ONE thing a person must be able to find after
 * pressing Save, and it was a bare paragraph under the buttons, the same weight as the small print
 * above it. */
body.link-mode #stage .inf-result {
  margin: 0; padding: var(--s4) var(--s5); max-width: none;
  background: var(--ok-soft); border-top: 1px solid var(--ok-line);
  font-size: 14px; line-height: 1.5; color: var(--ink);
  border-radius: 0 0 calc(var(--r-lg) - 1px) calc(var(--r-lg) - 1px);
}
body.link-mode #stage .inf-result b { color: var(--ok); }
body.link-mode #stage .inf-result .mono { font-size: 12px; color: var(--ink-4); }
body.link-mode #stage .inf-result.bad {
  background: var(--danger-soft); border-top-color: var(--danger-line); color: var(--ink);
}
body.link-mode #stage .inf-result.bad b { color: var(--danger); }

/* ------------------------------------------------- G. WHAT THIS LINK HAS ALREADY RECORDED
 *
 * `#link-recorded` answers "did my last one go through?" from the token's own list, and offers each of
 * its OWN visits back to be corrected. With rows it is a CARD in the form's language; with none it is
 * one quiet line, because a card whose whole content is "nothing yet" is furniture. `:has()` is what
 * tells the two apart — there is no class to hang it on, and the markup is the product's.
 */
body.link-mode #stage #link-recorded {
  margin: 0; max-width: none; font-size: 13px; line-height: 1.5; color: var(--ink-2);
}
body.link-mode #stage #link-recorded:has(ul) {
  padding: var(--s4) var(--s5);
  background: var(--raised); border: 1px solid var(--line);
  border-radius: var(--r-lg); box-shadow: var(--e1);
}
body.link-mode #stage #link-recorded > .pset-note { margin: 0; max-width: 78ch; }
body.link-mode #stage #link-recorded ul {
  list-style: none; margin: var(--s3) 0; padding: 0;
  display: flex; flex-direction: column; font-size: 13px;
}
/* ONE ROW PER VISIT, one hairline between them — the reading's row rule, and the visits keep the
   console's mono because a date and a record id ARE records. */
body.link-mode #stage #link-recorded ul > li {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s1) var(--s3);
  padding: 9px 0; min-width: 0; overflow-wrap: anywhere; color: var(--ink-2);
}
body.link-mode #stage #link-recorded ul > li + li { border-top: 1px solid var(--line); }
body.link-mode #stage #link-recorded ul > li > .pset-note { margin: 0; }
/* THE WAY BACK INTO ONE OF MY OWN VISITS sits at the end of its own row, so it is unmistakably that
   row's control and not a sixth button in a column of them. */
body.link-mode #stage #link-recorded ul > li > [data-link-open] { margin-left: auto; }

/* ------------------------------------------------- H. ON A PHONE
 * The same page at the phone's own gutter. Nothing here changes the STRUCTURE the phone already has
 * (the single column, the 44px targets, the released scroll chain): this is the measure, the padding
 * and the two places a row becomes a column.
 */
@media (max-width: 720px) {
  /* ONE GUTTER FOR THE WHOLE PAGE. MEASURED on a 390px phone before this rule: the stage's own
     horizontal padding inset the form card by `--s4` while the header card — which sits OUTSIDE the
     stage, directly in the sheet — ran flush to both screen edges, so the page's two cards had two
     different left edges. The gutter moves up to the sheet, where both are inside it, and the stage
     keeps only its vertical measure. */
  body.link-mode .sheet { padding: var(--s3) var(--s4) var(--s5); }
  body.link-mode #stage { padding: var(--s3) 0 0; }
  body.link-mode .case-head { padding: var(--s4); }
  body.link-mode #stage .inf-head:first-child { padding: var(--s4) var(--s4) var(--s3); }
  body.link-mode #stage .inf-head + .pset-note { padding: var(--s3) var(--s4) var(--s2); }
  body.link-mode #stage .inf-group { padding: var(--s4) var(--s4) var(--s3); }
  body.link-mode #stage .inf-group > summary .pset-note { text-align: left; margin-left: 0; }
  /* ONE COLUMN: the label ABOVE its value, which is the shape every other label/value pair takes on a
     phone (rv-fact's own phone rule, app.css:8560). */
  body.link-mode #stage .inf-facts > .inf-field { grid-template-columns: 1fr; gap: 2px; }
  body.link-mode #stage .inf-facts .inf-label { margin-bottom: 1px; }
  body.link-mode #stage .inf-actions { flex-direction: column; align-items: stretch; }
  body.link-mode #stage .inf-actions .btn,
  body.link-mode #stage .inf-actions #att-save { width: 100%; min-width: 0; }
  body.link-mode #stage .inf-actions .inf-quiet { text-align: left; }
  body.link-mode #stage .inf-result,
  body.link-mode #stage #link-recorded:has(ul) { padding: var(--s4) var(--s4); }
  body.link-mode #stage #link-recorded ul > li > [data-link-open] { margin-left: 0; }
}

/* ------------------------------------------------- I. MOTION
 * This block adds no keyframe and no transform. The only movement it can produce is the field's focus
 * ring arriving, and the press states the `.btn` family already owns — so the guard is the two rules
 * that gave a transition, restated beside them. (`* { transition-duration: 1ms !important }` at
 * app.css:1565 is the console's own reduced-motion contract and already covers the rest.) */
@media (prefers-reduced-motion: reduce) {
  body.link-mode #stage .inf-input { transition: none; }
}

/* ==================================================================================================
 * THE APPROVAL CHIP DOES NOT FIT A PHONE — A REAL OVERFLOW, FOUND BY RESTORING A DEAD GUARD
 * (2026-09-28)
 *
 * `verify_informal_render.py` had been dying before it asserted anything (a bare `POST /api/cases`
 * answered 403 under the operator's `cases.create` block, so it addressed `/api/cases/` and crashed
 * on `KeyError: 'case_path'`). Pointed at the suite's own create seam it now RUNS, and on the phone
 * pass it reports two failures:
 *
 *     phone: no element overflows the viewport        377.5px on case-approval-chip
 *     phone: nothing inside the card is clipped       1 clipped element(s)
 *
 * WHAT WAS MEASURED, not assumed. `.case-approval-chip` is `font-weight: 600; white-space: nowrap`
 * (app.css:6502) — ORIGINAL CSS — and carries a whole sentence as its label: the approval reading's
 * `word` ("Whether the approval predates the move is not established", 51 characters). NOWRAP forces
 * it onto ONE 378px line inside a 390px viewport, so it runs 18px past the right edge and the block
 * clips it. That is the clipped element: one element, one reason.
 *
 * IT IS NOT THE DESIGN PASS. Proven by measuring the same page with the appended rules removed
 * THROUGH THE CSSOM (181 of them, nothing written to disk): the chip measures w=378, right=408 in
 * BOTH states, identical. `awk 'NR>8700' app.css | grep -c case-approval` is 0 — the design blocks
 * never mention it, and no guard pins the chip's `white-space` (`verify_approval_after_move_ui` reads
 * its COLOUR only).
 *
 * THE FIX IS ONE PROPERTY, at the width where it bites. The chip still gets to BE a chip above 720px,
 * where it fits, so the desk render is untouched and the value a check could pin survives there; below
 * that it wraps like the rest of the block instead of running off the page. A label that cannot wrap
 * on a 390px screen is not a design, it is a defect that only shows when someone measures the pixels.
 * ================================================================================================== */
@media (max-width: 720px) {
  .case-approval .case-approval-chip { white-space: normal; }
}

/* ================================================================================================
   THE JOB DETAILS CARDS (Kazim, 2026-09-29): "the job details should be card to card ... a
   card/section that has the client company one right, prefilled and operator fills in, presses next,
   then the next card has the Parties section, then the next card should have Court Details ... then
   recipient(s), Service address, Process Server, and finally the Job ... during all this there should
   also be next and back buttons ... no field is required/compulsory and there should be a save/cancel
   button."

   APPEND-ONLY, as every block in this file is. The wizard is a NEW surface: the cards and their
   controls had no rules of their own before, so nothing above is being overridden and nothing here
   needs a specificity fight with an older rule.
   ================================================================================================ */
.inf-card-step {
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--raised);
  padding: var(--s3) var(--s4) var(--s4);
  margin: 0 0 var(--s3);
}
/* THE CARD'S OWN HEADING. `Step n of m` comes FIRST and stays quiet, so it reads as position rather
   than as a section title competing with the section's own heading underneath. */
.inf-card-step-head {
  display: flex; align-items: baseline; gap: var(--s3);
  padding-bottom: var(--s2); margin-bottom: var(--s2);
  border-bottom: 1px solid var(--line);
}
.inf-card-step-n { color: var(--ink-4); font-size: 11px; letter-spacing: .04em; }
.inf-card-step-title { font-size: 15px; }
/* INSIDE A CARD THE SECTION IS NOT A COLLAPSED BAR ANY MORE. The card IS the container, so its own
   `<details>` chrome (border, radius, margin) is removed and only the summary line remains — one box
   per card rather than a box inside a box. The `n/m` count and the open/closed memory are untouched;
   `openCard` opens them when the card is shown. */
.inf-card-step .inf-group {
  border: 0; border-radius: 0; margin: 0; padding: 0; background: transparent;
}
.inf-card-step .inf-group-head { padding: var(--s1) 0; }
.inf-card-step .inf-group + .inf-group { border-top: 1px solid var(--line); margin-top: var(--s2); }
/* THE NEXT/BACK BAR, under the cards and above the Save/Cancel bar, so the two rows do not read as
   one control group: Next moves through the form, Save writes it. */
.inf-wizard-nav {
  display: flex; align-items: center; gap: var(--s2);
  margin: var(--s3) 0 0; padding-top: var(--s3); border-top: 1px solid var(--line);
}
/* A DISABLED Next AT THE LAST CARD MUST STILL BE LEGIBLE — it is how the operator knows they are at
   the end, and a control greyed into the background says nothing. */
.inf-wizard-nav .btn:disabled { opacity: .45; cursor: not-allowed; }
/* ADVANCED VIEW: every card at once. The wizard bar sits above the cards; when the mode is on the
   bar says so and the cards lose their per-step spacing so the page reads as the old single form. */
.inf-wizard-bar { display: flex; align-items: center; gap: var(--s2); margin: 0 0 var(--s2); }
.inf-advanced-on .inf-card-step { margin-bottom: var(--s2); }
/* THE BUTTONS THAT REPLACED THE TABS, on This Job's header row. */
#inf-job-doors { display: flex; align-items: center; gap: var(--s2); margin: var(--s2) 0; }
/* ON A PHONE THE CARD'S FIELDS GO ONE PER ROW. The grid is the server's own (`inf-grid`); this only
   stops a two-column grid squeezing two inputs into 190px each. */
@media (max-width: 720px) {
  .inf-card-step { padding: var(--s3); }
  #inf-job-doors { flex-wrap: wrap; }
  .inf-wizard-nav { flex-wrap: wrap; }
}

/*
 * THE JOB/CASE LIST IS HIDDEN WHILE A JOB OR CASE IS OPEN (Kazim, 2026-09-29): "remove the left bar
 * which shows all the Jobs and Cases when in a job or a case, it should only show on the Home tab of
 * the tab, like the home tab of informal shows the left part of the informal jobs and same for hague
 * or translations, not when on the Case or Job."
 *
 * IT IS HIDDEN, NOT REMOVED, AND THAT IS THE WHOLE FIX. The rail is how the operator gets FROM the
 * list TO a job; deleting it would take away the Home state's index, which he explicitly wants kept.
 * It is also the element browser checks measure the holder chips on, so removing it would break
 * assertions rather than change a view.
 *
 * THE FLAG IS ONE THE APP ALREADY MAINTAINS. `body.on-home` is set by `showHome()` and cleared at the
 * single place a case is opened (`selectCase`, which adds `body.case-open`) — so this rule reads the
 * console's own answer to "is a job on screen" instead of introducing a second flag that could
 * disagree with it. `:has()` is NOT used: `on-home` is authoritative and this box has already been
 * bitten by `:has(body.case-open)` rules.
 *
 * THE TRACK COLLAPSES TOO. Hiding only the rail would leave a 380px empty column with a hairline
 * down the middle of the screen — the list would look "removed" while the layout still reserved its
 * place. `.desk-body` drops to the single content column at the same time, so the job gets the width
 * the list was holding.
 */
body.case-open .desk-body { grid-template-columns: minmax(0, 1fr); }
body.case-open .desk-body > .queue,
body.case-open #rail-body,
body.case-open #rail-head,
body.case-open #rail-count { display: none !important; }
/* AND THE WHOLE PANEL, so an empty track cannot paint its own hairline beside the sheet. */
body.case-open .desk-body > section.queue { display: none !important; }
@media (max-width: 720px) {
  /* On a phone the rail is already a drill-down below the sheet; the same rule keeps it out of the
     way there without a second definition of when it shows. */
  body.case-open .desk-body > .queue { display: none !important; }
}
