/* Bastion Cargo — graphite + mint (brandbook v1.0). Server-rendered, minimal CSS.
   All colors live in the token blocks below (light default, dark via media query,
   html[data-theme] override wins both ways — SPEC_UI §2). Do not hardcode hex in
   component rules; legacy var names (--navy/--gold/…) are kept as aliases. */

@font-face {
  font-family: "Manrope";
  font-style: normal;
  font-weight: 200 800;
  font-display: swap;
  src: url("/static/fonts/manrope-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Manrope";
  font-style: normal;
  font-weight: 200 800;
  font-display: swap;
  src: url("/static/fonts/manrope-cyrillic.woff2") format("woff2");
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

/* ---------- Tokens: light (default) ---------- */
:root {
  --bg: #f4f6f7;
  --surface: #ffffff;
  --surface-2: #edf1f1;
  --ink: #10171b;
  --body-t: #33414b;
  --muted: #54646e;
  --faint: #63717b;
  --border: #e2e8e9;
  --border-strong: #cbd5d7;
  --accent: #0aa173;
  --accent-hover: #08916a;
  --accent-ink: #087a58;
  --on-accent: #10171b;
  --accent-soft: #def2ea;
  --ok-t: #1f7a48;    --ok-bg: #e2f3e8;    --ok-br: #bfe5cc;  --ok-solid: #1f7a48;
  --warn-t: #8a6210;  --warn-bg: #faf0d7;  --warn-br: #ecd48a;
  --info-t: #2a5fc4;  --info-bg: #e8effc;
  --danger-t: #b3424e; --danger-bg: #fbe9eb; --danger-br: #f2c7cb;
  --violet-t: #6d51c8; --violet-bg: #eee9fa;
  --rose-t: #ad3b6e;  --rose-bg: #fbe7f0;
  --neutral-t: #54646e; --neutral-bg: #eceff1;
  --amber-strong: #d97706;
  --dg-bg: #b5484d; --dg-t: #ffffff;
  --local-bg: #10151a; --local-t: #2ee6a8; --local-br: transparent;
  --promoted-bg: #10151a; --promoted-t: #6aa4ff; --promoted-br: transparent;
  --row-hover: #f0f5f3;
  --shadow: 0 1px 2px rgba(16, 23, 27, 0.05), 0 4px 14px rgba(16, 23, 27, 0.06);
  --shadow-pop: 0 10px 28px rgba(16, 23, 27, 0.16);
  /* Top bar is graphite in BOTH themes — the brand anchor */
  --nav-bg: #10151a;
  --nav-ink: #f2f4f5;
  --nav-muted: #8fa0aa;
  --nav-border: #1d262d;
  --nav-accent: #2ee6a8;
  --nav-accent-soft: rgba(46, 230, 168, 0.13);
  --nav-hover: rgba(242, 244, 245, 0.07);
  --control-border: #89959b;
  --on-ok-solid: #ffffff;
  --shadow-accent: 0 4px 14px rgba(10, 161, 115, 0.22);
  --scrim: rgba(6, 10, 12, 0.6);
  --code-bg: #10151a; --code-ink: #c9d6de; --code-border: transparent;
  color-scheme: light;
  --login-bg-1: #1a252d; --login-bg-2: #0b0f12; --login-shadow: rgba(0, 0, 0, 0.35);
  /* Legacy aliases — old templates/inline styles still reference these */
  --navy: var(--ink);
  --navy-2: var(--surface-2);
  --gold: var(--accent);
  --gold-2: var(--accent-ink);
  --card: var(--surface);
  --text: var(--ink);
  --danger: var(--danger-t);
}

/* ---------- Tokens: dark (system preference) ---------- */
@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0a0d0e;
    --surface: #10161a;
    --surface-2: #171e23;
    --ink: #f4f6f7;
    --body-t: #c2cbd1;
    --muted: #9aa6ae;
    --faint: #75828a;
    --border: #212a30;
    --border-strong: #303c43;
    --accent: #2ee6a8;
    --accent-hover: #55ebb7;
    --accent-ink: #7ff0c9;
    --on-accent: #062019;
    --accent-soft: rgba(46, 230, 168, 0.13);
    --ok-t: #6fd695;    --ok-bg: rgba(111, 214, 149, 0.13);  --ok-br: rgba(111, 214, 149, 0.35); --ok-solid: #2e9e63;
    --warn-t: #efc168;  --warn-bg: rgba(239, 193, 104, 0.12); --warn-br: rgba(239, 193, 104, 0.35);
    --info-t: #6aa4ff;  --info-bg: rgba(106, 164, 255, 0.13);
    --danger-t: #ff8b92; --danger-bg: rgba(255, 139, 146, 0.12); --danger-br: rgba(255, 139, 146, 0.35);
    --violet-t: #b3a4f5; --violet-bg: rgba(179, 164, 245, 0.13);
    --rose-t: #f08bb5;  --rose-bg: rgba(240, 139, 181, 0.13);
    --neutral-t: #9aa6ae; --neutral-bg: rgba(154, 166, 174, 0.12);
    --amber-strong: #f0a44b;
    --local-bg: rgba(46, 230, 168, 0.1); --local-t: #2ee6a8; --local-br: rgba(46, 230, 168, 0.35);
    --promoted-bg: rgba(106, 164, 255, 0.13); --promoted-t: #6aa4ff; --promoted-br: rgba(106, 164, 255, 0.35);
    --row-hover: #141c21;
    --shadow: inset 0 1px 0 rgba(244, 246, 247, 0.05), 0 1px 2px rgba(0, 0, 0, 0.45), 0 6px 18px rgba(0, 0, 0, 0.35);
    --shadow-pop: 0 12px 30px rgba(0, 0, 0, 0.55);
  --control-border: #586770;
    --on-ok-solid: #062019;
    --shadow-accent: 0 4px 14px rgba(46, 230, 168, 0.18);
    --scrim: rgba(4, 6, 8, 0.7);
    --code-bg: #0a0d0e; --code-ink: #c9d6de; --code-border: #303c43;
    color-scheme: dark;
    --nav-border: #1f282f;
  }
}

/* ---------- Tokens: explicit per-account choice beats the system ---------- */
html[data-theme="dark"] {
  --bg: #0a0d0e;
  --surface: #10161a;
  --surface-2: #171e23;
  --ink: #f4f6f7;
  --body-t: #c2cbd1;
  --muted: #9aa6ae;
  --faint: #75828a;
  --border: #212a30;
  --border-strong: #303c43;
  --accent: #2ee6a8;
  --accent-hover: #55ebb7;
  --accent-ink: #7ff0c9;
  --on-accent: #062019;
  --accent-soft: rgba(46, 230, 168, 0.13);
  --ok-t: #6fd695;    --ok-bg: rgba(111, 214, 149, 0.13);  --ok-br: rgba(111, 214, 149, 0.35); --ok-solid: #2e9e63;
  --warn-t: #efc168;  --warn-bg: rgba(239, 193, 104, 0.12); --warn-br: rgba(239, 193, 104, 0.35);
  --info-t: #6aa4ff;  --info-bg: rgba(106, 164, 255, 0.13);
  --danger-t: #ff8b92; --danger-bg: rgba(255, 139, 146, 0.12); --danger-br: rgba(255, 139, 146, 0.35);
  --violet-t: #b3a4f5; --violet-bg: rgba(179, 164, 245, 0.13);
  --rose-t: #f08bb5;  --rose-bg: rgba(240, 139, 181, 0.13);
  --neutral-t: #9aa6ae; --neutral-bg: rgba(154, 166, 174, 0.12);
  --amber-strong: #f0a44b;
  --local-bg: rgba(46, 230, 168, 0.1); --local-t: #2ee6a8; --local-br: rgba(46, 230, 168, 0.35);
  --promoted-bg: rgba(106, 164, 255, 0.13); --promoted-t: #6aa4ff; --promoted-br: rgba(106, 164, 255, 0.35);
  --row-hover: #141c21;
  --shadow: inset 0 1px 0 rgba(244, 246, 247, 0.05), 0 1px 2px rgba(0, 0, 0, 0.45), 0 6px 18px rgba(0, 0, 0, 0.35);
  --shadow-pop: 0 12px 30px rgba(0, 0, 0, 0.55);
  --control-border: #586770;
  --on-ok-solid: #062019;
  --shadow-accent: 0 4px 14px rgba(46, 230, 168, 0.18);
  --scrim: rgba(4, 6, 8, 0.7);
  --code-bg: #0a0d0e; --code-ink: #c9d6de; --code-border: #303c43;
  color-scheme: dark;
  --nav-border: #1f282f;
}
html[data-theme="light"] {
  --bg: #f4f6f7;
  --surface: #ffffff;
  --surface-2: #edf1f1;
  --ink: #10171b;
  --body-t: #33414b;
  --muted: #54646e;
  --faint: #63717b;
  --border: #e2e8e9;
  --border-strong: #cbd5d7;
  --accent: #0aa173;
  --accent-hover: #08916a;
  --accent-ink: #087a58;
  --on-accent: #10171b;
  --accent-soft: #def2ea;
  --ok-t: #1f7a48;    --ok-bg: #e2f3e8;    --ok-br: #bfe5cc;  --ok-solid: #1f7a48;
  --warn-t: #8a6210;  --warn-bg: #faf0d7;  --warn-br: #ecd48a;
  --info-t: #2a5fc4;  --info-bg: #e8effc;
  --danger-t: #b3424e; --danger-bg: #fbe9eb; --danger-br: #f2c7cb;
  --violet-t: #6d51c8; --violet-bg: #eee9fa;
  --rose-t: #ad3b6e;  --rose-bg: #fbe7f0;
  --neutral-t: #54646e; --neutral-bg: #eceff1;
  --amber-strong: #d97706;
  --local-bg: #10151a; --local-t: #2ee6a8; --local-br: transparent;
  --promoted-bg: #10151a; --promoted-t: #6aa4ff; --promoted-br: transparent;
  --row-hover: #f0f5f3;
  --shadow: 0 1px 2px rgba(16, 23, 27, 0.05), 0 4px 14px rgba(16, 23, 27, 0.06);
  --shadow-pop: 0 10px 28px rgba(16, 23, 27, 0.16);
  --control-border: #89959b;
  --on-ok-solid: #ffffff;
  --shadow-accent: 0 4px 14px rgba(10, 161, 115, 0.22);
  --scrim: rgba(6, 10, 12, 0.6);
  --code-bg: #10151a; --code-ink: #c9d6de; --code-border: transparent;
  color-scheme: light;
  --nav-border: #1d262d;
}

/* One shared page width for EVERY screen + a stable scrollbar gutter —
   switching tabs must not shift the top bar or the headings (Aslan 18.07). */
:root { --page-w: min(1760px, 96vw); }
html { scrollbar-gutter: stable; }

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  font-family: "Manrope", system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
  background: var(--bg);
  color: var(--text);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Unclassed content links get the brand accent — readable on BOTH themes.
   Scoped with :not([class]) so classed anchors (.btn/.brand/.pager-link/...)
   keep full control of their own colors, including on hover. */
a:not([class]) { color: var(--accent-ink); }
a:not([class]):hover { color: var(--accent); }
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition: none !important; animation: none !important; }
}

/* Text for the screen reader only: off-screen for the eye, still in the
   document for assistive technology (`display:none` and `visibility:hidden`
   would hide it from BOTH). Use it only for something the sighted user already
   learns from the layout — a live count that arrives with a swap, a heading
   the design expresses with position alone. Never for a control: an invisible
   focusable element traps the keyboard on nothing. */
.sr-only {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; white-space: nowrap; border: 0;
  clip-path: inset(50%);
}

/* --- Navbar --- */
.navbar {
  position: sticky;
  top: 0;
  z-index: 60;
  background: var(--nav-bg);
  color: var(--nav-ink);
  border-bottom: 1px solid var(--nav-border);
}
.nav-inner {
  max-width: var(--page-w);
  margin: 0 auto;
  display: flex;
  align-items: center;
  gap: 24px;
  padding: 0 20px;
  height: 56px;
}
.brand {
  display: flex;
  align-items: center;
  gap: 9px;
  color: var(--nav-ink);
  text-decoration: none;
}
.brand svg { display: block; }
.brand-name { font-weight: 800; font-size: 16.5px; letter-spacing: -0.01em; }
.brand-sub {
  color: var(--nav-muted);
  font-weight: 600;
  font-size: 10.5px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  margin-left: 2px;
  padding-top: 3px;
}
.nav-links {
  display: flex;
  gap: 6px;
  margin-left: 12px;
}
.nav-link {
  color: var(--nav-muted);
  text-decoration: none;
  padding: 5px 13px;
  border-radius: 999px;
  font-size: 13.5px;
  font-weight: 600;
  transition: background 0.15s ease, color 0.15s ease;
}
.nav-link:hover:not(.disabled) { background: var(--nav-hover); color: var(--nav-ink); }
.nav-link.active { color: var(--nav-accent); background: var(--nav-accent-soft); }
.nav-link.disabled { color: var(--nav-muted); opacity: 0.55; cursor: default; }
/* Unread counter on a nav item. The navbar is one deliberate dark stage in
   both themes, so this needs no light/dark pair — but it still owes contrast:
   dark ink on mint (never the reverse, and never white on mint) measures
   11.36:1 (--nav-bg #10151a on --nav-accent #2ee6a8; the comment said 12.8:1
   until the review recomputed it — AAA either way). Drawn only when the number
   is non-zero, so it can never become a badge that always says the same
   thing (§5a). */
.nav-badge {
  display: inline-block;
  min-width: 17px;
  margin-left: 5px;
  padding: 0 5px;
  border-radius: 999px;
  background: var(--nav-accent);
  color: var(--nav-bg);
  font-size: 11px;
  font-weight: 700;
  line-height: 17px;
  text-align: center;
  font-variant-numeric: tabular-nums;
}
/* A seventh nav item pushed the bar past 1024 — measured: scrollWidth 1054 vs
   1009 without it, and the squeeze wrapped "Feedback" onto a second line
   inside its own pill, stretching every sibling to 51px (review 04.08, #7).
   Canon §5: 1024–1280 must fold gracefully. `nowrap` alone made it WORSE
   (1079) because nothing gave way, so the space comes from the brand's
   subtitle — decoration, while the brand NAME stays. */
@media (max-width: 1100px) {
  .brand-sub { display: none; }
  .nav-link { white-space: nowrap; }
}
.nav-user {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 12px;
}
.user-email { color: var(--nav-muted); font-size: 12.5px; text-decoration: none; }
a.user-email:hover { color: var(--nav-ink); text-decoration: underline; }
.logout-form { margin: 0; }
.theme-btn {
  width: 30px;
  height: 30px;
  border-radius: 999px;
  cursor: pointer;
  border: 1px solid var(--nav-border);
  background: transparent;
  color: var(--nav-muted);
  font-size: 14px;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: color 0.15s ease, border-color 0.15s ease;
}
.theme-btn:hover { color: var(--nav-accent); border-color: var(--nav-accent); }
/* ✎ (U+270E) is drawn small inside its em box, so at the shared 14px it reads
   noticeably smaller than the ☾ next to it. Optical sizing, not layout: the
   30px circle and its border are untouched. */
#feedback-btn { font-size: 18px; }

/* --- Toast (the ONE transient confirmation; everything else is an .alert
   after a redirect). Bottom-right, above the edge-nav rail, out of the way of
   the sticky action bars at the top. --- */
.toast-host {
  position: fixed; right: 18px; bottom: 18px; z-index: 60;
  display: flex; flex-direction: column; gap: 8px; align-items: flex-end;
  pointer-events: none;
}
.toast {
  max-width: min(380px, 80vw);
  padding: 10px 14px; border-radius: 10px;
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--ok-br); border-left: 3px solid var(--ok-solid);
  box-shadow: var(--shadow-pop); font-size: 13.5px;
  animation: toast-in 0.18s ease-out;
}
.toast.is-leaving { opacity: 0; transition: opacity 0.35s ease; }
@keyframes toast-in { from { opacity: 0; transform: translateY(6px); } }
@media (prefers-reduced-motion: reduce) {
  .toast { animation: none; }
  .toast.is-leaving { transition: none; }
}

/* --- Feedback inbox (Settings -> Feedback, superadmin only) --- */
.fb-item { margin-bottom: 12px; }
.fb-head { display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; }
/* User-written text: shown exactly as typed, wrapped, never interpreted. */
.fb-message { white-space: pre-wrap; overflow-wrap: anywhere; margin: 10px 0 6px;
  font-size: 14.5px; line-height: 1.55; max-width: 860px; }
.fb-url { margin: 0 0 8px; font-size: 12.5px; overflow-wrap: anywhere; }
.fb-meta { margin-bottom: 8px; }
.fb-handled { font-size: 12.5px; margin: 0 0 8px; }
.fb-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
/* Reserved width, so "saving…" appearing under the fingers never nudges the
   Done button sideways mid-press. */
.fb-note-state { flex: 0 0 62px; font-size: 12px; color: var(--muted); }
.fb-actions input { flex: 1 1 240px; min-width: 0; padding: 6px 10px;
  border: 1px solid var(--control-border); border-radius: 8px;
  background: var(--surface); color: var(--ink); font-family: inherit; font-size: 13px; }
/* Attached pictures (§11 v2). The thumbnail IS the stored picture, capped by
   the box: a report holds at most three and one person opens the page, so a
   second copy on disk would buy nothing. Height is fixed and width follows, so
   a row of screenshots of different shapes still reads as a row. */
.fb-shots { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 8px; }
.fb-shot { display: block; border: 1px solid var(--control-border);
  border-radius: 8px; overflow: hidden; background: var(--bg); line-height: 0; }
.fb-shot:hover { border-color: var(--accent); }
.fb-shot img { height: 88px; width: auto; max-width: 220px; object-fit: cover;
  display: block; }
/* A picture that was refused still says so — the report must never go quiet
   about something its author meant to show. */
.fb-shot-refused { align-self: center; font-size: 12.5px; color: var(--muted);
  border: 1px dashed var(--control-border); border-radius: 8px;
  padding: 6px 10px; overflow-wrap: anywhere; }
/* In the widget: what was just attached or just refused. */
.fb-shot-note { margin: 6px 0 0; font-size: 12.5px; min-height: 1em; }
.st-fb-new { background: var(--warn-bg); color: var(--warn-t); border: 1px solid var(--warn-br); }
.st-fb-seen { background: var(--info-bg); color: var(--info-t); }
.st-fb-done { background: var(--ok-bg); color: var(--ok-t); }
/* Kept out of the Quick RFQ picker (feedback #29). Neutral on purpose: it is
   housekeeping, not a verdict on the company — the status badge beside it is
   the one that carries a judgement. */
.st-hidden { background: var(--neutral-bg); color: var(--neutral-t); }
/* The closed list. A report that is dealt with is history: it keeps every word
   it was written with and simply stops competing with the ones still asking
   for something — no shadow, no lift, one step back in the stack. */
.fb-archive { margin-top: 20px; }
/* `--card` IS `--surface`, so recede to the PAGE colour — anything else here
   would be a no-op dressed as a decision. */
.fb-archive .fb-item { box-shadow: none; background: var(--bg); }
.fb-archive .fb-item:last-child { margin-bottom: 0; }
.fb-archive-more { margin: 10px 0 0; font-size: 12.5px; }
/* Done swaps the card out where it stands. The fade is what says it went
   somewhere rather than vanished; the duration matches `swap:180ms` on the
   form, so htmx removes the card exactly as it finishes disappearing. */
.fb-item.htmx-swapping { opacity: 0; transition: opacity 0.18s ease; }
@media (prefers-reduced-motion: reduce) {
  .fb-item.htmx-swapping { transition: none; }
}

/* --- Content --- */
.content { max-width: var(--page-w); margin: 0 auto; padding: 24px 24px 40px; }
.page-head h1 { margin: 0; font-size: 20px; font-weight: 800; letter-spacing: -0.01em; color: var(--ink); }

/* --- Buttons --- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  cursor: pointer;
  text-decoration: none;
  border: 1px solid transparent;
  border-radius: 999px;
  font-family: inherit;
  font-size: 14px;
  line-height: 1.3;
  padding: 9px 18px;
  font-weight: 700;
  color: var(--ink);
  background: var(--surface);
  border-color: var(--control-border);
  transition: transform 0.12s ease, box-shadow 0.12s ease, background 0.12s ease,
    border-color 0.12s ease, color 0.12s ease;
}
.btn:hover { transform: translateY(-1px); color: var(--ink); }
/* Disabled buttons must LOOK disabled (review finding 2026-07-18) — but keep
   pointer events so their explanatory title= tooltips still show. */
.btn:disabled { opacity: 0.55; cursor: not-allowed; }
.btn:disabled:hover { background: var(--surface); }
.btn-primary:disabled, .btn-primary:disabled:hover { background: var(--accent); box-shadow: none; }
.btn:disabled:hover { transform: none; box-shadow: none; }
.btn-primary {
  background: var(--accent);
  border-color: transparent;
  color: var(--on-accent);
  font-weight: 800;
  box-shadow: var(--shadow-accent);
}
.btn-primary:hover { color: var(--on-accent); }
.login-form .btn-primary { width: 100%; }
.btn-primary:hover { background: var(--accent-hover); }
.btn-ghost {
  background: transparent;
  color: var(--muted);
  border: 1px solid var(--control-border);
  padding: 7px 13px;
}
.btn-ghost:hover { color: var(--ink); border-color: var(--muted); }
.navbar .btn-ghost { color: var(--nav-muted); border-color: var(--nav-border); }
.navbar .btn-ghost:hover { color: var(--nav-ink); border-color: var(--nav-accent); }

/* --- Empty state --- */
.empty-state {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 56px 24px;
  text-align: center;
}
.empty-icon { font-size: 40px; }
.empty-title { font-size: 18px; font-weight: 700; margin: 12px 0 4px; color: var(--ink); }
.empty-sub { color: var(--muted); margin: 0; }

/* --- Data table --- */
.data-table {
  width: 100%;
  border-collapse: collapse;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 12px;
  overflow: hidden;
}
.data-table th, .data-table td {
  text-align: left;
  padding: 10px 14px;
  border-bottom: 1px solid var(--border);
  font-size: 14px;
}
.data-table td { font-variant-numeric: tabular-nums; }
.data-table th {
  background: var(--surface-2);
  color: var(--muted);
  font-weight: 700;
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.data-table tr:last-child td { border-bottom: none; }

/* --- Login --- */
.login-body {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: radial-gradient(1200px 500px at 50% -10%, var(--login-bg-1), var(--login-bg-2));
}
.login-card {
  background: var(--card);
  width: 360px;
  max-width: calc(100vw - 40px);
  padding: 32px 28px;
  border-radius: 14px;
  border-top: 4px solid var(--accent);
  box-shadow: 0 18px 40px var(--login-shadow);
}
.login-brand {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  font-size: 20px;
  font-weight: 800;
  letter-spacing: -0.01em;
  color: var(--ink);
  margin-bottom: 6px;
}
.login-tagline {
  text-align: center;
  color: var(--muted);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  margin: 0 0 22px;
}
.login-form { display: flex; flex-direction: column; }
.login-form label {
  font-size: 13px;
  font-weight: 600;
  color: var(--muted);
  margin-bottom: 6px;
}
.login-form input {
  padding: 10px 12px;
  border: 1px solid var(--control-border);
  border-radius: 8px;
  font-family: inherit;
  font-size: 15px;
  margin-bottom: 16px;
  background: var(--surface);
  color: var(--ink);
}
.login-form input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

/* --- Alerts --- */
.alert { padding: 10px 14px; border-radius: 8px; font-size: 14px; margin-bottom: 16px; }
.alert-error { background: var(--danger-bg); color: var(--danger-t); border: 1px solid var(--danger-br); }
.alert-ok { background: var(--ok-bg); color: var(--ok-t); border: 1px solid var(--ok-br); }

/* --- Controls bar (search / filters / column picker) --- */
.controls-bar {
  display: flex; align-items: flex-start; gap: 14px; flex-wrap: wrap;
  margin-bottom: 16px;
}
.controls-form { flex: 1 1 260px; }
.search-input {
  width: 100%; padding: 9px 16px; font-size: 14px; font-family: inherit;
  border: 1px solid var(--control-border); border-radius: 999px;
  background: var(--card); color: var(--ink);
}
.search-input::placeholder { color: var(--faint); }
.search-input:focus {
  outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
.col-picker { position: relative; }
/* The trigger IS a button (it lives in the page head's action cluster since
   17.08), so it wears `.btn .btn-small` and this rule only adds what a
   <summary> needs on top: no marker, no text selection on a double click. */
.col-picker > summary { list-style: none; user-select: none; }
.col-picker > summary::-webkit-details-marker { display: none; }
.col-picker[open] > summary { border-color: var(--accent); }
.col-picker-panel {
  position: absolute; right: 0; z-index: 20; margin-top: 6px; width: 220px;
  background: var(--card); border: 1px solid var(--border); border-radius: 12px;
  padding: 12px 14px; box-shadow: var(--shadow-pop);
  display: flex; flex-direction: column; gap: 6px;
}
.col-option { font-size: 14px; display: flex; gap: 8px; align-items: center; }
/* Arranging (31.08). The row grows a grab handle and two move buttons; the
   label takes the slack so the buttons sit on one right edge and the eye can
   run down them. Nothing here is a new colour — the buttons are borrowed
   surface, and the drop marker is the accent already used for focus. */
.col-option > label { display: flex; gap: 8px; align-items: center; flex: 1;
  min-width: 0; }
/* The grip is the ONLY thing in a row that starts a drag (31.08). It is also
   the only thing that SAYS the row can be moved: before it, the affordance was
   invisible and the price of guessing wrong was a switched-off column. */
.col-grip { flex: none; cursor: grab; color: var(--muted); font-size: 13px;
  line-height: 1; user-select: none; }
.col-grip:active { cursor: grabbing; }
.col-fixed { padding-left: 18px; }
.col-option.col-dragging { opacity: 0.5; cursor: grabbing; }
.col-option.col-drop-before { box-shadow: 0 -2px 0 0 var(--accent); }
.col-option.col-drop-after { box-shadow: 0 2px 0 0 var(--accent); }
.col-moves { display: flex; gap: 2px; flex: none; }
.col-move {
  font: inherit; font-size: 12px; line-height: 1; padding: 2px 5px;
  color: var(--muted); background: var(--surface-2);
  border: 1px solid var(--control-border); border-radius: 6px; cursor: pointer;
}
.col-move:hover { color: var(--text); border-color: var(--accent); }
.col-move:disabled { opacity: 0.35; cursor: default; }
.col-picker-hint { font-size: 12px; margin: 0 0 2px; }
.col-group { display: flex; flex-direction: column; gap: 6px; }
/* The switched-off columns keep their own block, so «what is on this table,
   in what order» is one uninterrupted list to read (§0: spare). */
.col-group-off { margin-top: 8px; padding-top: 8px;
  border-top: 1px solid var(--border); }
/* A switched-off column has nowhere to move TO, so its buttons are hidden —
   but they are DRAWN, and travel with the row when it is switched on. Leaving
   them out of the markup is what left a freshly ticked column unmovable until
   a reload (31.08). */
.col-group-off .col-moves { display: none; }
/* …and the GRIP too. A switched-off row cannot be dragged — the handler
   refuses it — so a grip that still says «grab me» is an affordance that
   lies, which is the family of the defect this whole grip was built to cure
   (review 01.09, NIT-5). The row keeps its left inset so the two groups line
   up, exactly as `col-fixed` does. */
.col-group-off .col-grip { visibility: hidden; }
/* The same two marks in the TABLE header, turned on their side: a column is
   dropped to the LEFT or to the RIGHT of the one under the cursor, so the
   accent line stands vertically (31.08). Same token as the panel's, because
   it is the same act. */
.data-table th[draggable="true"] { cursor: grab; }
.data-table th.col-dragging { opacity: 0.5; cursor: grabbing; }
.data-table th.col-drop-before { box-shadow: inset 2px 0 0 0 var(--accent); }
.data-table th.col-drop-after { box-shadow: inset -2px 0 0 0 var(--accent); }
/* A column that cannot be switched off looks switched off-proof: dimmed, with
   the reason in `title` (the disabled rule of §4). `cursor: help` because the
   only thing to do here is read why. */
.col-option:has(input:disabled) { opacity: 0.55; cursor: help; }
/* The picker's copy of the list state is wrapped in a span so the search can
   refresh it out of band. A bare <input type=hidden> is not a flex item; the
   wrapper would have been one, adding a second 6px gap and pushing Save down.
   `contents` keeps the element (and its id) in the DOM without a box. */
#picker-state { display: contents; }

/* --- Help dot (DESIGN_SYSTEM §4) ------------------------------------------
   The block-level explanation a card would otherwise print in full, one
   gesture away (Aslan, 14.08.2026 — the technical screens were half muted
   prose). A real <details>, so click and keyboard work with JS off and the
   keyboard gets a disclosure button for free; hover, Escape, click-outside and
   "one at a time" are the app.js enhancement.

   The panel FLOATS on purpose. A hint may never move the page under the
   reader, and inside `.manage-grid` (align-items: stretch) an in-place panel
   would stretch every sibling block in its row — the "grew downwards" failure
   §5a exists to prevent. Never place a dot inside `.table-scroll`: overflow
   clips an absolutely positioned panel.

   No new tokens: every colour here is an existing semantic pair, so the four
   synchronized theme blocks are untouched. The open state reuses the
   `--accent-soft` / `--accent-ink` pairing `.role-superadmin` already ships. */
.help { position: relative; display: inline-block; margin: 0; vertical-align: middle; }
.help > summary {
  list-style: none; cursor: help; user-select: none;
  /* 24px: the WCAG 2.5.8 target floor, and Aslan taps this on an iPad. */
  display: inline-flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; padding: 0;
  border: 1px solid var(--control-border); border-radius: 999px;
  background: var(--surface); color: var(--muted);
  font-size: 12.5px; font-weight: 800; line-height: 1;
}
/* Chrome draws a triangle from `list-style`, Safari from its own pseudo. */
.help > summary::-webkit-details-marker { display: none; }
.help > summary:hover { color: var(--ink); border-color: var(--accent); }
.help[open] > summary {
  background: var(--accent-soft); color: var(--accent-ink); border-color: var(--accent);
}
.help-pop {
  /* Above the sticky action bar (40) and the edge rail (35), below the navbar. */
  position: absolute; left: 0; top: calc(100% + 6px); z-index: 45;
  /* `max-content` so a one-line hint stays a small bubble instead of being
     padded out to a fixed box; the cap is a reading measure (~62 characters). */
  width: max-content; max-width: min(420px, calc(100vw - 32px));
  padding: 12px 14px; border-radius: 12px;
  background: var(--surface); border: 1px solid var(--border-strong);
  box-shadow: var(--shadow-pop);
  /* The dot sits next to headings (800) and eyebrows (uppercase, tracked).
     None of that may leak into running text — drop these four lines and the
     panel next to a `.manage-h` renders in uppercase letter-spaced 800. */
  font-size: 13.5px; font-weight: 400; line-height: 1.55; letter-spacing: normal;
  text-transform: none; text-align: left; white-space: normal;
  color: var(--body-t);
}
.help-pop-end { left: auto; right: 0; }
.help-pop > :first-child { margin-top: 0; }
.help-pop > :last-child { margin-bottom: 0; }
.help-pop p { margin: 0 0 8px; }
.help-pop b, .help-pop strong { color: var(--ink); font-weight: 700; }
.help-pop a { color: var(--accent-ink); }
.help-pop code { font-family: ui-monospace, "Cascadia Mono", Consolas, monospace; font-size: 0.95em; }
/* A heading and its dot share one line. `.h2-actions` cannot do this job: it
   is applied TO the heading, and <details> is flow content — invalid inside
   an <h2>/<h3>. */
.card-head { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin: 0 0 12px; }
/* `.card h2` (below) sets `margin: 0 0 12px` at the SAME specificity and later
   in the file, so a plain `.card-head > h2` reset lost — and `align-items:
   center` centres the MARGIN box, so the heading's text rode 6px high and the
   dot next to it looked misaligned on every card (measured in the browser,
   14.08). The extra `.card` beats it; the row supplies the bottom margin. */
.card-head > h2, .card-head > h3, .card .card-head > h2 { margin: 0; }
/* In a page head the row gap is 12–18px — too far for a mark that belongs to
   the title itself. */
.page-head > .help { margin-left: -6px; }
/* On paper a floating bubble would print over the text beneath it. */
@media print { .help { display: none !important; } }

.btn-small { padding: 7px 13px; font-size: 13px; margin-top: 0; }
.th-sort { color: inherit; text-decoration: none; }
.th-sort:hover { color: var(--accent-ink); }
.th-sort.active { color: var(--ink); }
.table-count { font-size: 12.5px; margin: 8px 2px; }
.table-footer { display: flex; justify-content: space-between; align-items: center; }
.pager { display: flex; gap: 12px; }
.pager-link {
  font-size: 13px; font-weight: 600; color: var(--ink); text-decoration: none;
  padding: 6px 14px; border: 1px solid var(--control-border); border-radius: 999px; background: var(--card);
}
.pager-link:hover { border-color: var(--accent); }

/* --- Quiet edge navigation -------------------------------------------------
   On desktop the otherwise-empty page gutter is a discovery sensor. Hovering
   it reveals a tall arrow-only rail; only that rail is clickable, not the
   full-height sensor. Visible pagers/back links remain the primary controls.
   Keep this component GET-only: never wire the rail itself to save/send
   actions. Stateful workflows require the shared dirty guard
   (DESIGN_SYSTEM §4). */
.edge-nav-side {
  position: fixed;
  top: 56px;
  bottom: 72px; /* leave the lower-right utility/feedback area alone */
  z-index: 35;  /* below the sticky detail bar (40) and navbar (60) */
  display: flex;
  align-items: center;
  justify-content: center;
  /* Include the page's 24px inner padding so the rail is centred between the
     viewport edge and the first visible table/card edge, not glued outside. */
  width: calc((100vw - var(--page-w)) / 2 + 24px);
}
.edge-nav-side-left {
  left: 0;
}
.edge-nav-side-right {
  right: 16px; /* never cover the browser scrollbar */
  width: calc((100vw - var(--page-w)) / 2 + 8px);
}
.edge-nav-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* 2vw intentionally has no upper cap: browser zoom-out increases the CSS
     viewport, so the rail grows in CSS pixels and keeps its perceived width.
     The real gutter remains the final safety cap. */
  width: min(max(30px, 2vw), calc(100% - 4px));
  height: clamp(280px, 34vh, 340px);
  padding: 0;
  border: 1px solid var(--control-border);
  border-radius: 40% / 7%;
  background: linear-gradient(
    180deg,
    color-mix(in srgb, var(--surface) 90%, transparent),
    color-mix(in srgb, var(--accent-soft) 44%, var(--surface)) 50%,
    color-mix(in srgb, var(--surface) 90%, transparent)
  );
  color: var(--accent-ink);
  box-shadow: var(--shadow);
  text-decoration: none;
  opacity: 0;
  pointer-events: none;
  transform: scaleY(0.96);
  transition: opacity 0.18s ease,
    transform 0.22s cubic-bezier(0.2, 0.8, 0.2, 1),
    border-color 0.2s ease, box-shadow 0.2s ease;
}
.edge-nav-side:hover .edge-nav-link,
.edge-nav-side:focus-within .edge-nav-link {
  opacity: 1;
  pointer-events: auto;
  transform: scaleY(1);
}
.edge-nav-link:hover {
  color: var(--accent-ink);
  border-color: var(--accent);
  box-shadow: var(--shadow-accent);
}
.edge-nav-link:focus-visible { outline-offset: 0; }
.edge-nav-arrow {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  flex: 0 1 auto;
  font-size: max(22px, 1.15vw);
  font-weight: 800;
  line-height: 1;
  transition: transform 0.18s ease;
}
.edge-nav-side-left .edge-nav-link:hover .edge-nav-arrow {
  transform: translateX(-0.1em);
}
.edge-nav-side-right .edge-nav-link:hover .edge-nav-arrow {
  transform: translateX(0.1em);
}
/* Progressive enhancement: browsers with CSS corner-shape support render a
   true squircle; the asymmetric radius above is the deliberate fallback. */
@supports (corner-shape: squircle) {
  .edge-nav-link { corner-shape: squircle; }
}
/* A modal or touch interaction has no useful hover-discovery phase. Below the
   desktop three-rail breakpoint the gutter is also too narrow for a safe
   target, so the ordinary visible navigation remains the only control. */
body:has(dialog[open]) .edge-nav { visibility: hidden; }
body:has(input:focus, textarea:focus, select:focus) .edge-nav {
  visibility: hidden;
}
body:has(.htmx-request) .edge-nav { visibility: hidden; }
body.edge-nav-dirty .edge-nav { visibility: hidden; }
@media (max-width: 1180px), (hover: none), (pointer: coarse) {
  .edge-nav { display: none; }
}
/* The styleguide specimen is the same link recipe in normal document flow,
   permanently revealed so both directions can be inspected side by side. */
.edge-nav-demo {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(48px, 10vw, 160px);
  padding: 24px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--surface-2);
}
.edge-nav-demo-side {
  display: flex;
  align-items: center;
  width: auto;
  left: auto;
  right: auto;
}
.edge-nav-demo .edge-nav-link {
  width: max(30px, 2vw);
  opacity: 1;
  pointer-events: auto;
  transform: none;
}
.task-card { border-left: 4px solid var(--accent); }
/* coverage chips + tag pickers */
.chip {
  display: inline-block; padding: 1px 8px; margin: 1px; border-radius: 999px;
  font-size: 11px; font-weight: 600; background: var(--surface-2); color: var(--body-t);
  white-space: nowrap; font-variant-numeric: tabular-nums;
}
.chip-mode { background: var(--info-bg); color: var(--info-t); }
.chip-cap { background: var(--violet-bg); color: var(--violet-t); }
/* an unverified value, never an error: the geo reference does not know this
   airport code (D6). No border, so it keeps the height of the chips beside it. */
.chip-warn { background: var(--warn-bg); color: var(--warn-t); }
/* An HS code with the name the reference gives it (SPEC_HS §3). A nomenclature
   name is a sentence, not a word, so this is the ONE chip that wraps: a 90-
   character nowrap pill is how a page starts scrolling sideways. */
.chip-hs { white-space: normal; max-width: 100%; }
/* The roomy form of the same answer — the name, the "not in HS 2022" warning
   and the DG question — printed under the code they belong to. */
.hs-line { margin: 2px 0 0; font-size: 12.5px; }
.tag-set { border: 1px solid var(--border); border-radius: 10px; padding: 8px 10px; }
.tag-set legend { font-size: 12px; color: var(--muted); padding: 0 4px; }
.tag-opt { display: inline-flex; gap: 3px; align-items: center; font-size: 13px; margin-right: 10px; }
.approvals-card { border-left: 4px solid var(--info-t); }
.shipment-card { border-left: 4px solid var(--ok-t); }
.sub-h { font-size: 14px; font-weight: 700; color: var(--ink); margin: 14px 0 8px; }
.inline-form { display: inline; margin-left: 6px; }
.btn-ghost-dark {
  background: transparent; color: var(--muted);
  border: 1px solid var(--control-border); padding: 7px 13px;
}
.btn-ghost-dark:hover { color: var(--danger-t); border-color: var(--danger-t); }
.st-appr-pending { background: var(--warn-bg); color: var(--warn-t); }
.st-appr-approved { background: var(--info-bg); color: var(--info-t); }
.st-appr-executed { background: var(--ok-bg); color: var(--ok-t); }
.st-appr-rejected { background: var(--neutral-bg); color: var(--neutral-t); }
.create-form { margin-top: 12px; }
.create-form > summary { color: var(--ink); font-weight: 700; cursor: pointer; }
.stack-form { display: flex; flex-direction: column; gap: 10px; max-width: 420px; margin-top: 10px; }
/* A column flex container stretches its children across the full width by
   default, and `width: auto` on the child does NOT opt out — stretch applies
   exactly when the cross size is auto (this is why the old `.btn-inline`
   never fixed it). Buttons hug their own label; fields keep stretching. */
.stack-form > .btn, .pick-field > .btn { align-self: flex-start; }
.stack-form label { font-size: 13px; color: var(--muted); display: flex; flex-direction: column; gap: 4px; }
.stack-form input, .stack-form select, .stack-form textarea {
  padding: 8px 10px; border: 1px solid var(--control-border); border-radius: 8px;
  font-family: inherit; font-size: 14px; background: var(--surface); color: var(--ink);
}
/* Read-only fields are styled once, globally, next to the base control rule
   they have to outweigh — see `input[readonly]` further down. */

/* --- Filters --- */
.page-head { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; margin-bottom: 16px; }
/* One PAIR of duplicate places, side by side inside its table cell. Flex and
   not a grid: an auto-fit grid inside a cell collapses to one column
   (DESIGN_SYSTEM §5a). Colors are inherited — no token of its own is needed. */
.dup-row { display: flex; gap: 18px; flex-wrap: wrap; align-items: center; }
.dup-option { display: inline-flex; gap: 8px; align-items: center; }

.filter-bar { display: flex; gap: 6px; margin-bottom: 0; align-items: center; flex-wrap: wrap; }
.filter-chip {
  padding: 5px 13px; border-radius: 999px; font-size: 12.5px; font-weight: 600;
  text-decoration: none; color: var(--muted);
  border: 1px solid var(--border); background: var(--card);
}
.filter-chip:hover { border-color: var(--border-strong); color: var(--ink); }
.filter-chip.active { background: var(--ink); color: var(--bg); border-color: var(--ink); }
/* Secondary filters that are lists rather than a handful of options. They sit
   under the chips, quieter, because they narrow a view rather than choosing
   one (DESIGN_SYSTEM §0 — colour and emphasis only where they carry meaning). */
.filter-selects { display: flex; gap: 14px; flex-wrap: wrap; align-items: center; }
.filter-selects label { display: flex; align-items: center; gap: 6px; font-size: 12.5px; }
.filter-selects select { font-size: 12.5px; padding: 4px 8px; }

/* --- Status badges --- */
.st-badge {
  display: inline-block; padding: 3px 10px; border-radius: 999px;
  font-size: 12px; font-weight: 700; white-space: nowrap;
  border: 1px solid transparent;
}
.st-ext { background: var(--neutral-bg); color: var(--neutral-t); }
/* ShipThis-side workflow statuses (semantic colours) */
.st-ext-published { background: var(--warn-bg); color: var(--warn-t); }
.st-ext-sent { background: var(--info-bg); color: var(--info-t); }
.st-ext-won { background: var(--ok-bg); color: var(--ok-t); }
.st-ext-lost { background: var(--danger-bg); color: var(--danger-t); }
.st-new { background: var(--neutral-bg); color: var(--neutral-t); }
.st-settling { background: var(--warn-bg); color: var(--warn-t); border: 1px solid var(--warn-br); }
.st-ready { background: var(--ok-bg); color: var(--ok-t); border: 1px solid var(--ok-br); }
.st-needs_info { background: var(--danger-bg); color: var(--danger-t); border: 1px solid var(--danger-br); }
/* Rate workflow in ShipThis (draft → published → accepted). A quotation may
   carry several rates, so each one wears its own badge and the ladder has to
   read at a glance: quiet for a draft, colour for the two that count. An
   unrecognised status keeps the neutral `.st-ext` base it is drawn with. */
.st-rate-draft { background: var(--neutral-bg); color: var(--neutral-t); }
.st-rate-published { background: var(--info-bg); color: var(--info-t); }
.st-rate-accepted { background: var(--ok-bg); color: var(--ok-t); border: 1px solid var(--ok-br); }
/* Provenance inside a badge ("tiltable: yes · default") — quieter by WEIGHT
   only: at 12px this is small text, so it owes 4.5:1, and dimming the badge
   ink to .7 measured 2.92:1 light / 3.73:1 dark (review 2026-07-29 #2). */
.badge-note { font-weight: 500; }
.badge-note::before { content: "· "; }
.st-rfq_draft, .st-rfq_sent { background: var(--info-bg); color: var(--info-t); }
/* Sign-in history (Settings). Two outcomes, so both carry colour: a refused
   attempt is the one worth spotting in a long list. */
.st-login-ok { background: var(--ok-bg); color: var(--ok-t); border: 1px solid var(--ok-br); }
.st-login-fail { background: var(--danger-bg); color: var(--danger-t); border: 1px solid var(--danger-br); }
/* Personal ShipThis key (own profile / Settings user card). Three states, and
   the middle one is the point: a token that is stored but cannot be decrypted
   is NOT a key, and must not look like one. Same recipe as .st-login-*. */
.st-key-set { background: var(--ok-bg); color: var(--ok-t); border: 1px solid var(--ok-br); }
.st-key-bad { background: var(--warn-bg); color: var(--warn-t); border: 1px solid var(--warn-br); }
.st-key-none { background: var(--neutral-bg); color: var(--neutral-t); }
/* P4 card workflow */
.st-audited { background: var(--info-bg); color: var(--info-t); }
.st-logist_review { background: var(--accent); color: var(--on-accent); }
.st-confirmed { background: var(--ok-solid); color: var(--on-ok-solid); }
.st-audit-ok { background: var(--ok-bg); color: var(--ok-t); }
.st-audit-stale { background: var(--warn-bg); color: var(--warn-t); }
.st-audit-error { background: var(--danger-bg); color: var(--danger-t); }
/* MR: local (manual) requests — graphite + mint, the brand lockup */
.st-local { background: var(--local-bg); color: var(--local-t); border: 1px solid var(--local-br); }
/* The SAME request once it has been created in ShipThis (Aslan, 26.08). Born
   here is a fact that never changes, so the plate stays the brand plate and
   only the ink moves: one dimension, two values. It may not stay mint —
   `LOCAL` has to keep meaning «still only here», which is the row somebody
   has to act on. Ink is the dark-theme `--info-t` value, 7.31:1 on the
   graphite plate. */
.st-promoted { background: var(--promoted-bg); color: var(--promoted-t); border: 1px solid var(--promoted-br); }
/* A mirrored customer ShipThis no longer has (D-211). Danger, not warn: the
   row cannot be picked at all, which is a closed door rather than a caution. */
.st-gone { background: var(--danger-bg); color: var(--danger-t); border: 1px solid var(--danger-br); }
/* Sales: per-file visibility on a local rate (§0.3). "internal" is the default
   and by far the common case, so it stays the quiet neutral one; "shared" is
   the deliberate act and gets the colour. */
/* The same fact in prose, under the customer name on a request card. */
.cust-gone { font-size: 12.5px; margin-top: 2px; color: var(--danger-t); }
/* The requests left behind by a customer ShipThis deleted, on that customer's
   card — a task list, so one row per line and the badges inline (26.08). */
.orphan-list { list-style: none; margin: 0 0 10px; padding: 0; }
.orphan-list li { padding: 5px 0; border-bottom: 1px solid var(--border);
  display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap; }
.orphan-list li:last-child { border-bottom: none; }

.st-vis-internal { background: var(--neutral-bg); color: var(--neutral-t); }
.st-vis-creator { background: var(--info-bg); color: var(--info-t); }
/* Sales: what the enquiry is FOR (§0.16a). "not stated" gets no badge at all —
   a badge whose value is always the same says nothing — so only these two
   exist. `budgetary` is warn rather than danger: it is a legitimate request
   that deserves a cheaper answer, not a problem. */
.st-purpose-live { background: var(--info-bg); color: var(--info-t); }
.st-purpose-budgetary { background: var(--warn-bg); color: var(--warn-t); border: 1px solid var(--warn-br); }
/* A local customer waiting to be looked at: a normal queue state, so it
   wears the same warn tokens as `budgetary` right above and for the same
   stated reason — it is a legitimate row, not a problem. It used to draw
   in the danger colours (review 2026-08-01, F-48). */
.st-review { background: var(--warn-bg); color: var(--warn-t); border: 1px solid var(--warn-br); }
/* A badge that is also the control that edits it (one fact, one place — §5a).
   Keeps the pill look, gains the pointer and a real focus ring. */
.badge-btn { cursor: pointer; font-family: inherit; }
.badge-btn:hover { filter: brightness(0.97); }

/* Radio list for the purpose question — a real list of choices, not a
   dropdown, because the external answers it once and must actually read it. */
/* A REQUIRED choice of two with nothing pre-selected: the radios sit at "not
   stated" and the server refuses to guess. Two screens ask one this way —
   the external's "what is this for?" and the alias form's "country or a city
   in it?" (R-1) — so it is ONE rule under two names rather than a copy: the
   options carry a sentence each, which is exactly what `.tag-opt` cannot do
   (it is `white-space: nowrap`, for short tags). */
.purpose-choice, .choice-set { display: flex; flex-direction: column; gap: 6px; margin-top: 6px; }
/* `.stack-form label` is a COLUMN flex (label above field), so inside that
   form the row has to be won back by specificity — the same reason
   `.tag-opt` carries a `.stack-form label.tag-opt` twin. Without it the radio
   sits ABOVE its sentence instead of beside it (found in the browser, 15.08;
   no test can see this). */
.purpose-opt, .choice-opt, .stack-form label.choice-opt {
  display: flex; flex-direction: row; align-items: baseline; gap: 8px;
  font-size: 13.5px; font-weight: 400; color: var(--ink); cursor: pointer;
}
.choice-opt input[type="radio"] { margin: 0; accent-color: var(--accent); flex: none; }

/* Sales: where the deal stands (§6). Colour carries meaning only once there is
   something to say — "no offer" is the resting state of most requests, so it
   stays neutral rather than shouting on every card. */
.st-stage-no_offer { background: var(--neutral-bg); color: var(--neutral-t); }
.st-stage-offer_sent { background: var(--info-bg); color: var(--info-t); }
.st-stage-negotiating { background: var(--violet-bg); color: var(--violet-t); }
.st-stage-won { background: var(--ok-solid); color: var(--on-ok-solid); }
.st-stage-lost { background: var(--danger-bg); color: var(--danger-t); border: 1px solid var(--danger-br); }
.st-stage-no_response { background: var(--warn-bg); color: var(--warn-t); border: 1px solid var(--warn-br); }

/* --- Client offer: one compact LINE per option ---------------------------
   A five-column table inside a ~320px rail is the "wall" DESIGN_SYSTEM §5a
   warns about, so each option is a two-line block instead: the price and its
   label first, everything qualifying it in a quieter second line. */
.offer-list { list-style: none; margin: 0 0 10px; padding: 0; }
.offer-opt { padding: 6px 0; border-bottom: 1px solid var(--border); }
.offer-opt:last-child { border-bottom: none; }
.offer-opt-head { display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap; }
.offer-opt-head b { font-variant-numeric: tabular-nums; }
.offer-opt-sub { display: block; font-size: 12.5px; margin-top: 2px; }
.offer-opt-won { background: var(--ok-bg); border-radius: 8px; padding: 6px 8px; }
.offer-win { color: var(--ok-t); font-weight: 700; }
.margin-pos { color: var(--ok-t); font-weight: 600; }
.margin-neg { color: var(--danger-t); font-weight: 600; }
/* Option rows in the editor: they wrap across the dialog's width rather than
   stacking in one narrow column (§5a — width is for data and controls). */
.offer-row {
  display: flex; flex-wrap: wrap; gap: 8px 10px; align-items: flex-end;
  padding: 8px 0; border-bottom: 1px dashed var(--border);
}
.offer-row label { flex: 1 1 120px; font-size: 13px; color: var(--muted);
  display: flex; flex-direction: column; gap: 4px; }
.offer-row input, .offer-row select { width: 100%; }
/* The row's own Remove button: never stretches like the labelled fields do,
   and sits on the baseline of the inputs rather than of their labels. */
.offer-row-del { flex: 0 0 auto; align-self: flex-end; }
.offer-row-del[hidden] { display: none; }

/* Follow-up draft (§0.16b). Reads as a quotation of what you are about to
   paste, not as an editable field — it is copy-only, and looking like an
   input would promise sending that deliberately does not exist here. */
.followup { margin-top: 12px; padding-top: 10px;
  border-top: 1px solid var(--border-strong); }
.followup-text {
  white-space: pre-wrap; font-family: inherit; font-size: 13px;
  background: var(--surface-2); color: var(--ink);
  padding: 10px; border-radius: 8px; margin: 0 0 8px;
}
.filter-sep { color: var(--muted); padding: 0 2px; }

/* Intake dropzone + file chips (Add request): drag'n'drop, Ctrl+V paste and
   the per-file "AI reads it" / "attach to request" toggles. */
/* Background --surface, not --surface-2. The dashed edge and the "Choose
   files" button inside both draw with --control-border, which measures
   2.70:1 light / 2.88:1 dark on --surface-2 — under the 3:1 floor for the
   boundary of a control (WCAG 1.4.11) — and 3.07 / 3.12 on --surface. That
   edge is the only thing saying "drop files here" to someone who does not
   drag with a mouse, so it is the background that moves, not the token
   (review 2026-08-01, F-35). The dashes still separate the zone from the card. */
.dropzone {
  border: 1px dashed var(--control-border); border-radius: 12px;
  padding: 14px; margin-top: 6px; background: var(--surface);
  position: relative; transition: border-color .15s ease, background .15s ease;
}
.dropzone.is-over { border-color: var(--accent); background: var(--accent-soft); }
.dropzone-input { position: absolute; width: 1px; height: 1px; opacity: 0; }
/* The reset needs the SPECIFICITY, not just the declarations: `.stack-form
   input` (0,1,1) and `.mreq-label input[type=file]` (0,2,1) both beat the bare
   class, so the "hidden" input kept their width:100% and padding — 516x18,
   absolutely positioned with no offsets, sitting invisibly across the first
   line of the hint. Clicking the words "or paste a screenshot" opened the file
   dialog, and the browser refused to click the button underneath it at all.
   Written as a class chain so it does not depend on source order.
   (Found by the browser AFTER the first fix, which changed the declarations
   and left the specificity alone — review 16.08.) */
.mreq-label .dropzone input.dropzone-input,
.stack-form .dropzone input.dropzone-input {
  width: 1px; height: 1px; padding: 0; border: 0; margin: 0;
}
.dropzone-input:focus-visible + .dropzone-hint label {
  outline: 2px solid var(--accent); outline-offset: 2px;
}
/* `.stack-form label` (0,1,1) beats `.btn` (0,1,0), so inside the feedback
   dialog the picker button came out muted-grey like a disabled control — and
   went dark on hover, because `.btn:hover` is specific enough. One recipe must
   not look like two things on two screens (DESIGN_SYSTEM §0). */
.dropzone-hint label.btn { color: var(--ink); }
.dropzone-hint { display: flex; align-items: center; gap: 10px; margin: 0;
                 font-size: 13px; font-weight: 400; flex-wrap: wrap; }
.dropzone-note { margin: 8px 0 0; font-size: 12.5px; font-weight: 400; }
/* No drag'n'drop in this browser — the native picker is the whole story. */
.dropzone-basic { border-style: solid; }
.dropzone-basic .dropzone-hint .muted { display: none; }

.file-chips { list-style: none; margin: 8px 0 0; padding: 0;
              display: flex; flex-direction: column; gap: 6px; }
.file-chip {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 7px 10px; font-size: 13px; font-weight: 400;
  border: 1px solid var(--border); border-radius: 8px; background: var(--surface);
}
.file-chip-thumb { width: 34px; height: 34px; object-fit: cover;
                   border-radius: 6px; border: 1px solid var(--border); }
.file-chip-name { font-weight: 700; color: var(--ink); overflow-wrap: anywhere;
                  flex: 1 1 auto; min-width: 0; }
.file-chip-size { font-variant-numeric: tabular-nums; white-space: nowrap; }
.file-chip-flag { display: flex; align-items: center; gap: 4px;
                  color: var(--muted); white-space: nowrap; }
.file-chip-flag.is-locked { opacity: 0.55; }
.file-chip-x {
  border: 0; background: transparent; color: var(--muted); cursor: pointer;
  font-size: 18px; line-height: 1; padding: 0 4px;
}
.file-chip-x:hover { color: var(--danger-t); }
/* The feedback widget's own list. Three stacked chips pushed Send below the
   fold of a 1366x768 screen while ~800px stood empty either side, so this one
   wraps ACROSS instead of down (canon §5a). The thumbnail is also bigger and
   `contain` rather than `cover`: a 34px crop of the top-left corner of two
   screenshots of the same app is the same grey rectangle twice, and the ×
   next to it is a decision about which picture to drop. */
/* Send can never leave the screen. Measured at 1366x768 with three pictures
   attached: the dialog reaches the UA's max-height and scrolls, and the button
   row sat 36px BELOW the fold — the widget looked like it had no way to send.
   Wrapping the chips (below) was not enough on its own, and trimming content
   until it fits is a fix that lasts until the next line of copy. So the
   actions stay put while the rest scrolls under them (review 16.08). */
.fb-modal .mreq-actions {
  position: sticky; bottom: -14px; margin-bottom: -14px;
  padding: 10px 0 14px; background: var(--surface);
}
.fb-chips { flex-direction: row; flex-wrap: wrap; gap: 8px; }
.fb-chips .file-chip { flex: 0 1 auto; max-width: 100%; }
.fb-chips .file-chip-thumb { width: 72px; height: 54px; object-fit: contain;
                             background: var(--bg); }
.fb-chips .file-chip-name { flex: 0 1 auto; }

/* Customer picker (Sales Workspace §7/§9): search box + suggestion list, used
   in the intake modal, on the request card and in the merge picker. */
/* HEIGHT IS FOR READING, and a list that runs off the screen is not a list
   (Aslan, 24.08). Sixteen answers used to push the "Add request" modal past
   the bottom of the screen and give the page a scrollbar of its own; the cap
   is five rows and half of the sixth — enough of the list to read, with the
   cut row saying there is more. `hidden auto` keeps the rounded corners
   clipping horizontally while the rows scroll. */
.pick-list { list-style: none; margin: 6px 0 0; padding: 0;
             border: 1px solid var(--border); border-radius: 8px;
             max-height: 236px; overflow: hidden auto; }
/* A scrollbar to GRAB with a mouse, not an overlay hair that shows up while
   the wheel turns. The pseudo-elements are what earn that in Blink and WebKit:
   styling them at all switches the element off the overlay bar and onto one
   that takes room in the layout — which is also the only spelling the iPad
   this was reported from (Safari before 18.2) understands. */
.pick-list::-webkit-scrollbar { width: 10px; }
.pick-list::-webkit-scrollbar-track { background: var(--surface-2); }
.pick-list::-webkit-scrollbar-thumb {
  background: var(--border-strong); border-radius: 8px;
  border: 2px solid var(--surface-2);
}
.pick-list::-webkit-scrollbar-thumb:hover { background: var(--control-border); }
/* Firefox has no ::-webkit- pseudo-elements and needs the standard pair — but
   ONLY Firefox may have it: in Chromium `scrollbar-width` DISABLES every rule
   above and hands the list its 0px overlay bar straight back (measured on the
   browser stage, 24.08, before this guard existed). */
@supports not selector(::-webkit-scrollbar) {
  .pick-list { scrollbar-width: thin;
               scrollbar-color: var(--border-strong) var(--surface-2); }
}
.pick-list + .pick-list { margin-top: 6px; }
.pick-list li + li { border-top: 1px solid var(--border); }
.pick-item {
  display: flex; align-items: center; gap: 8px; width: 100%; text-align: left;
  padding: 8px 10px; font-size: 13px; background: var(--surface);
  color: var(--body-t); border: 0; cursor: pointer;
}
.pick-item:hover { background: var(--surface-2); color: var(--ink); }
/* A suggestion row may also be a LINK — the calculators' destination picker
   (W4). Same row, same hover, same guard: what differs is that the whole
   calculation travels in the href, so choosing works with the keyboard, with
   the mouse and with JS off, and the address bar stays the state of the page.
   `text-decoration` has to be said because the row is not `a:not([class])`. */
a.pick-item { text-decoration: none; }
a.pick-item:hover { text-decoration: none; }
.pick-name { font-weight: 700; color: var(--ink); overflow-wrap: anywhere; }
/* A CODE IS NOT A NAME AND MUST NOT BREAK. `.pick-name` wraps anywhere because
   the picker it was written for lists customer names, which can be long enough
   to need it; on a three-letter airport code the same rule broke «BOG» across
   two lines as «BO / G» (seen on the live screen, 01.09). */
.pick-code { white-space: nowrap; overflow-wrap: normal; }
.pick-empty { font-size: 13px; margin: 8px 0 0; }
/* The focus list's own tail (§4): the list carries the whole base, and when a
   base outgrows the ceiling this row says by how many. Deliberately NOT a
   `.pick-item` — no hover, no pointer, nothing to press — so it reads as a
   note the list makes about itself rather than a customer nobody can pick. */
.pick-more { display: block; padding: 8px 10px; font-size: 12px;
             color: var(--muted); background: var(--surface-2); }
/* THE CREATE ROW WITH ITS OWN ANSWER (SPEC_CUSTOMER_WRITE §5, design wave
   31.08). «+ Create «X»» used to be a `.pick-item` — one full-width button.
   It now has to carry a label AND a button AND say where the customer will be
   created, without becoming a form inside a form, so it is a flex row inside
   the same `.pick-list` frame. Deliberately NOT a `.pick-item`: the tap guard
   must fire on the button, not on the row, and nothing about the row itself is
   pressable. */
.pick-create {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 8px 10px; font-size: 13px; background: var(--surface);
}
.pick-create .pick-name { flex: 1 1 auto; min-width: 140px; }
.pick-create .btn { margin-top: 0; }
/* The lines that row is allowed to print about itself: what it will do (quiet)
   and what it wants looked at first (warn). Both sit INSIDE the list frame, at
   the picker's own denser size — `.manage-hint` is the nearest twin and
   belongs to `.manage-block` panels at 12.5px. */
.pick-why {
  display: block; padding: 0 10px 8px; font-size: 12px; line-height: 1.4;
  color: var(--muted); background: var(--surface);
}
.pick-why-warn { color: var(--warn-t); font-weight: 600; }
/* A modifier and not a fourth component: the same row holding a control
   instead of a sentence, so it needs the top padding a text row borrows from
   the row above it. */
.pick-why.pick-opt { padding-top: 8px; }
.pick-chosen { font-weight: 700; color: var(--ink); }
.pick-field { display: flex; flex-direction: column; gap: 6px; }
.pick-field .search-input { width: 100%; }
/* Merge picker on the customer card: the search box and its suggestion rows
   keep the single-column control measure (same 420px as .stack-form) instead
   of spanning the whole page, and the list stays directly under the box.
   Selected via the hx-target attribute because customer_local.html belongs
   to a parallel change (2026-08-01) — fold this into a class there later. */
#merge-suggest, .search-input[hx-target="#merge-suggest"] { max-width: 420px; }

/* MR: Add-request modal (native <dialog>) */
.mreq-modal {
  border: 1px solid var(--border); border-radius: 14px; padding: 18px 20px;
  width: min(560px, 92vw); background: var(--card); color: var(--ink);
}
.mreq-modal::backdrop { background: var(--scrim); }
.mreq-modal h2 { margin: 0 0 12px; font-size: 18px; }
.mreq-label { display: block; margin-bottom: 12px; font-size: 14px; font-weight: 600; }
.mreq-label input[type="file"], .mreq-label textarea {
  display: block; width: 100%; margin-top: 6px; font-weight: 400; font-family: inherit;
  border: 1px solid var(--control-border); border-radius: 8px; padding: 8px; font-size: 13px;
  background: var(--surface); color: var(--ink);
}
.mreq-actions { display: flex; gap: 8px; justify-content: flex-end; align-items: center; }
.mreq-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
             gap: 10px 14px; font-size: 14px; }
.mreq-grid label { display: flex; flex-direction: column; gap: 4px; font-weight: 600; }
.mreq-grid input, .mreq-grid select, .mreq-edit textarea {
  border: 1px solid var(--control-border); border-radius: 8px; padding: 7px 8px;
  font-family: inherit; font-size: 13px; font-weight: 400; width: 100%;
  background: var(--surface); color: var(--ink);
}
.mreq-grid .mreq-check { flex-direction: row; align-items: center; }
.mreq-grid .mreq-check input { width: auto; }
.mreq-wide { grid-column: 1 / -1; }
#mreq-lines input, #mreq-lines select {
  border: 1px solid var(--control-border); border-radius: 6px; padding: 5px 6px;
  font-family: inherit; font-size: 13px; width: 100%;
  background: var(--surface); color: var(--ink);
}
.htmx-indicator { opacity: 0; transition: opacity 200ms; }
.htmx-request .htmx-indicator, .htmx-request.htmx-indicator { opacity: 1; }
.gap-row td { background: var(--surface-2); border-top: none; }

/* Sales task body — free text from ShipThis, keep line breaks */
.task-body {
  white-space: pre-wrap; font-size: 13px; color: var(--body-t);
  background: var(--surface-2); border-left: 3px solid var(--accent);
  padding: 6px 10px; margin: 4px 0 2px; border-radius: 0 8px 8px 0;
}

/* Segment edit: floating panel anchored to the row — never stretches the table */
.seg-details { position: relative; display: inline-block; margin: 0; }
.seg-panel {
  position: absolute; right: 0; z-index: 30; width: 320px;
  background: var(--card); border: 1px solid var(--border); border-radius: 12px;
  padding: 12px; box-shadow: var(--shadow-pop);
}
.seg-panel form { margin: 4px 0; }
.seg-edit label { display: block; font-size: 11px; color: var(--muted); margin: 5px 0 0; }
.seg-edit input, .seg-edit select {
  display: block; width: 100%; box-sizing: border-box; font-family: inherit;
  margin: 2px 0 0; padding: 6px 8px; font-size: 13px;
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--control-border); border-radius: 6px;
}
.st-quoted { background: var(--violet-bg); color: var(--violet-t); }
/* Meeting tasks (B-053). `overdue` is the warn pair the settling badge uses;
   `question` marks a CONFIRM row in the closed tail, the one list where tasks
   and questions stand together. */
.st-overdue { background: var(--warn-bg); color: var(--warn-t);
  border: 1px solid var(--warn-br); }
.st-question { background: var(--info-bg); color: var(--info-t); }
.st-closed { background: var(--neutral-bg); color: var(--neutral-t); }
.settle-left { font-size: 12px; color: var(--warn-t); margin-left: 6px; }
/* Why a disabled action is disabled, standing NEXT to the action. A `title`
   is the tooltip a mouse finds and a finger never does, and these cards are
   read on an iPad (review W0-W2, finding #41). The measure is capped so one
   long sentence cannot push the sticky bar's buttons off their line. */
.action-why { font-size: 12px; color: var(--muted); max-width: 46ch; }
.missing-hint { font-size: 12px; color: var(--danger-t); margin-left: 6px; cursor: help; }
/* "+1" next to a Buy amount: the request carries more rates than the one
   shown. Quiet — it is a pointer to the card, not a warning. */
.rates-more { font-size: 12px; color: var(--muted); margin-left: 6px; cursor: help; }

/* --- Table extras --- */
.row-link { cursor: pointer; }
.row-link:hover td { background: var(--row-hover); }
.cell-ref { font-weight: 700; color: var(--ink); white-space: nowrap; }
/* Agents list roll-up: score then how many scores it averages. */
.cell-rating { white-space: nowrap; }
.cell-time { white-space: nowrap; color: var(--muted); }

/* --- Detail page --- */
.detail-head { gap: 12px; margin-bottom: 14px; }
.detail-head h1 { margin: 0; }
.back-link { margin-left: auto; color: var(--muted); text-decoration: none; font-size: 14px; }
.back-link:hover { color: var(--ink); }
/* Two auto margins in one flex row SPLIT the free space between them — they do
   not queue. A head that has both `.ab-spacer` and a `.back-link` therefore
   stranded its button cluster in mid-air, halfway between the title and the
   back link, instead of flush right like every other page head (review 14.08;
   the pair existed on geo_aliases with one lone button and read as an oddity —
   five buttons on the geo dashboard made it obvious).

   The fix is on the SPACER, not on the back link, and that distinction was
   measured. Neutralising the back link's own `margin-left:auto` fixes the one
   line and breaks the two: `.page-head` wraps, each flex LINE distributes its
   own free space, and a back link that wrapped onto line 2 alone then sat
   flush LEFT under the title — visible below ~845px, i.e. at 175% zoom on a
   1440 screen (re-check, 14.08). Growing the spacer instead consumes the free
   space before any auto margin is considered, so line 1 comes out flush right
   AND a wrapped back link keeps its own auto margin. */
.page-head .ab-spacer { flex: 1 1 auto; margin-left: 0; }
.detail-grid {
  display: grid; grid-template-columns: 1fr 1fr; gap: 18px; margin-bottom: 18px;
}
@media (max-width: 900px) { .detail-grid { grid-template-columns: 1fr; } }
.card {
  background: var(--card); border: 1px solid var(--border);
  border-radius: 12px; padding: 18px 20px; margin-bottom: 18px;
  box-shadow: var(--shadow);
  /* Long unbroken tokens (file names, refs, emails) wrap instead of
     overflowing the card — inherited by everything inside. */
  overflow-wrap: anywhere;
}
/* Copy-critical tokens must never break mid-token (env keys, raw payload) */
.card code, .card pre { overflow-wrap: normal; }
.card h2 { margin: 0 0 12px; font-size: 15px; font-weight: 800; color: var(--ink); }
/* A muted line inside a card, above the door it explains: a plain `p`'s
   own margins push the button away from the sentence that sends people to
   it. A class and not an inline `style=` (D-379). */
.card-note { margin: 0 0 10px; color: var(--muted); }
/* Standalone message card (403, empty states): a full-width panel for two
   sentences reads as a layout bug — cap it at a comfortable measure. */
.msg-card { max-width: 640px; }
.msg-card p { margin: 0 0 12px; font-size: 14px; }
.msg-card p:last-child { margin-bottom: 0; }
/* Retired row (deactivated user): present and readable, visibly secondary.
   Controls inside stay at full strength so they remain operable. */
.row-dim > td { opacity: 0.55; }
.row-dim > td:last-child { opacity: 1; }
/* Creator-facing gaps block: full width above the rails — it is the one thing
   the creator is expected to act on. */
.gaps-card { border-color: var(--border-strong); }
.gaps-card .gap-question { font-size: 12.5px; margin: 2px 0 0 2px; }
.gaps-actions { display: flex; gap: 8px; flex-wrap: wrap; margin: 12px 0 0; }

/* --- Users admin ------------------------------------------------------- */
/* Role badges: one look per rung of the ladder, so the Role column carries
   the whole story and no second "superadmin" badge is needed. */
.role-superadmin { background: var(--accent-soft); color: var(--accent-ink); border-color: var(--accent); }
.role-logist { background: var(--info-bg); color: var(--info-t); }
.role-sales_staff { background: var(--neutral-bg); color: var(--neutral-t); }
.role-sales_external { background: var(--violet-bg); color: var(--violet-t); }

.users-table td { vertical-align: middle; }
/* A date+time never wraps into two lines mid-row, and neither does the button
   next to it: with "Last signed in" added the row is wide enough that at 1366
   the last column started squeezing "Manage" into two lines. The table scrolls
   inside .table-scroll instead (DESIGN_SYSTEM §5a). */
.users-table .users-seen { white-space: nowrap; }
.users-table .manage-btn { white-space: nowrap; }
/* The editing row: full table width, so forms lay out as blocks instead of a
   narrow column squeezed into the last cell. */
.manage-row > td { background: var(--surface-2); padding: 16px 14px; }
.manage-head { display: flex; gap: 10px; align-items: baseline; margin-bottom: 12px; }
/* Flex, not grid: this lives inside a table cell, where `auto-fit` + `1fr`
   need a definite width the table layout does not reliably provide. Flex
   wrapping only needs the available width, so the blocks really do spread
   across the page instead of stacking in one narrow column. */
.manage-grid {
  display: flex; flex-wrap: wrap; gap: 14px; align-items: stretch;
  width: 100%;
}
.manage-block {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 10px; padding: 12px 14px; margin: 0;
  flex: 1 1 300px; min-width: 260px; max-width: 460px;
  display: flex; flex-direction: column;
}
/* Push each block's button to its own bottom edge, so buttons line up across
   a row and never drift away from the fields they belong to. */
.manage-block .manage-actions { margin-top: auto; padding-top: 10px; }
/* When the form is a CHILD of the block (Sales / AI models / Access blocks)
   rather than the block itself (user manage rows), it must fill the block's
   height and be a flex column of its own — otherwise the `margin-top: auto`
   above pushes against the form's own bottom, not the block's, and the
   buttons never line up across the row (the §5a promise). */
.manage-block > form { flex: 1 1 auto; display: flex; flex-direction: column; }
.manage-h { margin: 0 0 10px; font-size: 12.5px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
.manage-hint { font-size: 12.5px; margin: 2px 0 0; }
.manage-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; }
.manage-block .stack-form, .manage-block label { margin: 0; }
.manage-block input, .manage-block select { width: 100%; }
/* Wider dialog for two-column forms (Add user) — the 560px default is a
   single-column measure. */
.modal-wide { width: min(860px, 94vw); }
/* The form inside ANY dialog must actually USE the dialog's width. The dialog
   itself is the measure here — `.stack-form`'s own 420px cap is for forms
   sitting on an open page. Inside a 560px dialog the capped form left fields
   ~140px short of the right edge and looked shifted left; inside the wide
   860px one the blocks that were supposed to sit side by side stacked instead
   (Aslan, 31.07: "блок узкий, а окно широкое"). Width is for data and
   controls — DESIGN_SYSTEM §5a. */
.mreq-modal .stack-form { max-width: none; }

/* --- Updates (release notes) ------------------------------------------- */
.upd-list { list-style: none; margin: 0; padding: 0; }
.upd-item {
  display: flex; align-items: baseline; gap: 12px;
  padding: 9px 0; border-bottom: 1px solid var(--border);
}
.upd-item:last-child { border-bottom: 0; }
.upd-link { font-size: 14px; font-weight: 700; color: var(--accent-ink); text-decoration: none; }
.upd-link:hover { text-decoration: underline; }
/* Date and time as one right-aligned column: the time is the quieter half. */
.upd-when {
  margin-left: auto; display: flex; gap: 10px; align-items: baseline;
  font-size: 12.5px; white-space: nowrap;
}
.upd-date { color: var(--muted); }
.upd-time { color: var(--faint); min-width: 38px; text-align: right; }
.upd-nav { display: flex; gap: 10px; flex-wrap: wrap; justify-content: space-between; }
/* "major" = worth reading in full. Accent-tinted so it reads as a signal
   without shouting; ordinary polish releases carry no badge at all. */
.upd-badge {
  background: var(--accent-soft); color: var(--accent-ink);
  border-color: var(--accent); text-transform: uppercase; letter-spacing: .04em;
}
.upd-major .upd-link { color: var(--ink); }

/* --- Updates hub: latest-5 fold + debts & ideas registries --- */
.upd-rest > summary {
  cursor: pointer; list-style: none; display: inline-block;
  margin-top: 10px; padding: 6px 0;
  color: var(--accent-ink); font-weight: 700; font-size: 13.5px;
}
.upd-rest > summary::-webkit-details-marker { display: none; }
.upd-rest > summary::before { content: '▸ '; color: var(--accent); font-size: 12px; }
.upd-rest[open] > summary::before { content: '▾ '; }
.upd-month { margin: 14px 0 4px; font-size: 13px; font-weight: 800;
  color: var(--muted); text-transform: uppercase; letter-spacing: .04em; }
.hub-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px;
  align-items: start; margin-top: 16px; }
@media (max-width: 1100px) { .hub-grid { grid-template-columns: 1fr; } }
.hub-count { font-size: 14px; font-weight: 600; color: var(--muted); }
/* `var(--line)` used to stand here and that token does not exist: an
   unresolvable var() drops the whole declaration, so both registries had
   no separators at all, in either theme (review 2026-08-01, F-34). The
   release list one line up has always used --border. */
.hub-item { border-bottom: 1px solid var(--border); padding: 7px 0; }
.hub-item:last-child { border-bottom: 0; }
.hub-item > summary {
  display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap;
  cursor: pointer; list-style: none;
}
/* The row expands, so it says so. It killed the native marker and put nothing
   back, which — together with the missing separators above — made both
   registries read as flat text (review 2026-08-01, F-49). Same recipe as
   `.upd-rest > summary` a few rules up. */
.hub-item > summary::before { content: '▸'; color: var(--accent); font-size: 12px; }
.hub-item[open] > summary::before { content: '▾'; }
.hub-item > summary::-webkit-details-marker { display: none; }
.hub-id { font-weight: 700; font-variant-numeric: tabular-nums;
  color: var(--ink); font-size: 13.5px; }
.hub-title { flex: 1 1 auto; min-width: 200px; font-size: 14px;
  color: var(--body-t); }
.hub-item[open] .hub-title { color: var(--ink); }
.hub-date { color: var(--muted); font-size: 12.5px; white-space: nowrap;
  font-variant-numeric: tabular-nums; }
.hub-body { margin: 8px 0 4px 2px; font-size: 13.5px; }
.hub-tail > summary { padding-top: 2px; }
/* The Ideas card holds two sources that must stay visibly apart: typed here
   (plain text) and docs/BACKLOG.md (our Markdown). One small heading each. */
/* A card heading that carries its own action on the same line — the button
   belongs to this block, so it lives at this block's edge, not in the page
   head (DESIGN_SYSTEM §5a). */
.card h2.h2-actions { display: flex; align-items: center; gap: 6px; }
.hub-h { margin: 14px 0 2px; font-size: 12px; font-weight: 800;
  color: var(--muted); text-transform: uppercase; letter-spacing: .04em; }
.hub-h:first-of-type { margin-top: 8px; }

/* --- Categories on the two open registries (19.08.2026) --------------------
   Eight categories, one per entry, shared by debts and ideas. NOT ONE NEW
   COLOUR: the label is grey on purpose, because severity is the only coloured
   thing in a registry row and it answers the more urgent question — eight
   coloured categories next to a red «major» badge sink the badge. */

/* The rail beside the list. Width is for controls, height is for reading
   (§5a): standing the vocabulary on its side costs the list no rows, and
   leaves room for a count and for how much of the category burns. */
.cat-split { display: grid; grid-template-columns: 216px minmax(0, 1fr);
  gap: 22px; margin-top: 10px; }
.cat-rail { align-self: start; border-right: 1px solid var(--border);
  padding-right: 10px; }
.cat-r { display: grid; grid-template-columns: 1fr auto; gap: 1px 8px;
  align-items: baseline; width: 100%; text-align: left;
  padding: 6px 8px 6px 9px; border-left: 3px solid transparent;
  border-radius: 0 8px 8px 0; color: var(--body-t);
  font-size: 13px; line-height: 1.3; }
.cat-r, .cat-r:hover { text-decoration: none; }
.cat-r:hover { background: var(--surface-2); color: var(--ink); }
.cat-r .n { color: var(--muted); font-size: 12.5px;
  font-variant-numeric: tabular-nums; }
/* What merely TOUCHES this category and is filed elsewhere — faint, because
   it is deliberately not part of the sum. */
.cat-r .plus { color: var(--faint); font-size: 11px; margin-left: 3px; }
.cat-r .s { grid-column: 1 / -1; font-size: 11px; color: var(--warn-t);
  font-weight: 700; }
.cat-r[aria-current] { background: var(--surface-2);
  border-left-color: var(--accent); color: var(--ink); font-weight: 700; }
.cat-r[aria-current] .n { color: var(--ink); }
/* A category with nothing open KEEPS its row: «0 open, 16 closed» is an
   answer, and a row that disappears cannot be found again. */
.cat-r.is-zero, .cat-r.is-zero .n { color: var(--faint); }
.cat-rail-sep { height: 1px; background: var(--border); margin: 6px 0 6px 9px; }
.cat-note { margin: 10px 9px 0; font-size: 11px; color: var(--faint);
  line-height: 1.45; }

/* Below the two-column breakpoint the rail does NOT become a tall stack of
   its own — it folds into a row of pills. Measured first as a vertical block:
   eleven rows pushed the first entry ~350px down an iPad, which is worse than
   having no categories at all. */
@media (max-width: 1180px) {
  .cat-split { grid-template-columns: 1fr; }
  .cat-rail { display: flex; flex-wrap: wrap; gap: 6px; align-items: center;
    border-right: 0; border-bottom: 1px solid var(--border);
    padding: 0 0 12px; }
  /* The same pill as `.filter-chip` on the list pages — one recipe, so a
     person meets one control and not two. The 3px marker of the desktop rail
     is overridden on ALL FOUR sides here, or the pill loses its left edge. */
  .cat-r { display: inline-flex; width: auto; gap: 7px; align-items: baseline;
    padding: 5px 13px; border: 1px solid var(--border);
    border-radius: 999px; font-size: 12.5px; font-weight: 600;
    color: var(--muted); background: var(--card); }
  .cat-r:hover { border-color: var(--border-strong); color: var(--ink);
    background: transparent; }
  .cat-r[aria-current] { background: var(--ink); color: var(--bg);
    border-color: var(--ink); }
  .cat-r[aria-current] .n { color: var(--bg); opacity: .8; }
  .cat-r .n { font-size: 11.5px; font-weight: 700; }
  .cat-r .s { display: none; }   /* the severity line needs a second row */
  .cat-rail-sep { width: 1px; height: 16px; margin: 0 2px; }
  .cat-note { flex: 1 0 100%; margin: 2px 0 0; }
}

/* The one label inside a row: the entry's OWN category, never what it
   touches. */
.cat-tags { display: inline-flex; gap: 4px; flex: 0 0 auto; }
.cat-tag { font-size: 11px; font-weight: 700; padding: 2px 8px;
  border-radius: 999px; border: 1px solid var(--border);
  background: var(--surface-2); color: var(--muted); white-space: nowrap; }
.cat-closed .hub-title { color: var(--muted); }
/* The divider over the entries that merely touch the pressed category — and
   over the closures written before the format existed. */
.cat-also { margin: 16px 0 2px; padding-top: 12px;
  border-top: 1px solid var(--border-strong); font-size: 12px;
  font-weight: 800; text-transform: uppercase; letter-spacing: .04em;
  color: var(--muted); }
.cat-also .hub-count { text-transform: none; letter-spacing: 0; }
/* The full name inside an opened entry, and what it also touches. */
.cat-line { margin: 0 0 6px; font-size: 12px; color: var(--muted); }
.cat-line b { color: var(--ink); font-weight: 700; }
.cat-empty { margin: 14px 2px; color: var(--muted); font-size: 13px; }
/* A block a pressed category has nothing to show from. It is HIDDEN rather
   than dropped, because ids inside it are the targets of out-of-band swaps —
   and `hidden` only wins on an element no other rule gives a `display` to. */
.cat-block[hidden] { display: none; }

/* --- Tasks from the recorded meetings on /updates (B-053) ------------------
   The row IS the hub-item recipe above; only four things are new, and every
   colour below is an existing token. The text these rules dress is UNTRUSTED
   (hard rule #7) — escaped by Jinja and never Markdown, so no rule here may
   assume markup inside. */
/* Who owes it. Bold ink next to the muted date, so a person's name reads as a
   name and not as another piece of metadata. */
.hub-who { font-weight: 700; font-size: 13px; color: var(--ink);
  white-space: nowrap; }
/* A row with nothing to disclose is not a <details>, so it has no marker —
   this empty box takes the marker's place and keeps every title on one left
   edge (the marker is 12px, the flex gap supplies the rest). */
.hub-flat { display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; }
.hub-flat::before { content: ''; width: 12px; }
/* What was actually said. A rule down the left rather than italics, which this
   app does not use for voice (DESIGN_SYSTEM §1). */
.task-quote { margin: 0 0 6px; padding-left: 10px; color: var(--body-t);
  border-left: 2px solid var(--border-strong); }
.task-note { margin: 0 0 6px; color: var(--muted); }
/* Which conversation it came from, at which minute — the quietest line on the
   card, and the one that makes a task checkable. */
.task-src { margin: 0; color: var(--faint); font-size: 12.5px; }
.task-foot { margin: 14px 0 0; color: var(--faint); font-size: 12px; }
/* --- /updates dashboard: a row of counters + ONE open registry -------------
   Design FINAL-SPEC 18.08.2026, mock-ups 03a…03d. The row of tiles IS the
   switch between registries, so it must never be scrolled out of reach: it
   sticks under the navbar and, once the page moves, collapses into a single
   line of pills. Each tile deflates WHERE IT IS — nothing slides sideways, so
   the thing under the reader's hand is still the thing under their hand.

   The height it gives up is put back as a bottom margin by app.js, and that is
   not decoration: the collapse shrinks a box that sits in the flow, so without
   the compensation the document — and the scroll position with it — jumps up by
   the difference. It is done in script rather than in CSS because only the
   browser knows what the row's height actually is at this width. A WRAPPER
   holding the space instead would have been simpler and is wrong: sticky only
   sticks inside its own parent, so the row would come unstuck the moment the
   page scrolled past that wrapper (measured, 18.08). */
.dash-bar {
  margin: 16px 0 0;
  position: sticky; top: 56px;
  z-index: 35;                       /* below the detail bar (40), navbar (60) */
  background: var(--bg); padding: 8px 0 12px;
}
/* The line under the row appears only once the row is condensed — that is the
   moment it stops being part of the page and becomes a bar over it. */
.dash-bar::after {
  content: ''; display: block; height: 1px; background: var(--border);
  margin-top: 12px; opacity: 0; transition: opacity .15s ease;
}
.dash-bar.is-condensed::after { opacity: 1; }

/* Not "five columns": the grid decides how many fit, so a sixth or eighth
   registry narrows the tiles in the same row instead of asking for a redesign. */
.dash-tiles { display: grid; gap: 12px;
  grid-template-columns: repeat(auto-fit, minmax(196px, 1fr)); }
.dash-tile {
  display: flex; flex-direction: column; gap: 3px; text-align: left;
  border: 1px solid var(--border); border-radius: 12px; padding: 13px 15px 14px;
  background: var(--surface); text-decoration: none;
  transition: border-color .15s ease, background .15s ease, transform .12s ease;
}
.dash-tile:hover { border-color: var(--control-border); transform: translateY(-1px); }
.dash-tile .n { font-size: 30px; font-weight: 800; line-height: 1.03;
  color: var(--ink); font-variant-numeric: tabular-nums; letter-spacing: -.02em; }
.dash-tile .lbl { font-size: 13px; font-weight: 700; color: var(--body-t); }
.dash-tile .sub { font-size: 11.5px; color: var(--muted); line-height: 1.35; }
/* The single piece of bad news in the row (a task past its date) is not grey
   like the facts around it. */
.dash-tile .sub-warn { color: var(--warn-t); font-weight: 700; }
.dash-tile.is-active { border-color: var(--accent); background: var(--accent-soft);
  box-shadow: var(--shadow-accent); }
.dash-tile.is-active .lbl { color: var(--ink); }
/* A registry that is not built yet: a shape, never an invented number. */
.dash-tile.is-future { border-style: dashed; background: transparent; }
.dash-tile.is-future .n { color: var(--faint); font-weight: 700; }
.dash-tile.is-future .lbl { color: var(--muted); }

.dash-bar.is-condensed { padding: 6px 0 8px; }
.dash-bar.is-condensed .dash-tile { flex-direction: row; align-items: baseline;
  gap: 7px; padding: 7px 13px; border-radius: 999px; }
.dash-bar.is-condensed .dash-tile .n { font-size: 14px; letter-spacing: 0; }
.dash-bar.is-condensed .dash-tile .sub { display: none; }
.dash-bar.is-condensed .dash-tile:hover { transform: none; }

/* The panel. Two columns are what let a whole registry fit the window — and
   the page scrolls normally, because a scroller inside a panel breaks the
   browser's own find, Home/End and the wheel. */
/* No top margin: it would COLLAPSE with the bottom margin app.js puts on the
   condensed row, and the compensation would come up a couple of pixels short.
   The gap is the row's own bottom padding. */
.dash-panel { position: relative; }
/* TWO COLUMNS CUT ON THE SERVER (`app/templating.py::halves`), not balanced by
   the browser. They were balanced by the browser until 19.08.2026, and on
   Safari/iPadOS a long list dropped WHOLE into the first column about a second
   after the page settled — the right half of the panel blank, rows and
   separators ending at the middle (Aslan's screens). It never reproduced here:
   not on Chromium across ~560 measured cells of width × zoom × device scale ×
   scrollbars × minimum font size, and not on the WebKit build available to us,
   including iPad device emulation and a deliberately late webfont swap. So
   this is not a guess at the trigger — it takes the balancer out of the
   picture, and the note that used to stand here already said the balancer
   could empty half a panel (`break-inside` on the list, caught in the mock-up,
   cost a rebuild). Two sibling boxes cannot be re-balanced by anybody. */
.dash-cols { display: grid; grid-template-columns: 1fr 1fr; column-gap: 30px;
  align-items: start; }
/* One column: the halves stack FIRST ABOVE SECOND, which is the original list
   again, in its own order. */
@media (max-width: 1100px) { .dash-cols { grid-template-columns: 1fr; } }
/* A long title has to wrap inside its own half rather than widen the track. */
.dash-col { min-width: 0; }
/* Kept for PRINT, where the page is still fragmented: the break is forbidden
   to the ROW, never to a list. */
.dash-cols .hub-item, .dash-cols .upd-item { break-inside: avoid; }
.dash-cols .upd-month, .dash-cols .hub-h { break-after: avoid; }
/* A long title wraps inside itself and the date stays at the right edge
   instead of dropping to a line of its own. Scoped to the panel on purpose —
   the global `.hub-*` recipe is shared with other pages. */
.dash-cols .hub-item > summary, .dash-cols .hub-flat { flex-wrap: nowrap; }
.dash-cols .hub-title { min-width: 0; }
.dash-cols .hub-id, .dash-cols .hub-date, .dash-cols .hub-who,
.dash-cols .st-badge { flex: 0 0 auto; }
.dash-foot { margin: 14px 0 0; color: var(--faint); font-size: 12px; }
/* A feedback row is a LINK, not a disclosure: its body and pictures live in
   the inbox, so the whole row goes there. */
.fb-line { text-decoration: none; color: inherit; }
.fb-line:hover .hub-title { color: var(--ink); text-decoration: underline; }
/* While the next registry is on its way — and only when it is slow enough to
   notice. The delay IS the feature: a bar that flashes on every press is
   noise, one that shows up at ~200 ms is an answer. */
.dash-wait { position: absolute; inset: 0 0 auto; height: 2px; opacity: 0;
  background: var(--accent); border-radius: 999px 999px 0 0; }
.dash-panel.htmx-request .dash-wait { opacity: 1; transition: opacity .1s ease .2s; }

/* Page speed on /updates (docs/TOOLING.md). Two additions only: air around the
   tables, and a delta that reads as better or worse without the reader doing
   the subtraction. The colours are the semantic tokens the badges already use,
   so the pair follows the theme by itself — and the +/- sign carries the same
   meaning for anyone who does not see the colour. */
.perf-table { margin: 6px 0 4px; }
.perf-worse { color: var(--danger-t); font-weight: 700; }
.perf-better { color: var(--ok-t); font-weight: 700; }
/* A typed idea is shown EXACTLY as written — no Markdown pass — so the line
   breaks the author made have to survive on their own. */
.idea-text { white-space: pre-wrap; margin: 0 0 6px; color: var(--body-t); }
.idea-meta { font-size: 12.5px; margin: 0 0 8px; }
/* The edit form is the standard `.stack-form` recipe, minus its 420px cap:
   a 4000-character textarea inside it grew DOWN as a narrow column on a wide
   screen — exactly the case §5a exists for, and the same reason
   `.mreq-modal .stack-form` drops the cap (review 04.08, #8). */
.idea-edit { margin: 0 0 10px; gap: 6px; max-width: none; }
.idea-stamp { font-size: 12px; }
.idea-actions { display: flex; gap: 8px; align-items: center;
  flex-wrap: wrap; margin-bottom: 6px; }
.idea-bid { width: 92px; font-variant-numeric: tabular-nums; }
.idea-tail-row { margin: 0 0 6px; }
/* Debt severity — the registry's four levels on the semantic tokens. */
.sev-blocker { background: var(--danger-bg); color: var(--danger-t);
  border: 1px solid var(--danger-br); }
.sev-major { background: var(--warn-bg); color: var(--warn-t);
  border: 1px solid var(--warn-br); }
.sev-minor { background: var(--info-bg); color: var(--info-t); }
.sev-hygiene { background: var(--neutral-bg); color: var(--neutral-t); }
/* A fifth badge for a heading the parser could not read: the entry stays on
   the page rather than disappearing, and its badge says the level is unknown
   instead of rendering as an uncoloured blank. */
.sev-unknown { background: var(--neutral-bg); color: var(--neutral-t);
  border: 1px dashed var(--border-strong); }

/* Rendered Markdown (a patchnote). Reading measure is capped: full-width
   prose at 1920 is unreadable. Everything below is layout + tokens only. */
.md-body { max-width: 860px; font-size: 14.5px; line-height: 1.7; color: var(--body-t); }
.md-body > :first-child { margin-top: 0; }
.md-body > :last-child { margin-bottom: 0; }
/* Headings get a real rule above them, like a Markdown preview: on a long
   release note the sections have to be findable by eye alone. */
.md-body h1 {
  font-size: 22px; font-weight: 800; color: var(--ink); margin: 0 0 6px;
  letter-spacing: -0.01em; line-height: 1.3;
}
.md-body h1 + p { margin-top: 14px; }
.md-body h2 {
  font-size: 16px; font-weight: 800; color: var(--ink);
  margin: 30px 0 12px; padding-top: 18px;
  border-top: 1px solid var(--border-strong);
}
.md-body h3 { font-size: 14.5px; font-weight: 700; color: var(--ink); margin: 20px 0 7px; }
.md-body p { margin: 0 0 12px; }
.md-body ul, .md-body ol { margin: 0 0 12px; padding-left: 22px; }
.md-body li { margin: 5px 0; }
.md-body li > ul, .md-body li > ol { margin: 5px 0; }
.md-body strong { color: var(--ink); font-weight: 700; }
.md-body a { color: var(--accent-ink); }
.md-body hr { border: 0; border-top: 1px solid var(--border); margin: 22px 0; }
.md-body blockquote {
  margin: 0 0 11px; padding: 2px 0 2px 14px;
  border-left: 3px solid var(--border-strong); color: var(--muted);
}
.md-body code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px;
  background: var(--code-bg); color: var(--code-ink);
  border: 1px solid var(--code-border); border-radius: 5px; padding: 1px 5px;
}
.md-body pre {
  background: var(--code-bg); color: var(--code-ink);
  border: 1px solid var(--code-border); border-radius: 8px;
  padding: 12px 14px; overflow-x: auto; margin: 0 0 11px;
}
.md-body pre code { background: none; border: 0; padding: 0; font-size: 12.5px; }
/* Tables stay real tables (borders survive) and scroll inside the wrapper the
   route adds — the page itself never scrolls sideways. */
.md-body .table-scroll { margin: 0 0 16px; }
.md-body table {
  border-collapse: collapse; width: 100%;
  border: 1px solid var(--border-strong); border-radius: 8px;
  overflow: hidden;
  font-size: 14px; font-variant-numeric: tabular-nums;
}
.md-body th, .md-body td {
  border-bottom: 1px solid var(--border); padding: 9px 13px; text-align: left;
  vertical-align: top;
}
.md-body th + th, .md-body td + td { border-left: 1px solid var(--border); }
.md-body tbody tr:last-child > * { border-bottom: 0; }
.md-body th {
  background: var(--surface-2); color: var(--ink); font-weight: 700;
  border-bottom: 1px solid var(--border-strong);
}
.md-body tbody tr:nth-child(even) { background: var(--surface-2); }
.kv-list { display: grid; grid-template-columns: 160px 1fr; row-gap: 7px; margin: 0; font-size: 14px; font-variant-numeric: tabular-nums; }
.kv-list dt { color: var(--muted); }
.kv-list dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
/* Facts listed under a form inside a card keep the form's own rhythm — the
   rule replaces the inline margin the customer card used to carry. */
.card > .stack-form + .kv-list { margin-top: 14px; }
/* Narrow card rails: tighter label column so long values don't overflow */
.detail-rail .kv-list { grid-template-columns: 126px 1fr; }
/* One rate inside the Rate card. Several of them stack down the right rail
   (Aslan, 12.08.2026: the rail has the room, and every rate must be visible),
   separated by a rule instead of a nested card — a card in a card reads as
   two levels of importance where there is only one. */
.rate-entry + .rate-entry { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border); }
.rate-entry-head { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-bottom: 8px; }
.trail { list-style: none; margin: 0; padding: 0; font-size: 13px; }
.trail li { padding: 6px 0; border-bottom: 1px solid var(--border); display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap; min-width: 0; }
.trail li > a { min-width: 0; }
.trail li:last-child { border-bottom: none; }
.trail-ts { color: var(--muted); white-space: nowrap; font-variant-numeric: tabular-nums; }
.trail-action { font-weight: 700; color: var(--ink); }
.trail-missing { color: var(--danger-t); }
.muted { color: var(--muted); }
details > summary { cursor: pointer; font-weight: 700; color: var(--ink); }
.payload-json {
  margin-top: 12px; padding: 14px; background: var(--code-bg); color: var(--code-ink);
  border: 1px solid var(--code-border);
  border-radius: 10px; font-size: 12.5px; line-height: 1.45;
  overflow-x: auto; max-height: 600px; overflow-y: auto;
}

/* --- Agents: country chips (fill = KYC state, letter = source) --- */
.country-chip {
  display: inline-flex; align-items: center; gap: 3px;
  padding: 2px 9px; margin: 1px 2px 1px 0; border-radius: 999px;
  font-size: 12px; font-weight: 600; white-space: nowrap;
  border: 1px solid var(--border); background: var(--card); color: var(--muted);
}
.country-chip[role="button"] { cursor: pointer; }
.country-chip[role="button"]:hover { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); }
.country-chip.kyc-passed { background: var(--ok-bg); color: var(--ok-t); border-color: var(--ok-br); }
.country-chip.kyc-failed {
  color: var(--danger-t); border-color: var(--danger-br);
  background: repeating-linear-gradient(45deg, var(--danger-bg), var(--danger-bg) 6px, var(--surface) 6px, var(--surface) 12px);
}
.country-chip.kyc-conflict::after {
  content: ""; width: 7px; height: 7px; border-radius: 50%;
  background: var(--amber-strong); display: inline-block; margin-left: 3px;
}
.chip-check { font-size: 11px; }
.src-badge {
  font-size: 9px; font-weight: 700; letter-spacing: .3px; line-height: 1.5;
  background: var(--ink); color: var(--bg); border-radius: 4px; padding: 0 3px;
}
.subagent-mark { color: var(--amber-strong); font-size: 11px; }
/* --- Per-field provenance on the MR edit form (SPEC_SHIPTHIS_WRITE §5a) ---
   THREE weights, not five, though the vocabulary has five kinds: a badge that
   varies by colour without the colour MEANING anything is noise on a form
   with fourteen of them (counted 25.08; DESIGN_SYSTEM §5a — "is anything on screen saying
   nothing"). What the three say:
     quiet   — the AI read it, or the system worked it out. Nobody decided.
     warning — a DEFAULT is standing in because the request said nothing, and
               that assumption is what goes out in the RFQ. Worth spotting.
     colour  — a PERSON edited it, and no AI pass overwrites that (§5a).
   All existing tokens, so the four theme blocks need no new entries. It is
   NOT `.src-badge`: that name is the geo reference's and means something
   else entirely (one look, one meaning — series review #3). */
.prov-badge {
  display: inline-block; margin-left: 6px; padding: 1px 7px;
  border-radius: 999px; font-size: 11px; font-weight: 600;
  white-space: nowrap; vertical-align: middle;
  border: 1px solid transparent;
}
.prov-document, .prov-text, .prov-computed {
  background: var(--neutral-bg); color: var(--neutral-t);
}
.prov-default { background: var(--warn-bg); color: var(--warn-t); border-color: var(--warn-br); }
.prov-human { background: var(--info-bg); color: var(--info-t); }
/* A cargo dictionary value ShipThis does not have (D-185). It is a WARNING,
   not an error: the field still holds what the extraction read, and the
   truth-gate is the live match at write time — this only says, before the
   person gets there, that this word will be refused. */
.dict-unknown { border-color: var(--warn-br) !important; }
.dict-hint {
  display: block; margin-top: 2px; font-size: 11px; color: var(--warn-t);
}
/* …and the same line when it is INFORMATION rather than a warning. Amber is
   a message on this screen — /styleguide says «Amber means WARN and only
   warn» under these very samples — and the name of a code somebody typed
   correctly is not a warning (review F-7). Same size, same place, quiet. */
.dict-hint.hs-ok { color: var(--muted); }
.mreq-files { margin: 0 0 12px; }
.mreq-files .mreq-check { display: inline-flex; gap: 4px; font-weight: 500; }
/* A quiet FACT about a row — a seaport, a customs zone, "this is a city name".
   It used to be byte-identical to `.chip-warn`, which means "we could not
   verify this": two meanings, one look, and they sit next to each other on
   /styleguide (series review #3, §5.3). An outline chip says "a label", the
   amber fill above says "look at this". Padding gives the border its 2px back
   so the chips beside it keep their height. */
.chip-svc {
  background: var(--surface-2); color: var(--muted);
  border: 1px solid var(--border); padding: 0 7px;
}

/* --- Tag grids (inline chip checkboxes; overrides the .stack-form column rule
       that used to leave checkboxes floating above their labels) --- */
.tag-columns { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
@media (max-width: 900px) { .tag-columns { grid-template-columns: 1fr; } }
.tag-grid { display: flex; flex-wrap: wrap; gap: 4px 12px; }
.tag-opt, .stack-form label.tag-opt {
  display: inline-flex; flex-direction: row; gap: 5px; align-items: center;
  font-size: 13px; margin: 0; color: var(--text); white-space: nowrap;
}
.tag-opt input[type="checkbox"] { margin: 0; accent-color: var(--accent); }
.tag-opt input[type="radio"] { margin: 0; accent-color: var(--accent); }
/* An option that EXISTS but cannot be taken today — a surcharge currency with
   no exchange rate (SPEC_CALCULATORS decision 12). Shown, not hidden: an
   option that disappears reads as one that was never offered, and the reason
   printed under it is what tells a person what to do about it. Muted, never
   danger: nothing is wrong, something is missing. */
.tag-opt-off, .stack-form label.tag-opt-off { color: var(--muted); cursor: not-allowed; }

/* --- Branch (country) panel --- */
/* An accent EDGE, not an accent fill: the open panel is half a screen of
   forms, and "never accent large areas" (DESIGN_SYSTEM §1, ≤10%) applies.
   Same recipe as .task-body / .approvals-card — the coloured left rule says
   "this is the open one" without shouting. */
.branch-panel {
  margin-top: 12px; padding: 16px 18px; border: 1px solid var(--border);
  border-left: 4px solid var(--accent);
  border-radius: 12px; background: var(--surface-2);
}
.branch-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
@media (max-width: 900px) { .branch-grid { grid-template-columns: 1fr; } }
/* One criterion per row. The note takes whatever is left of the row (§10) and
   the meta caption sits at the END of the same row — the grid used to declare
   four tracks for five children, so the caption always started an implicit
   second row and the note never reached the right edge. Named areas make the
   narrow layout a re-flow rather than a re-order. */
.rating-row {
  display: grid; gap: 8px; align-items: center; margin-bottom: 6px;
  font-size: 14px; font-variant-numeric: tabular-nums;
  grid-template-columns: 210px 64px minmax(0, 1fr) auto;
  grid-template-areas: "label score note meta";
}
.rating-label { grid-area: label; }
.rating-score, .rating-score-ro { grid-area: score; }
.rating-note, .rating-note-ro { grid-area: note; min-width: 0; }
.rating-meta { grid-area: meta; font-size: 11px; white-space: nowrap; }
.rating-row.is-stale { opacity: .55; }
.rating-none { font-style: normal; }
.rating-actions { display: flex; align-items: center; gap: 10px;
  flex-wrap: wrap; margin-top: 8px; }
.rating-legend { font-size: 12px; }
.rating-score-ro { font-weight: 700; color: var(--ink); }
/* The editable row has one extra control (n), so it needs its own five-track
   version of the same areas. */
.rating-row:not(.rating-row-ro) {
  grid-template-columns: 210px 56px 64px minmax(0, 1fr) auto;
  grid-template-areas: "label score n note meta";
}
.rating-row:not(.rating-row-ro) .rating-n { grid-area: n; }
/* Narrow: the criterion keeps its own line with the score, and the note wraps
   UNDER it across the full width instead of being squeezed into a 64px track. */
@media (max-width: 900px) {
  .rating-row, .rating-row:not(.rating-row-ro) {
    grid-template-columns: minmax(0, 1fr) 56px 64px;
    grid-template-areas: "label score n" "note note note" "meta meta meta";
    margin-bottom: 12px;
  }
  .rating-row-ro { grid-template-areas: "label score score" "note note note" "meta meta meta"; }
  .rating-meta { white-space: normal; }
}
.rating-row select, .rating-row input {
  padding: 5px 8px; border: 1px solid var(--control-border); border-radius: 6px;
  font-family: inherit; font-size: 13px; background: var(--surface); color: var(--ink);
  min-width: 0;
}
.inline-edit > summary { list-style: none; cursor: pointer; }

/* --- Contact role colours (chip colour = role; country stays text) --- */
.role-pricing { background: var(--ok-bg); color: var(--ok-t); }
.role-sales { background: var(--info-bg); color: var(--info-t); }
.role-ops { background: var(--warn-bg); color: var(--warn-t); }
.role-customs { background: var(--violet-bg); color: var(--violet-t); }
.role-accounting { background: var(--rose-bg); color: var(--rose-t); }
.role-management { background: var(--neutral-bg); color: var(--neutral-t); }
.role-general { background: var(--neutral-bg); color: var(--muted); }

/* --- request detail: parser-completeness additions (gap report 2026-07) --- */
.st-dg { background: var(--dg-bg); color: var(--dg-t); font-weight: 700; }
.instructions-text { white-space: pre-wrap; font-size: 15px; }
.route-chain { list-style: none; margin: 0; padding: 0; }
.route-chain li { padding: 6px 0 6px 18px; position: relative; border-left: 2px solid var(--border-strong); margin-left: 8px; }
.route-chain li::before { content: ''; position: absolute; left: -6px; top: 12px; width: 10px; height: 10px; border-radius: 50%; background: var(--accent); border: 2px solid var(--surface); }
.route-kind { display: inline-block; min-width: 90px; color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: .04em; }
.route-label { font-weight: 700; }
.cargo-totals { display: flex; flex-wrap: wrap; gap: 6px; margin: 10px 0 4px; }

/* --- QS quick-send (SPEC_QUICKSEND) --- */
.qs-oneliner { margin: -10px 0 16px; font-size: 15px; color: var(--muted); }
.qs-section > summary { cursor: pointer; list-style: none; }
.qs-section > summary h2 { display: inline-block; margin: 0; }
.qs-section > summary::before { content: '▸ '; color: var(--accent); }
.qs-section[open] > summary::before { content: '▾ '; }
.qs-check-list { list-style: none; margin: 0; padding: 0; }
.qs-check { padding: 6px 0; font-size: 14px; border-bottom: 1px dashed var(--border); }
.qs-check:last-child { border-bottom: none; }
.qs-check-icon { display: inline-block; min-width: 22px; font-weight: 700; }
.qs-check-ok .qs-check-icon { color: var(--ok-t); }
.qs-check-warn { background: var(--warn-bg); }
.qs-check-warn .qs-check-icon { color: var(--warn-t); }
.qs-check-block { background: var(--danger-bg); }
.qs-check-block .qs-check-icon { color: var(--danger-t); }
.alert-warn { background: var(--warn-bg); color: var(--warn-t); border: 1px solid var(--warn-br); }
.qs-btn { width: auto; font-weight: 800; }
.qs-actions { display: flex; gap: 10px; align-items: center; margin: 10px 0; flex-wrap: wrap; }
.qs-field { margin: 10px 0; }
.qs-label { display: flex; align-items: center; gap: 10px; font-weight: 700; color: var(--ink); margin-bottom: 4px; }
.qs-subject { width: 100%; font-family: inherit; font-size: 14.5px; font-variant-numeric: tabular-nums; padding: 8px 12px;
  background: var(--surface); color: var(--ink); border: 1px solid var(--control-border); border-radius: 8px; }
/* pre-wrap (not pre): long lines wrap instead of forcing a horizontal
   scrollbar; auto-grow JS removes the vertical one so the whole letter shows */
.qs-body { width: 100%; font-family: inherit; font-size: 14.5px; line-height: 1.6; font-variant-numeric: tabular-nums; padding: 12px 14px;
  white-space: pre-wrap; overflow-wrap: break-word; resize: vertical;
  background: var(--surface); color: var(--ink); border: 1px solid var(--control-border); border-radius: 8px; }
.qs-body-main { min-height: 620px; }
/* One copy button everywhere (D-010): quicksend + the offer cards. */
.copy-btn { margin: 0; }
/* Signature footnote under the letter: quiet, and pre-line because the
   signature itself is multi-line plain text (users_util.SIGNATURE_TEMPLATE). */
.qs-signature-preview { margin: 6px 0 0; font-size: 12.5px; }
.qs-signature-text { white-space: pre-line; color: var(--body-t); }
/* ready-to-paste package block: email text, so it wraps like the letter does */
.qs-packages { font-family: inherit; font-size: 14px; line-height: 1.55;
  font-variant-numeric: tabular-nums; white-space: pre-wrap; overflow-wrap: break-word;
  margin: 0 0 8px; padding: 10px 12px; background: var(--surface);
  color: var(--ink); border: 1px solid var(--border); border-radius: 8px; }
.qs-model { font-size: 12px; font-weight: 400; }
.qs-notes { white-space: pre-wrap; }
.qs-edit-block { border: 1px solid var(--border); border-radius: 10px; padding: 8px 12px; margin: 8px 0; }
.qs-edit-block > summary { cursor: pointer; font-weight: 700; color: var(--ink); }
.qs-revlog { margin-top: 8px; font-size: 13px; }
.qs-country-row { display: flex; gap: 14px; align-items: center; margin: 12px 0; flex-wrap: wrap; }
.qs-country-row select { padding: 7px 10px; font-family: inherit; background: var(--surface); color: var(--ink);
  border: 1px solid var(--control-border); border-radius: 8px; }
.qs-agent { border: 1px solid var(--border); border-radius: 10px; margin: 8px 0; padding: 6px 12px; }
/* Two fixed lines, not one wrapping flex row. The side column is ~460px wide
   at 1366, where name + badges + six tag chips + the rating strip + the
   counter wrapped into three unpredictable lines and the counter itself moved
   between them — 61 of 88 rows broke (browser check 2026-08-02). Now the
   counter owns the top-right cell and the rating/tag strip owns the second
   line, so row height is a function of the agent NAME alone. */
/* The hide control (feedback #29) shares the RIGHT-HAND track with the address
   counter, on the second line. It had a track of its own spanning both lines
   first, and that was measured wrong in the browser at 1920: the identity line
   is the crowded one — name, KYC chip, "RFQ sent", now "used N×" — and taking
   56px off it wrapped it onto a second line, growing the row from 49px to 77px.
   Which is the very thing this grid exists to prevent. The rating/tag line has
   slack (66px of content in a 579px track, measured), so the button costs
   nothing there, and the right-hand column is sized by the wider of the two
   anyway — the counter — so the identity line is back to its full width. */
.qs-agent > summary {
  cursor: pointer;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas: "id count" "meta act";
  align-items: center; column-gap: 10px; row-gap: 2px;
}
.qs-agent-id { grid-area: id; min-width: 0;
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.qs-agent-count { grid-area: count; justify-self: end; white-space: nowrap; }
.qs-agent-meta { grid-area: meta; min-width: 0;
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.qs-agent-act { grid-area: act; justify-self: end; align-self: center; }
/* One notch shorter than a normal .btn-small, and only in the vertical: a 33px
   control on a 21px line adds 12px to EVERY row, and a country like AE has 153
   of them — this list is scanned, not read (§5a). The type stays at 13px; §7
   forbids shrinking that to fit. */
.qs-agent-act .btn { padding-top: 3px; padding-bottom: 3px; }
.qs-agent-all { transform: scale(1.15); accent-color: var(--accent); }
/* A brand marked paused / do_not_use: listed last, no select-all, and the
   placeholder keeps the name column aligned with the rows that do have a box. */
.qs-agent-inactive { border-style: dashed; }
.qs-agent-nopick { width: 15px; text-align: center; color: var(--danger-t); font-weight: 700; }
.st-agent-do_not_use { background: var(--danger-bg); color: var(--danger-t); border: 1px solid var(--danger-br); }
.st-agent-paused { background: var(--warn-bg); color: var(--warn-t); border: 1px solid var(--warn-br); }
.qs-agent-sent { background: var(--info-bg); color: var(--info-t); }
/* "you have written to them N times" — the axis the list is ordered by
   (B-061). Drawn only from 1 up, so it says something wherever it appears. */
.qs-agent-used { background: var(--ok-bg); color: var(--ok-t); }
/* The hide dialog: the .mreq-modal recipe, plus a right-aligned action row.
   The reason radios reuse .choice-opt (row layout inside a .stack-form). */
.qs-hide-why { border: 0; padding: 0; margin: 0; display: flex;
  flex-direction: column; gap: 6px; }
.qs-hide-why legend { padding: 0; font-size: 13px; color: var(--muted); }
.qs-hide-actions { display: flex; gap: 8px; flex-wrap: wrap;
  justify-content: flex-end; }
/* What the country's list drops, and the way back. Collapsed by default —
   on most countries it is empty or holds one line. */
.qs-hidden-strip { margin-top: 10px; font-size: 13px; }
.qs-hidden-strip > summary { cursor: pointer; }
.qs-hidden-list { list-style: none; margin: 8px 0 0; padding: 0;
  display: flex; flex-direction: column; gap: 6px; }
.qs-hidden-list li { display: flex; align-items: center; gap: 6px;
  flex-wrap: wrap; }
/* The same marks on the agent card: what is hidden, why, by whom, and back. */
.hidden-marks { margin: 10px 0 0; }
.hidden-marks-head { margin: 0 0 6px; font-size: 12.5px; font-weight: 700;
  color: var(--navy); }
.hidden-mark-list { list-style: none; margin: 0; padding: 0; font-size: 13px;
  display: flex; flex-direction: column; gap: 6px; }
.hidden-mark-list li { display: flex; align-items: center; gap: 6px;
  flex-wrap: wrap; }
.hidden-mark-undo { margin: 0; }
.qs-tag-more { color: var(--muted); }
/* "the column says UAE, this came from the drop address" — a second line
   inside the transit banner. Deliberately NOT .muted: --muted has never been
   contrast-checked against --warn-bg, and inheriting the alert's own ink is
   both safer and the right emphasis. */
.qs-transit-src { display: block; margin-top: 6px; font-size: 13px; }
/* Rating strip in the picker (§10): the score the list is ordered by, then the
   four criteria it is made of. One flex row so it never pushes the summary
   into a second line on its own; tabular digits so the numbers line up down
   the list. */
.qs-rating-avg { font-weight: 700; }
.qs-rating-scope { font-weight: 400; color: var(--muted); }
.qs-rating-mini { display: inline-flex; gap: 4px; flex-wrap: wrap; align-items: center;
  font-size: 11px; font-variant-numeric: tabular-nums; }
.qs-rating-crit { display: inline-flex; gap: 2px; align-items: baseline;
  padding: 1px 5px; border-radius: 999px; background: var(--surface-2);
  color: var(--muted); }
.qs-rating-crit b { color: var(--ink); font-weight: 700; }
/* --muted, not --faint: on --surface-2 (the chip's own background)
   --faint measures 4.42:1 light / 4.27:1 dark, under the 4.5:1 the
   design system names by name — the token is fine, the background under
   it is darker than the one it was checked on (F-35). */
.qs-rating-crit.is-empty b { color: var(--muted); font-weight: 400; }
.qs-rating-none { color: var(--muted); }
.qs-contact-groups { padding: 6px 0 4px 26px; }
.qs-group-label { margin: 8px 0 2px; font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
.qs-contact-list { list-style: none; margin: 0; padding: 0; }
.qs-contact { padding: 3px 0; }
.qs-contact label { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; cursor: pointer; }
.qs-contact input[type="checkbox"] { accent-color: var(--accent); }
.qs-contact-other { opacity: .55; }
.qs-email-addr { font-family: ui-monospace, monospace; font-size: 13px; }
.qs-summary { font-weight: 700; }
.qs-attach { margin: 14px 0; border-top: 1px dashed var(--border); padding-top: 10px; }
.qs-attach-toggle { display: flex; gap: 8px; align-items: center; cursor: pointer; }
.qs-attach-list { margin: 8px 0 0 24px; }
.qs-others > summary { cursor: pointer; font-size: 13px; }
.qs-progress-row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; padding: 5px 0; border-bottom: 1px dashed var(--border); font-variant-numeric: tabular-nums; }
.st-qs-queued { background: var(--neutral-bg); color: var(--neutral-t); }
.st-qs-sending { background: var(--warn-bg); color: var(--warn-t); }
.st-qs-sent { background: var(--ok-bg); color: var(--ok-t); }
.st-qs-failed { background: var(--danger-bg); color: var(--danger-t); }
.st-qs-cancelled { background: var(--neutral-bg); color: var(--neutral-t); }
/* --- QS full-width layout (PC-first, 16:9; Aslan's feedback 2026-07-12) --- */
/* The quicksend page opts out of the 1100px .content cap so the letter can
   dominate the viewport on a wide monitor; side columns are pushed to the
   edges. Capped so it stays sane on ultrawide screens. */
.content-wide { max-width: var(--page-w); }
.qs-layout {
  display: grid;
  /* Aslan 18.07: the letter dominates the LEFT half; facts, recipients and
     progress stack on the RIGHT — no more empty right half. */
  grid-template-columns: minmax(600px, 58fr) minmax(400px, 42fr);
  gap: 16px; align-items: start;
}
.qs-col { min-width: 0; }
.qs-col .card { margin-bottom: 16px; }
.qs-cargo-table { font-size: 12px; }
.qs-cargo-table th, .qs-cargo-table td { padding: 4px 6px; }
.qs-party { margin: 6px 0 0; font-size: 13px; }
.qs-dirty-hint { color: var(--warn-t); font-weight: 400; }
@media (max-width: 1160px) { .qs-layout { grid-template-columns: 1fr; } }
/* --- QS AI chat --- */
.qs-chat-log { max-height: 340px; overflow-y: auto; margin-bottom: 10px; border: 1px solid var(--border); border-radius: 10px; padding: 8px 12px; }
.qs-chat-entry { padding: 6px 0; border-bottom: 1px dashed var(--border); }
.qs-chat-entry:last-child { border-bottom: none; }
.qs-chat-meta { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; font-size: 12px; color: var(--muted); }
.qs-chat-meta .btn-small { margin: 0; padding: 2px 8px; font-size: 12px; }
.qs-chat-user { margin: 4px 0 0 8px; padding: 6px 10px; background: var(--surface-2); border-radius: 8px; font-size: 13px; white-space: pre-wrap; }
.qs-chat-note { margin: 4px 0 0 24px; padding: 3px 10px; border-left: 3px solid var(--warn-br); font-size: 13px; color: var(--body-t); white-space: pre-wrap; }
.qs-chat-form { display: flex; gap: 8px; align-items: flex-end; margin-top: 10px; }
.qs-chat-form textarea { flex: 1; min-width: 0; min-height: 120px; font-family: inherit; font-size: 14px; line-height: 1.5; padding: 10px 12px; border: 1px solid var(--control-border); border-radius: 8px; background: var(--surface); color: var(--ink); resize: vertical; }
.qs-current { background: var(--ok-bg); color: var(--ok-t); }

/* --- /styleguide (SPEC_UI Step 1 — living component catalog) --- */
.sg-section { margin-bottom: 28px; }
/* A sample of the /updates counter row stands STILL: on its own page the row
   is sticky, and a specimen that follows the scroll would sit on top of the
   next specimen down. */
.sg-static .dash-bar { position: static; }
.sg-h {
  font-size: 12px; font-weight: 800; letter-spacing: .07em; text-transform: uppercase;
  color: var(--muted); margin: 0 0 10px;
}
.sg-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
/* A specimen shown at its working width instead of stretched across the
   page, and spaced from the specimen above it. The styleguide's own
   framing — nothing the components carry — so it lives in the `sg-`
   namespace rather than in an inline `style=` (D-379). */
.sg-narrow { max-width: 320px; }
.sg-narrow-s { max-width: 260px; }
.sg-spaced { margin-top: 14px; }
/* The navbar keeps its own dark palette in both themes, so its parts can only
   be judged against it — showing them on the page surface would prove nothing. */
.sg-navstage { display: flex; gap: 6px; align-items: center; padding: 10px 12px;
  background: var(--nav-bg); border: 1px solid var(--nav-border); border-radius: 12px; }
.sg-swatches { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 8px; }
.sg-sw { border: 1px solid var(--border); border-radius: 10px; overflow: hidden; background: var(--surface); }
.sg-sw-c { height: 44px; }
.sg-sw p { margin: 0; padding: 6px 9px; font-size: 11.5px; color: var(--muted); font-family: ui-monospace, monospace; }

/* --- Step 2: wide PC-first work layouts (SPEC_UI §3, preview v2) --- */
.alert-note { background: var(--neutral-bg); color: var(--neutral-t); border: 1px solid var(--border); }
.ab-spacer { margin-left: auto; }
/* Lists: the page stays centered; extra columns scroll INSIDE .table-scroll */
.content-list { max-width: var(--page-w); }
.table-scroll { overflow-x: auto; border-radius: 12px; }
/* Request card: 3 rails, everything useful on one Full-HD screen */
.content-detail { max-width: var(--page-w); padding-top: 0; }
.detail-actionbar {
  position: sticky; top: 56px; z-index: 40;
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 10px 0; margin-bottom: 14px;
  background: color-mix(in srgb, var(--bg) 94%, transparent);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--border);
}
/* Action buttons sit right after the badges (Aslan 18.07: closer to the
   left, smaller) — .ab-gap is a small fixed separator, NOT a full spacer. */
.ab-gap { width: 14px; }
.detail-actionbar .btn { font-size: 12.5px; padding: 6px 13px; margin-top: 0; }
.detail-actionbar h1 {
  margin: 0; font-family: ui-monospace, Consolas, monospace;
  font-size: 18px; font-weight: 800; letter-spacing: -0.01em;
}
.detail-actionbar .back-link { margin-left: 0; }
.detail-3col {
  display: grid;
  grid-template-columns: minmax(270px, 300px) minmax(480px, 1fr) minmax(295px, 330px);
  gap: 12px; align-items: start;
}
.detail-rail, .detail-center { display: flex; flex-direction: column; gap: 12px; min-width: 0; }
.detail-rail .card, .detail-center .card { margin-bottom: 0; }
/* Below ~1180px the three column minimums no longer fit — collapse straight
   to ONE column. A forced 2-col placed the right rail BELOW the tall center
   (review finding 19.07); 1366px laptops keep all three columns. */
@media (max-width: 1180px) {
  .detail-3col { grid-template-columns: 1fr; }
}
/* Secondary zone: technical blocks collapsed in place (variant A) */
.more-label {
  margin: 18px 0 8px; font-size: 11px; font-weight: 800;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--faint);
}
.more-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 12px; align-items: start; }
.more-grid .card { margin-bottom: 0; }
details.sec > summary {
  cursor: pointer; list-style: none;
  font-size: 15px; font-weight: 800; color: var(--ink);
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
}
details.sec > summary::-webkit-details-marker { display: none; }
details.sec > summary::before { content: '▸'; color: var(--accent); font-size: 12px; }
details.sec[open] > summary::before { content: '▾'; }
details.sec > .sec-body { margin-top: 10px; }

/* --- Review fixes 19.07 (SPEC_UI §1.8): native form elements follow the
   theme; refs get a real mono class; keyboard row links --- */
button, input, select, textarea { font: inherit; }
input, select, textarea { accent-color: var(--accent); }
select, textarea,
input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="submit"]) {
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--control-border); border-radius: 8px; padding: 6px 9px;
}
/* A read-only field (the feedback dialog's "Page" line) is a fact shown for
   confirmation, not something to fill in. It has to say so by LOOKING inert:
   inset surface, quiet border, muted ink, no text caret. Still selectable, so
   it can be copied.

   The four :not()s are not decoration — they are weight. The rule above scores
   four attribute selectors, so the old `.stack-form input[readonly]` (one
   class, one attribute) lost to it and the field kept the editable white
   background no matter where the rule was placed (browser check 2026-08-02).
   Matching its shape and adding [readonly] wins by one, everywhere, and the
   rule is no longer tied to one form class. */
input[readonly]:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="submit"]) {
  background: var(--surface-2); border-color: var(--border); color: var(--muted);
  cursor: default;
}
.card h3 { margin: 12px 0 6px; font-size: 14px; font-weight: 700; }
.ref { font-family: ui-monospace, "Cascadia Mono", Consolas, monospace; font-size: 0.95em; }
.rowlink-a { color: inherit; text-decoration: none; }
.rowlink-a:hover { color: var(--accent-ink); }

/* ============================== CALCULATORS ==============================
   The Calculators tab (SPEC_CALCULATORS §4, design wave W2a). Seven families,
   every one of them because the system has no recipe for the job — the
   justification for each is in `review_findings/calc_w2_design_report.md` §6,
   and specimens of all of them are on /styleguide.

   NOT ONE LITERAL COLOUR: every value below is a token (DESIGN_SYSTEM §2), so
   both themes follow without a second block. */

/* The derivation line under a charge or a field — «where this number came
   from». SPEC §3 asks every line to carry its provenance, and the system's
   own quiet captions are each bound to their own component
   (`.dash-tile .sub`, `.qs-agent-meta`, `.upd-when`); none exists inside
   `.data-table`. */
.calc-basis { font-size: 12px; line-height: 1.45; color: var(--muted); margin-top: 2px; }
.calc-basis .prov-badge { margin-left: 4px; }
.stack-form > div > .calc-basis, .stack-form > .calc-basis { margin-top: -6px; }

/* A refusal reason under a field. Small danger-coloured text exists three
   times in this file and each time it is scoped to its own block
   (`.trail-missing`, `.perf-worse`, `.dict-unknown`). */
.calc-bad { font-size: 12px; line-height: 1.45; color: var(--danger-t); font-weight: 700; margin-top: 4px; }

/* Money cells. `.data-table td` is left-aligned; money has to line up on the
   decimal, and `tabular-nums` is already inherited from the table. The only
   other `text-align: right` in this file is `.upd-time`. */
.calc-amt { text-align: right; white-space: nowrap; }
th.calc-amt { text-align: right; }
.calc-amt-cur { color: var(--muted); font-weight: 600; font-size: 12px; margin-left: 4px; }
.calc-none { color: var(--muted); }

/* Two- and four-field rows inside a 300px rail. `.mreq-grid`
   (auto-fill minmax(210px, 1fr)) collapses to one column there, and
   `.manage-grid` is a full-width panel recipe. */
.calc-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.calc-quad { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; }
.calc-quad label { font-size: 12px; }
.calc-opts { display: flex; flex-direction: column; gap: 7px; margin-top: 2px; }
/* A grid child keeps the browser's default input width and overflows a narrow
   rail unless it is told it may shrink. */
.calc-pair > label, .calc-quad > label { min-width: 0; }
.calc-pair input, .calc-pair select, .calc-quad input { width: 100%; min-width: 0; }

/* The state of the RATE behind the figures — three `.st-badge` variants on the
   existing semantic tokens, the way every feature adds its own (`st-qs-*`,
   `st-agent-*`). `st-gone` was not reused: it means «the row disappeared in
   ShipThis», and one concept must look the same everywhere. */
.st-rate-active  { background: var(--ok-bg);     color: var(--ok-t);     border-color: var(--ok-br); }
.st-rate-soon    { background: var(--warn-bg);   color: var(--warn-t);   border-color: var(--warn-br); }
.st-rate-expired { background: var(--danger-bg); color: var(--danger-t); border-color: var(--danger-br); }

/* The totals. The tiles themselves are `.dash-tile` unchanged; this only stops
   the pair from stretching across the whole centre column. */
.calc-totals { grid-template-columns: repeat(auto-fit, minmax(185px, 1fr));
               max-width: 760px; margin-top: 14px; }
.calc-subtotals { margin-top: 12px; }

/* Small utilities that keep `style=` out of the templates. */
.calc-group-label { display: block; font-size: 13px; color: var(--muted); margin-bottom: 4px; }
.calc-note { font-size: 13px; color: var(--muted); margin: 0 0 12px; }
.calc-recent { margin-top: 14px; }
.calc-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 14px; }
.calc-actions-center { justify-content: center; }
.calc-refusal .empty-icon { color: var(--danger-t); }
.calc-inline-fix { display: inline-flex; align-items: center; gap: 6px; margin-top: 6px; }
.calc-inline-fix .calc-group-label { margin-bottom: 0; }
.calc-inline-fix select { padding: 4px 8px; font-size: 12.5px; }
.calc-reorder-bar { margin: 0 0 12px; }
.calc-reorder-bar .calc-actions { margin-top: 8px; }

/* Rearranging the tool row (Aslan 30.08: the order is personal and set by
   dragging). Three parts the system has no recipe for, because it has no other
   draggable list: what to take hold of, what a plate looks like in the air,
   and the gap it left behind. */
.calc-grip {
  display: inline-block; margin-right: 6px; color: var(--faint);
  cursor: grab; font-size: 12px; letter-spacing: -1px; vertical-align: 1px;
}
.calc-chip-drag {
  box-shadow: var(--shadow-pop); border-color: var(--accent);
  transform: translateY(-2px); cursor: grabbing;
}
.calc-chip-slot {
  display: inline-block; width: 96px; height: 29px; border-radius: 999px;
  border: 1px dashed var(--control-border); background: var(--surface-2);
}
/* The move pair, on the plate. The BUTTON is `.col-move` — the column
   picker's own control, reused rather than a second one invented, so
   «this list can be rearranged» looks the same on both surfaces
   (DESIGN_SYSTEM §0: one concept, one recipe). Only the box around the pair
   is new, because here the pair sits inside a pill and not at the end of a
   table row. */
.calc-moves { display: inline-flex; gap: 2px; margin-left: 6px; }
.calc-moves .col-move { line-height: 1; }

/* --- The administration screen (Settings -> Tariffs & FSC, wave W3) ------
   Five small families, and every one of them exists because the same thing
   drawn with an existing recipe would say something else. NOT ONE LITERAL
   COLOUR, as above.

   `.calc-attention` — the «this runs out this week» list. Not `.kv-list`
   (which is a term/definition pair) and not a bare `<ul>` (whose bullets and
   default indent make a two-line entry unreadable next to a badge). */
.calc-attention { list-style: none; margin: 0; padding: 0;
                  display: flex; flex-direction: column; gap: 10px; }
.calc-attention li { padding-left: 2px; line-height: 1.5; }

/* Where a figure came from, in a table cell: a document name is long, dull and
   not what the eye is looking for. `.muted` is the page-level tone and would
   also shrink nothing; this keeps the column narrow and quiet. */
.calc-src { color: var(--muted); font-size: 12.5px; max-width: 320px; }

/* The «was -> became» comparison. The arrow is its own cell so the two amounts
   stay right-aligned on their decimals (`.calc-amt`), which is the whole point
   of showing them side by side. */
.calc-diff-arrow { color: var(--faint); text-align: center; width: 24px; }
.calc-diff-pct { font-weight: 700; }
/* A change over the anomaly threshold. `.calc-bad` is a BLOCK caption under a
   field; this is an inline value inside a money column, and the two cannot be
   the same rule. */
.calc-diff-alarm { font-weight: 700; color: var(--danger-t); }

/* The second confirmation of an unusual jump (Спека RC §8.5). A checkbox with
   its sentence beside it, big enough to be a deliberate act rather than
   something the eye slides past. */
/* `.stack-form label` is (0,1,1) and this was (0,1,0), so the form's own
   «one control per line» rule won and the checkbox sat ABOVE its sentence
   instead of beside it — a control that looks broken. Found in a screenshot;
   no snapshot or test can see it, because the markup is identical either way. */
.stack-form .calc-ack, label.calc-ack {
  display: flex; align-items: flex-start; gap: 8px;
  font-weight: 600; line-height: 1.5; margin: 4px 0 2px; }
.calc-ack input { margin-top: 3px; width: auto; flex: none; }

/* Six short columns stretched over the full page width put «0.9800» and
   «1.4500» a hand's breadth apart, with the arrow orphaned between them. A
   comparison is read by moving the eye a little, not a lot (DESIGN_SYSTEM §5a:
   width is for data — this table's data is three rows). */
.calc-diff-table { max-width: 900px; }
