/* runmetal design system — "Modernist".
 *
 * Tokens verified against the design project's authoritative styles.css
 * (_ds/modernist-72adf64b/…). This file is the single source of truth for the
 * look — every island and every template consumes it; retune here, never in a
 * component. One deliberate divergence: the design system @imports Archivo
 * from Google Fonts; brand rule 2 says self-host, so we ship subsetted
 * variable WOFF2s instead. No CDN, no external request.
 *
 * Square corners everywhere (--radius-* are 0 on purpose), Archivo at
 * 400/600/800, ONE accent red. The --color-accent-2-* ramp is a machine
 * stand-in the design system readme disowns ("treat them as one role") —
 * don't reach for it as a second accent.
 */

/* — Archivo (variable weight 400–800), self-hosted — */
@font-face {
  font-family: "Archivo";
  font-style: normal;
  font-weight: 400 800;
  font-stretch: 100%;
  font-display: swap;
  src: url("../fonts/archivo-latin.92895abae7c5.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: "Archivo";
  font-style: normal;
  font-weight: 400 800;
  font-stretch: 100%;
  font-display: swap;
  src: url("../fonts/archivo-latin-ext.288d2a1c9f04.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Archivo";
  font-style: normal;
  font-weight: 400 800;
  font-stretch: 100%;
  font-display: swap;
  src: url("../fonts/archivo-vietnamese.dbc5c2f39794.woff2") format("woff2");
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1,
    U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329,
    U+1EA0-1EF9, U+20AB;
}

:root {
  --color-bg: #f3f2f2;
  /* True white, only for surfaces that emulate paper (document/PDF frames) —
     never a page or module ground. */
  --color-paper: #ffffff;
  /* Text set on red, and the red it sits on (2026-10-12 audit). Off-white on
     the brand red measured 3.76:1, under AA's 4.5:1 at every button size;
     one step down with true white is 4.74:1 and reads as the same red. Every
     text-bearing red fill uses this pair. Graphic fills (radio dots, check
     marks, slider thumbs, the progress bar) keep --color-accent: non-text
     only needs 3:1. */
  --color-action: var(--color-accent-600);
  --color-on-action: #ffffff;
  --color-surface: #eae9e9;
  --color-text: #201e1d;
  --color-accent: #ec3013;
  --color-accent-2: #e15b47;
  --color-divider: color-mix(in srgb, #201e1d 40%, transparent);

  /* Tonal ramps — one shared lightness scale, so the same step of any role
     matches the others in visual value. */
  --color-neutral-100: #f8f4f4;
  --color-neutral-200: #eae7e7;
  --color-neutral-300: #d7d3d3;
  --color-neutral-400: #bab6b6;
  --color-neutral-500: #9b9797;
  --color-neutral-600: #7d7979;
  --color-neutral-700: #605d5d;
  --color-neutral-800: #444141;
  --color-neutral-900: #2d2b2b;

  --color-accent-100: #fff2ef;
  --color-accent-200: #ffe0d9;
  --color-accent-300: #ffc4b8;
  --color-accent-400: #ff9783;
  --color-accent-500: #ff563c;
  --color-accent-600: #dd2b0f;
  --color-accent-700: #ae1800;
  --color-accent-800: #7c1405;
  --color-accent-900: #4d170e;

  --color-accent-2-100: #fff2ef;
  --color-accent-2-200: #ffe0da;
  --color-accent-2-300: #ffc4b9;
  --color-accent-2-400: #ff9784;
  --color-accent-2-500: #ef6853;
  --color-accent-2-600: #c94b39;
  --color-accent-2-700: #9e3526;
  --color-accent-2-800: #71261b;
  --color-accent-2-900: #471d16;

  --font-heading: "Archivo", system-ui, sans-serif;
  --font-heading-weight: 800;
  --font-body: "Archivo", system-ui, sans-serif;

  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-6: 24px;
  --space-8: 32px;

  --radius-sm: 0px;
  --radius-md: 0px;
  --radius-lg: 0px;

  --shadow-sm: 0 1px 2px color-mix(in srgb, #2d2b2b 14%, transparent);
  --shadow-md: 0 3px 10px color-mix(in srgb, #2d2b2b 16%, transparent);
  --shadow-lg: 0 12px 32px color-mix(in srgb, #2d2b2b 22%, transparent);
}

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

/* The hidden attribute must always win. The UA's [hidden]{display:none} loses
   to any author display (`.btn{display:inline-flex}` kept a hidden Compare
   button on screen, reading "Compare 0 responses" over its own empty state). */
[hidden] {
  display: none !important;
}

body {
  margin: 0;
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-body);
  font-size: 15px;
  line-height: 1.55;
  font-weight: 400;
}

h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-heading);
  font-weight: var(--font-heading-weight);
  line-height: 1.12;
  letter-spacing: -0.015em;
  margin: 0 0 var(--space-2);
}
h1 { font-size: 42px; }
h2 { font-size: 32px; }
h3 { font-size: 25px; }
h4 { font-size: 20px; }
h5 { font-size: 16px; }
h6 { font-size: 13px; letter-spacing: 0.08em; text-transform: uppercase; }

p { margin: 0 0 var(--space-3); }
/* Link text takes accent-700 (2026-10-12): the brand red is 3.76:1 on the
   page, a fail at body size; the kickers made the same move in ADR 0030. */
a { color: var(--color-accent-700); text-underline-offset: 3px; }
a:hover { color: var(--color-accent-800); }
img { display: block; max-width: 100%; }

input[type="number"] { -moz-appearance: textfield; }
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

/* Muted ink is neutral-700 — the 55% mix it used to be computed to ~3.6:1
   on the page ground, under AA for the 11–13px it is always set at. */
.text-muted { color: var(--color-neutral-700); }
/* Screen-reader-only text — names a control the visual layout carries by
   position (the vendor-picker checkboxes, table select columns). */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
.tabular { font-variant-numeric: tabular-nums; }

:focus { outline: none; }
:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
::selection { background: color-mix(in srgb, var(--color-accent) 30%, transparent); }

.hr {
  height: 2px;
  border: 0;
  margin: var(--space-4) 0;
  background: var(--color-divider);
}

/* — buttons — */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  cursor: pointer;
  text-decoration: none;
  font-family: var(--font-heading);
  font-weight: var(--font-heading-weight);
  font-size: 14px;
  line-height: 1.2;
  color: var(--color-text);
  background: transparent;
  border: 1px solid transparent;
  padding: var(--space-2) calc(var(--space-3) * 1.2);
  border-radius: var(--radius-md);
}
.btn svg { display: block; }
.btn:disabled { opacity: 0.45; cursor: not-allowed; }
/* Faded red under faded white text bottomed out near 2:1 — a disabled
   label still has to be readable (2026-08-27 audit). Neutral fill, full-
   opacity muted ink. */
.btn-primary:disabled {
  opacity: 1;
  background: var(--color-neutral-300);
  color: var(--color-neutral-700);
}
.btn-primary { background: var(--color-action); color: var(--color-on-action); }
.btn-primary:hover:not(:disabled) { background: var(--color-accent-700); }
.btn-primary:active:not(:disabled) { background: var(--color-accent-800); }
/* Every button reads as a button: a filled ground and a visible edge, never
   a bare word floating in the page. */
.btn-secondary {
  background: var(--color-surface);
  border-color: var(--color-text);
  box-shadow: 2px 2px 0 color-mix(in srgb, var(--color-text) 14%, transparent);
}
.btn-secondary:hover { background: color-mix(in srgb, var(--color-text) 7%, var(--color-surface)); }
.btn-secondary:active {
  background: color-mix(in srgb, var(--color-text) 14%, var(--color-surface));
  box-shadow: none;
  transform: translate(1px, 1px);
}
/* Ghost is the quiet action — still reads as a button (a ground and an
   edge), but in ink, not accent. It was an accent-100 chip with a red border,
   which put red on every Delete, Remove, Note and Refresh in every table row:
   red stopped meaning "the action on this screen" (§8 rule 3) and started
   meaning "a button". Neutral-600 is the same edge the inputs wear (3.94:1). */
.btn-ghost {
  color: var(--color-text);
  background: transparent;
  border-color: var(--color-neutral-600);
}
.btn-ghost:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }
.btn-ghost:active {
  background: color-mix(in srgb, var(--color-text) 14%, transparent);
  transform: translate(1px, 1px);
}
.btn-sm { font-size: 12px; padding: 5px 10px; }
.btn-icon { width: 36px; height: 36px; padding: 0; }
/* Row-height icon-only button: pairs with .btn-sm rows. The label moves to
   aria-label + title — never ship one without both. */
.btn-icon-sm { width: 28px; height: 28px; padding: 0; vertical-align: middle; }
/* Modernist sets labels flush left — even inside a full-width button. */
.btn-block {
  width: 100%;
  margin-top: var(--space-2);
  justify-content: flex-start;
  text-align: left;
}
/* The mock's signature: a chamfered top-right corner on forward actions. */
.btn-notch { clip-path: polygon(0 0, calc(100% - 10px) 0, 100% 10px, 100% 100%, 0 100%); }
.btn-notch-sm { clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 8px, 100% 100%, 0 100%); }

/* ── review screen: raw email beside the entry form ────────────────────────
 * The table's inputs used to overflow their module because the cells had no
 * declared widths; fixed layout + full-width inputs keep everything inside
 * the 2px edge at any zoom.
 */
.review-grid {
  display: grid;
  grid-template-columns: minmax(320px, 0.85fr) minmax(420px, 1.15fr);
  gap: var(--space-6);
  align-items: start;
}
@media (max-width: 1000px) { .review-grid { grid-template-columns: 1fr; } }

.section-label {
  text-transform: lowercase;
  margin: 0 0 var(--space-3);
}
.module {
  border: 2px solid var(--color-text);
  background: var(--color-surface);
}
.module-pad { padding: var(--space-4); }
.module-head {
  padding: 8px 12px;
  border-bottom: 1px solid var(--color-divider);
  font-size: 11.5px;
  color: var(--color-neutral-700);
}
.module-foot {
  padding: 10px 12px;
  border-top: 1px solid var(--color-divider);
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}
.raw-email {
  margin: 0;
  padding: 12px;
  font-size: 12.5px;
  line-height: 1.6;
  white-space: pre-wrap;
  font-family: var(--font-body);
  max-height: 380px;
  overflow-y: auto;
}
.entry-table { table-layout: fixed; min-width: 520px; }
.entry-table th,
.entry-table td { padding: 6px 8px; vertical-align: middle; }
.entry-table .col-check { width: 62px; text-align: center; }
.entry-table .col-money { width: 96px; }
.entry-table .col-note { width: 130px; }
.entry-table .input { width: 100%; min-width: 0; font-size: 12.5px; padding: 5px 7px; }
.entry-table input:not([type="checkbox"]) { width: 100%; min-width: 0; }
.line-name { font-size: 12.5px; font-weight: 600; line-height: 1.3; }
.line-meta { font-size: 11px; }
/* Per-line prices are retroactive work — a shop comparing on totals never
   opens this, so it sits closed inside the module as a row of its own rather
   than standing between the email and the figures it came for. Takes the
   suggest-panel's +/- marker; only the frame differs. */
.line-detail > summary {
  padding: 10px var(--space-4);
  border-bottom: 1px solid var(--color-divider);
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  justify-content: space-between;
  display: flex;
}
.line-detail[open] > summary { border-bottom: 2px solid var(--color-text); }
.check-center { justify-content: center; }
.field-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-3);
  margin-top: var(--space-4);
}
.field-grid .input,
.module-pad textarea.input { width: 100%; }
.action-row {
  display: flex;
  gap: 10px;
  align-items: center;
  margin: var(--space-4) 0 var(--space-3);
}

/* — forms — */
.field > label,
.field > legend {
  display: block;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.01em;
  margin-bottom: 7px;
  color: color-mix(in srgb, var(--color-text) 78%, transparent);
}
/* A checkbox group is one question: fieldset + legend name the group for AT
   where a bare label would name nothing. Reset the UA chrome. */
fieldset.field { border: 0; padding: 0; margin: 0; min-width: 0; }
fieldset.field > legend { padding: 0; }
.input,
select.input {
  width: 100%;
  min-height: 36px;
  padding: 6px 10px;
  font: inherit;
  font-size: 14px;
  color: var(--color-text);
  caret-color: var(--color-accent);
  /* A light well with a defined writing line — surface-on-bg put three
     grays within a few percent of each other and the form read as murk
     (2026-08-27 review). */
  background: var(--color-neutral-100);
  border: 1px solid var(--color-neutral-600);
  border-bottom-color: var(--color-neutral-600);
  border-radius: var(--radius-md);
  transition: border-color 0.15s ease;
}
.input:hover:not(:focus-visible) {
  border-color: color-mix(in srgb, var(--color-text) 45%, transparent);
}
.input:hover { border-color: color-mix(in srgb, var(--color-text) 45%, transparent); }
.input:focus-visible { border-color: var(--color-accent); outline-offset: 0; }
.input[aria-invalid="true"] { border-color: var(--color-accent); }
textarea.input { min-height: 90px; resize: vertical; }
/* Repaint the browser's yellow autofill with the field's own ground. */
.input:-webkit-autofill,
.input:-webkit-autofill:hover,
.input:-webkit-autofill:focus {
  -webkit-box-shadow: 0 0 0 1000px var(--color-surface) inset;
  -webkit-text-fill-color: var(--color-text);
  caret-color: var(--color-accent);
}
/* Selects wear the design, not the OS: kill native chrome (Safari's rounded
   gradient breaks brand rule 1) and draw our own Lucide chevron, stroke 2.
   The stroke hex is --color-text — a data URI can't read the token. */
select.input,
select.cell-input {
  appearance: none;
  -webkit-appearance: none;
  padding-right: 28px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23201e1d' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 8px center;
  background-size: 13px;
}
select.input::-ms-expand,
select.cell-input::-ms-expand { display: none; }
/* Number inputs: no spinners, no Safari height quirk — they sit flush with
   their text siblings. Numeric entry UX comes from inputMode, not type. */
input.input[type="number"],
input.cell-input[type="number"] { appearance: textfield; -moz-appearance: textfield; }
input.input[type="number"]::-webkit-outer-spin-button,
input.input[type="number"]::-webkit-inner-spin-button,
input.cell-input[type="number"]::-webkit-outer-spin-button,
input.cell-input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

.seg {
  display: inline-flex;
  overflow: hidden;
  border: 1px solid var(--color-divider);
  border-radius: var(--radius-md);
}
.seg-opt {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 12px;
  font-size: 13px;
  cursor: pointer;
}
.seg-opt input { position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none; }
.seg-opt + .seg-opt { border-left: 1px solid var(--color-divider); }
.seg-opt:has(input:checked) { background: var(--color-action); color: var(--color-on-action); }
.seg-opt:not(:has(input:checked)):hover {
  background: color-mix(in srgb, var(--color-text) 7%, transparent);
}
.seg-opt:has(input:focus-visible) { outline: 2px solid var(--color-accent); outline-offset: -2px; }

/* — cards — */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: var(--space-3);
}
.card {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-3);
  border-radius: var(--radius-md);
  background: var(--color-surface);
}
.card-kicker {
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  /* accent-700, not accent: at 10px this is body copy, and plain accent is
     3.47:1 on --color-surface. WCAG's large-text exemption starts at 24px
     (18.66px bold), so it never covered these (audit F10, ADR 0030). */
  color: var(--color-accent-700);
}
.card-title {
  font-family: var(--font-heading);
  font-weight: var(--font-heading-weight);
  font-size: 17px;
  line-height: 1.2;
}
/* Explicit inks, not opacity — an opacity here compounds with any muted
   child (card-meta inside card-body measured ~2.4:1 before this). */
.card-body { font-size: 13px; color: var(--color-neutral-800); flex: 1; }
.card-meta { font-size: 11px; color: var(--color-neutral-700); }
.elev-sm { box-shadow: var(--shadow-sm); }
.elev-md { box-shadow: var(--shadow-md); }
.elev-lg { box-shadow: var(--shadow-lg); }

/* — tags — */
.tag {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  /* A tag is one line, always — wrapping mid-label reads as broken UI. */
  white-space: nowrap;
  font-size: 11px;
  letter-spacing: 0.02em;
  padding: 3px 10px;
  border-radius: calc(var(--radius-md) * 0.75);
}
.tag-accent { background: var(--color-accent-100); color: var(--color-accent-800); }
/* No .tag-accent-2 — the accent-2 ramp is a machine stand-in the design
   system disowns (brand rule 7); a second accent role must not exist. */
.tag-neutral { background: var(--color-neutral-100); color: var(--color-neutral-800); }
.tag-outline { border: 1px solid var(--color-accent); color: var(--color-accent-700); }
/* Provenance, not status (ADR 0039) — it sits beside a status tag and must
   not compete with it. Neutral hairline: not accent, because red is action
   and emphasis only (brand rule 3) and a supplier runmetal added is neither
   a problem nor a thing to do. Measured, not assumed (ADR 0030's lesson):
   neutral-700 text on surface is 5.38:1, and the hairline is neutral-600
   (3.55:1) — the same token the form boundaries moved to. */
.tag-quiet { border: 1px solid var(--color-neutral-600); color: var(--color-neutral-700); }

/* — radios — */
.radio {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  font-size: 13px;
}
.radio input { position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none; }
.radio .dot {
  width: 16px;
  height: 16px;
  /* Square, like everything else (brand rule 1) — the inset fill below is
     the checked marker, so the circle earns nothing. */
  border-radius: 0;
  border: 1.5px solid var(--color-divider);
  flex: none;
}
.radio:has(input:checked) .dot {
  background: var(--color-accent);
  border-color: var(--color-accent);
  box-shadow: inset 0 0 0 4px var(--color-bg);
}
.radio:has(input:focus-visible) .dot { outline: 2px solid var(--color-accent); outline-offset: 2px; }

/* — dialog (the manual-review modal) — */
.dialog-backdrop {
  position: fixed;
  inset: 0;
  z-index: 70;
  display: grid;
  place-items: center;
  padding: var(--space-4);
  background: color-mix(in srgb, var(--color-neutral-900) 50%, transparent);
}
.dialog {
  width: min(440px, 100%);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-4);
  background: var(--color-surface);
  border: 2px solid var(--color-text);
  box-shadow: var(--shadow-lg);
}
.dialog-title {
  font-family: var(--font-heading);
  font-weight: var(--font-heading-weight);
  font-size: 20px;
}
.dialog-body { font-size: 14px; opacity: 0.85; }
.dialog-actions { display: flex; justify-content: flex-end; gap: var(--space-2); }

/* — selection ring — the design's one selection affordance. Absolutely
   positioned inside a position:relative parent; for sort segments, filter
   chips, flagged rows and the winning compare card, so nobody invents a
   second idiom per screen. Not for inputs — a missing/invalid field is
   `.input[aria-invalid="true"]`, never a ring on top of a border. */
.ring-selected {
  position: absolute;
  inset: 0;
  border: 2px solid var(--color-accent);
  pointer-events: none;
}

/* — empty states — §9: the waiting state is the common case; make it
   informative, never blank. Pair with an icon and real copy. */
.empty {
  border: 1px dashed var(--color-neutral-500);
  padding: 56px 24px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  text-align: center;
}
.empty h2,
.empty h3 {
  font-size: 20px;
  text-transform: lowercase;
  margin: 0;
}
.empty p { max-width: 44ch; color: var(--color-neutral-700); font-size: 13px; margin: 0; }

/* Photography prints in pure black and white. */
.grayscale { filter: grayscale(1) contrast(1.08); }

/* — navigation — */
.nav {
  display: flex;
  align-items: center;
  gap: 22px;
  padding: 0 26px;
  height: 60px;
  position: sticky;
  top: 0;
  z-index: 40;
  background: var(--color-bg);
  border-bottom: 2px solid var(--color-divider);
}
.nav-brand {
  display: flex;
  align-items: center;
  margin-right: 8px;
  text-decoration: none;
}
/* The lockup is one asset at a fixed 139 x 28 — symbol 28, gap 14, wordmark 22
   outlined. Height drives it; width follows so the ratio can't be squashed. */
.nav-brand img {
  height: 28px;
  width: auto;
  display: block;
}
.nav-links { display: flex; align-items: center; gap: 26px; margin-right: auto; }
.nav-brand,
.nav-avatar,
.nav form { flex: none; }
.nav a { color: inherit; text-decoration: none; font-size: 13px; font-weight: 600; }
.nav a:hover,
.nav a[aria-current="page"] { color: var(--color-accent-700); }
.nav-avatar {
  width: 30px;
  height: 30px;
  background: var(--color-text);
  color: var(--color-bg);
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-heading);
  font-weight: 800;
  font-size: 13px;
}

/* — account menu: the avatar discloses Settings / Sign out — */
.nav-account { position: relative; }
.nav-account > summary {
  list-style: none;
  cursor: pointer;
}
.nav-account > summary::-webkit-details-marker { display: none; }
.nav-account > summary:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}
.nav-account[open] > summary { background: var(--color-accent); }
.nav-menu {
  position: absolute;
  right: 0;
  top: calc(100% + 10px);
  min-width: 210px;
  z-index: 50;
  border: 2px solid var(--color-text);
  background: var(--color-bg);
  box-shadow: var(--shadow-md);
  animation: nav-menu-in 120ms ease-out;
}
@keyframes nav-menu-in {
  from { opacity: 0; transform: translateY(-4px); }
  to { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .nav-menu { animation: none; }
}
.nav-menu-who {
  padding: 10px 14px;
  font-size: 11.5px;
  color: color-mix(in srgb, var(--color-text) 55%, transparent);
  border-bottom: 1px solid var(--color-divider);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.nav-menu-item,
.nav .nav-menu-item {
  display: block;
  width: 100%;
  padding: 10px 14px;
  border: 0;
  background: none;
  text-align: left;
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  color: var(--color-text);
  text-decoration: none;
  cursor: pointer;
}
.nav-menu-item:hover,
.nav .nav-menu-item:hover {
  background: var(--color-surface);
  color: var(--color-accent-700);
}

/* — tables — */
.table { width: 100%; border-collapse: collapse; font-size: 14px; }
.table th {
  text-align: left;
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--color-text) 60%, transparent);
  padding: var(--space-2);
  border-bottom: 2px solid var(--color-divider);
}
.table td { padding: var(--space-2); border-bottom: 1px solid var(--color-divider); }
.table tbody tr:hover { background: color-mix(in srgb, var(--color-text) 4%, transparent); }

/* Wide content scrolls inside its own box; the page body never scrolls sideways. */
.table-scroll { overflow-x: auto; }

/* — page scaffolding — */
/* The layout spec's page frame (docs/design/runmetal-rfq-layout.md §1.2):
   documents read at 1180; workbenches — material list, choose vendors,
   compare, mail — get 1320. 26px gutters, 34px head, 90px deep floor. */
.page { max-width: 1180px; margin: 0 auto; padding: 34px 26px 90px; }
.page-wide { max-width: 1320px; }

@media (max-width: 720px) {
  .page { padding: 28px var(--space-4) 60px; }
  .page-title { font-size: 26px; }
  /* The nav has more in it than fits a phone: let it wrap onto a second line
     instead of pushing the avatar off the right edge of the document. */
  .nav {
    gap: 12px 16px;
    padding: 10px var(--space-4);
    height: auto;
    flex-wrap: wrap;
  }
  /* The links wrap rather than run 4px past the screen edge (2026-10-13). */
  .nav-links { gap: 8px 16px; order: 3; width: 100%; flex-wrap: wrap; }
  .nav-brand { margin-right: auto; }
  /* Where the avatar lands moves with the wrap (left of the second row on a
     phone, right of the first a little wider), so the menu anchors to the
     nav bar itself — under it, at its right gutter — instead of to the
     avatar, which opened it 164px off-screen at 390. The sticky nav is the
     containing block. */
  .nav-account { position: static; }
  .nav-menu { top: 100%; right: var(--space-4); }
}
.page-kicker {
  font-size: 11px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  /* See .card-kicker — 11px is not "large text" either. */
  color: var(--color-accent-700);
  font-weight: 800;
  margin-bottom: 11px;
}
.page-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-6);
  flex-wrap: wrap;
  /* Every screen needs air under the heading block. Older screens faked it
     with an <hr>; without this the first module collides with the CTA. */
  margin-bottom: var(--space-6);
}
.page-head + .hr { margin-top: 0; }
/* Two or three actions in a page head: one column, right-aligned, so a
   second button never has to be parked under the header and collide with
   whatever block comes next (the RFQ screen's stat band, 2026-09-04). */
.page-actions {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--space-2);
}
/* The layout spec's header block: 34px lowercase H1, 13.5px/1.6 sub. */
.page-title {
  font-size: 34px;
  letter-spacing: -0.025em;
  text-transform: lowercase;
  margin: 0 0 8px;
}
.page-sub {
  font-size: 13.5px;
  line-height: 1.6;
  color: var(--color-neutral-700);
  max-width: 68ch;
}

.notice {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0 0 var(--space-4);
  font-size: 13px;
  max-width: 70ch;
}
/* A notice that carries its one fix (confirm your email): the button keeps
   its label on one line and the sentence takes the wrap. */
.notice form { flex: none; margin-left: auto; }
.notice form .btn { white-space: nowrap; }
/* Brand rule 4: raw accent on the page ground computes to 3.76:1 — fine for
   large or bold type, not 13px body copy. (The brief said ~4.3:1 until
   2026-08-24; the shipped tokens say otherwise.) Warnings follow the
   design's banner idiom instead: deep accent ink on the accent-100 tint,
   which measures 6.55:1, with room for an icon (rule 5: never colour
   alone). */
.notice-warn {
  color: var(--color-accent-700);
  background: var(--color-accent-100);
  border: 1px solid var(--color-accent);
  padding: var(--space-2) var(--space-3);
  font-weight: 600;
}
.notice-ok {
  color: var(--color-neutral-700);
  border: 1px solid var(--color-divider);
  padding: var(--space-2) var(--space-3);
}

/* ── account pages ────────────────────────────────────────────────────────────
 * The entrance layout: the product's pitch on the left, the form on the right.
 * allauth renders most screens from its own templates, so form controls are
 * styled by element rather than by class. Scoped to .auth-card so nothing here
 * can leak into the app's own screens.
 */
.entrance {
  display: grid;
  grid-template-columns: 1.05fr 0.95fr;
  min-height: calc(100vh - 60px); /* the sticky nav */
  min-height: calc(100dvh - 60px); /* dvh where supported: no iOS toolbar jump */
}
.entrance-brand {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding: 72px 56px 56px;
  background: var(--color-surface);
  border-right: 2px solid var(--color-divider);
}
.entrance-brand img { width: 64px; height: 64px; display: block; margin-bottom: var(--space-2); }
.entrance-brand h2 {
  font-size: 34px;
  letter-spacing: -0.025em;
  text-transform: lowercase;
  line-height: 1.12;
  max-width: 15ch;
  margin: 0;
}
.entrance-brand p {
  max-width: 44ch;
  font-size: 14px;
  color: var(--color-neutral-700);
  margin: 0;
}
.entrance-panel {
  display: flex;
  /* Top-aligned, not centred: the card's top edge meets the mark across the
     rule. Centring floated a short card (password reset is three elements)
     halfway down a column whose neighbour starts at the top. */
  align-items: flex-start;
  justify-content: center;
  padding: 72px var(--space-6) 56px;
}
.entrance-panel .auth-card { width: min(440px, 100%); }
@media (max-width: 880px) {
  .entrance { grid-template-columns: 1fr; min-height: 0; }
  /* The nav already carries the brand; the pitch panel is a desktop luxury. */
  .entrance-brand { display: none; }
  .entrance-panel { padding: 28px var(--space-4) 60px; align-items: flex-start; }
}

.auth-card {
  background: var(--color-surface);
  border: 2px solid var(--color-text);
  padding: var(--space-8) var(--space-6);
}
.check,
.auth-card .auth-check {
  display: flex;
  align-items: center;
  gap: 9px;
  font-size: 13px;
  color: var(--color-text);
  cursor: pointer;
}
.auth-card .auth-check { margin-bottom: var(--space-3); }
/* A square checkbox — the platform default renders round on macOS and reads
   as a radio. Zero radius, accent when checked, check drawn as a clipped L.
   `.check` is the shared primitive (§8: primitives exist once); .auth-check
   keeps its name for the entrance card. */
.check input,
.auth-card .auth-check input {
  appearance: none;
  -webkit-appearance: none;
  width: 16px;
  height: 16px;
  min-height: 0;
  margin: 0;
  flex: none;
  display: inline-grid;
  place-content: center;
  border: 1.5px solid var(--color-divider);
  background: var(--color-bg);
  border-radius: 0;
  cursor: pointer;
}
.check input:checked,
.auth-card .auth-check input:checked {
  background: var(--color-accent);
  border-color: var(--color-accent);
}
.check input:checked::before,
.auth-card .auth-check input:checked::before {
  content: "";
  width: 9px;
  height: 5px;
  border-left: 2px solid var(--color-bg);
  border-bottom: 2px solid var(--color-bg);
  transform: translateY(-1.5px) rotate(-45deg);
}
.check input:focus-visible,
.auth-card .auth-check input:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
  border-color: var(--color-accent);
}
.auth-card h1 {
  font-size: 28px;
  text-transform: lowercase;
  margin-bottom: var(--space-2);
}
.auth-card h2 { font-size: 20px; }
.auth-card p { font-size: 14px; }
.auth-card > p:first-of-type { color: color-mix(in srgb, var(--color-text) 65%, transparent); }

.auth-card label {
  display: block;
  font-size: 12px;
  margin-bottom: 5px;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}
.auth-card input:not([type="checkbox"]):not([type="radio"]):not([type="submit"]):not([type="button"]),
.auth-card select,
.auth-card textarea {
  width: 100%;
  min-height: 36px;
  padding: 6px 10px;
  font: inherit;
  font-size: 14px;
  color: var(--color-text);
  caret-color: var(--color-accent);
  background: var(--color-bg);
  border: 1px solid var(--color-divider);
  border-radius: var(--radius-md);
}
.auth-card input:focus-visible,
.auth-card select:focus-visible,
.auth-card textarea:focus-visible {
  border-color: var(--color-accent);
  outline-offset: 0;
}
.auth-card input[type="checkbox"] { accent-color: var(--color-accent); }

/* The browser's autofill paint (that yellow) fights the system — repaint it
   with our own ground. The 1000px inset shadow is the standard override. */
.auth-card input:-webkit-autofill,
.auth-card input:-webkit-autofill:hover,
.auth-card input:-webkit-autofill:focus {
  -webkit-box-shadow: 0 0 0 1000px var(--color-bg) inset;
  -webkit-text-fill-color: var(--color-text);
  caret-color: var(--color-accent);
}

/* Entrance CTAs are block buttons: full width, label flush left — the
   readme is explicit that labels sit flush left even inside buttons. */
/* `:not(.btn)` — a button carrying the system's own .btn classes has already
   said what it is. allauth's secondary actions ("Cancel", "Request new
   code") come through as plain <button>s and were painted accent by this
   rule, so Cancel read exactly like Confirm; the button element override
   hands them .btn-secondary and this leaves it alone. */
.auth-card button:not(.btn),
.auth-card input[type="submit"],
.auth-card a.button {
  display: flex;
  width: 100%;
  align-items: center;
  justify-content: flex-start;
  text-align: left;
  gap: 6px;
  cursor: pointer;
  text-decoration: none;
  font-family: var(--font-heading);
  font-weight: var(--font-heading-weight);
  font-size: 14px;
  line-height: 1.2;
  margin-top: var(--space-2);
  padding: 11px 16px;
  border-radius: var(--radius-md);
  border: 1px solid transparent;
  background: var(--color-action);
  color: var(--color-on-action);
  clip-path: polygon(0 0, calc(100% - 10px) 0, 100% 10px, 100% 100%, 0 100%);
}
.auth-card button:not(.btn):hover,
.auth-card input[type="submit"]:hover { background: var(--color-accent-700); }
.auth-card button:not(.btn):active,
.auth-card input[type="submit"]:active { background: var(--color-accent-800); }
/* Secondary and link-style actions allauth marks with its own tags. */
.auth-card a:not(.button) {
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 13px;
}

/* Django renders field errors as a <ul class="errorlist"> above the input —
   on every form, not just the entrance card (it shipped scoped to .auth-card
   and the app forms regressed to browser bullets when they stopped borrowing
   that class, 2026-08-26). Accent-700 ink: small text, §8 rule 4. */
.errorlist {
  list-style: none;
  margin: 0 0 var(--space-2);
  padding: 0;
  font-size: 12px;
  font-weight: 600;
  color: var(--color-accent-700);
}
/* Under its input (the app forms render them there so grid rows stay
   aligned), the message hugs the field it corrects. */
.field .errorlist { margin: 5px 0 0; }
.auth-card form > p,
.auth-card .field { margin-bottom: var(--space-3); }
/* allauth's stock screens hang their trailing prose and secondary link off
   the form as siblings, where the button's own margin-top can't reach them:
   "Please contact us…" sat flush against the CTA's clipped corner. */
.auth-card form + p,
.auth-card form + a { margin-top: var(--space-4); }
/* Inside a module, rules are 1px — 2px is for module edges. */
.auth-card .hr { height: 1px; margin: var(--space-6) 0 var(--space-4); }

@keyframes rm-fade {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: none; }
}
.rm-screen { animation: rm-fade 0.28s ease both; }

/* A reply row landing in the inbox: drops in with an accent flash that decays. */
@keyframes rm-land {
  0% { opacity: 0; transform: translateY(-6px); background: var(--color-accent-100); }
  60% { background: var(--color-accent-100); }
  100% { opacity: 1; transform: none; background: transparent; }
}
.rm-land { animation: rm-land 0.5s ease both; }

/* The waiting dots on "sent — nothing back yet". */
@keyframes rm-pulse {
  0%, 100% { opacity: 0.25; }
  50% { opacity: 1; }
}
.rm-pulse { animation: rm-pulse 1.2s ease-in-out infinite; }

@keyframes rm-bar {
  0% { transform: translateX(-100%); }
  100% { transform: translateX(240%); }
}
.rm-progress {
  width: 190px;
  height: 3px;
  background: var(--color-neutral-300);
  overflow: hidden;
}
.rm-progress > div {
  height: 100%;
  width: 38%;
  background: var(--color-accent);
  animation: rm-bar 1.05s ease-in-out infinite;
}

/* ── the loading screen ───────────────────────────────────────────────────────
 * The design's signature: an end mill machines the runmetal lockup — the
 * cutter drops down the r's stem, traverses the arm, then wipes right to
 * reveal the wordmark, throwing chips as it cuts. Shown only over genuine
 * async work (fetching a list, sending an RFQ) — never on navigation.
 * Loops every 1.4s; markup lives in frontend/src/feedback.jsx, mirrored on
 * the staff /brand/ sheet (drift-tested in tests/test_brand.py).
 */
.rm-loader {
  position: fixed;
  inset: 0;
  z-index: 80;
  background: var(--color-bg);
  display: flex;
  align-items: center;
  justify-content: center;
  animation: rm-fade 0.2s ease both;
}
.rm-loader-inner {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 22px;
  width: min(380px, 72vw);
}
.rm-loader-label {
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--color-neutral-700);
  font-weight: 700;
}
.rm-loader .rm-progress { width: 100%; }

.rm-billet { fill: var(--color-accent); }
.rm-glyph { fill: var(--color-bg); } /* light knock-out so it reads on red */
.rm-word { fill: var(--color-text); }
.rm-chip { fill: var(--color-neutral-500); }
/* The machine tool's own colours — the design's literals, held here so the
   loader and the brand sheet paint from one place (brand rule: no hex in
   components). Not palette values: an end mill is steel, not ink. */
.rm-tool-body { fill: #17100e; }
.rm-flute { fill: #2e2422; }
.rm-tool-center { fill: #8a7f7b; }
.rm-tool-ring { fill: none; stroke: #c9bdb8; stroke-width: 0.9; opacity: 0.55; }
.rm-stem { animation: rm-cut-stem 1.4s linear infinite; }
.rm-arm { animation: rm-cut-arm 1.4s linear infinite; }
.rm-wipe { animation: rm-cut-wipe 1.4s linear infinite; }
.rm-tool { animation: rm-cut-tool 1.4s linear infinite; }
.rm-flutes {
  transform-box: fill-box;
  transform-origin: center;
  animation: rm-spin 0.45s linear infinite;
}
.rm-chips .rm-chip { animation: rm-chip 0.5s linear infinite; }
.rm-chips .rm-chip:nth-child(2) { animation-delay: 0.1s; }
.rm-chips .rm-chip:nth-child(3) { animation-delay: 0.2s; }
.rm-chips .rm-chip:nth-child(4) { animation-delay: 0.3s; }
.rm-chips .rm-chip:nth-child(5) { animation-delay: 0.4s; }

@keyframes rm-cut-stem { 0%, 4% { height: 0px; } 26%, 100% { height: 52px; } }
@keyframes rm-cut-arm { 0%, 26% { width: 0px; } 48%, 100% { width: 32px; } }
@keyframes rm-cut-wipe { 0%, 52% { width: 0px; } 88%, 100% { width: 367px; } }
@keyframes rm-spin { to { transform: rotate(360deg); } }
@keyframes rm-chip {
  0% { opacity: 0; transform: translate(0, 0); }
  12% { opacity: 0.9; }
  100% { opacity: 0; transform: translate(var(--cx), var(--cy)); }
}
@keyframes rm-cut-tool {
  0% { transform: translate(34px, 4px); opacity: 0; }
  4% { transform: translate(34px, 20px); opacity: 1; }
  26% { transform: translate(34px, 86px); opacity: 1; }
  29% { transform: translate(34px, 86px); opacity: 0; }
  30% { transform: translate(46px, 34px); opacity: 0; }
  33% { transform: translate(50px, 34px); opacity: 1; }
  48% { transform: translate(84px, 34px); opacity: 1; }
  51% { transform: translate(84px, 34px); opacity: 0; }
  52% { transform: translate(140px, 50px); opacity: 0; }
  55% { transform: translate(150px, 50px); opacity: 1; }
  88% { transform: translate(505px, 50px); opacity: 1; }
  92%, 100% { transform: translate(505px, 50px); opacity: 0; }
}

/* ── toast ── the design's bottom-center confirmation, auto-dismissed. */
.rm-toast {
  position: fixed;
  left: 50%;
  bottom: 26px;
  transform: translateX(-50%);
  z-index: 60;
  background: var(--color-text);
  color: var(--color-bg);
  padding: 12px 20px;
  font-size: 12.5px;
  font-weight: 600;
  animation: rm-fade 0.2s ease both;
}

@media (prefers-reduced-motion: reduce) {
  .rm-screen,
  .rm-land,
  .rm-pulse,
  .rm-toast,
  .rm-loader,
  .rm-progress > div,
  .rm-flutes,
  .rm-chips .rm-chip { animation: none; }
  /* With the cut animations off, show the finished mark, not a blank billet. */
  .rm-stem { animation: none; height: 52px; }
  .rm-arm { animation: none; width: 32px; }
  .rm-wipe { animation: none; width: 367px; }
  .rm-tool { display: none; }
}

/* ═════════════════════════════════════════════════════════════════════════
 * Screen components — the delivered prototype's layouts, built once here so
 * every screen inherits the same treatment (§8: primitives exist once).
 * ═════════════════════════════════════════════════════════════════════════ */

/* — stat tiles: the four-across strip above an RFQ — */
.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  border: 2px solid var(--color-text);
  background: var(--color-divider);
  gap: 1px;
  margin-bottom: var(--space-6);
}
.stat { background: var(--color-surface); padding: var(--space-4) var(--space-4); }
.stat-value {
  font-family: var(--font-heading);
  font-weight: var(--font-heading-weight);
  font-size: 30px;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.stat-label {
  font-size: 10.5px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-neutral-700);
  margin-top: 6px;
}

/* — a bordered data table whose cells hold boxed inputs (material list) — */
.grid-table {
  width: 100%;
  border-collapse: collapse;
  border: 2px solid var(--color-text);
  background: var(--color-surface);
}
.grid-table th {
  text-align: left;
  font-size: 10.5px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--color-neutral-700);
  font-weight: 700;
  padding: 12px 10px;
  border-bottom: 2px solid var(--color-text);
  white-space: nowrap;
}
.grid-table td {
  padding: 8px 10px;
  border-bottom: 1px solid var(--color-divider);
  vertical-align: middle;
}
.grid-table tbody tr:last-child td { border-bottom: none; }
/* A line row and its detail row (readback, spec/certs, parse questions)
   read as one — the divider between lines lives on the detail row. */
.grid-table tr.has-detail td { border-bottom: none; }
.grid-table .row-num {
  width: 42px;
  text-align: center;
  font-variant-numeric: tabular-nums;
  color: var(--color-neutral-700);
  font-size: 12.5px;
}
.grid-table .cell-input,
.grid-table .input,
.grid-table select.cell-input,
.grid-table select.input {
  width: 100%;
  min-width: 0;
  font-family: var(--font-body);
  font-size: 13px;
  padding: 9px 11px;
  color: var(--color-text);
  background: var(--color-bg);
  border: 1px solid var(--color-neutral-600);
  border-radius: 0;
}
.grid-table .cell-input:focus-visible,
.grid-table .input:focus-visible {
  outline: none;
  border-color: var(--color-accent);
  box-shadow: inset 0 0 0 1px var(--color-accent);
}
.grid-table .cell-input.is-missing { border-color: var(--color-accent); border-width: 2px; }
/* The background shorthand above wipes the chevron drawn on selects — put it
   back, sized for the tighter grid cells, so a dropdown reads as one. */
.grid-table select.cell-input,
.grid-table select.input {
  padding-right: 26px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23201e1d' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 7px center;
  background-size: 12px;
}
.grid-table .num-cell { text-align: right; }
.grid-table .num-cell .cell-input,
.grid-table .num-cell .input { text-align: right; font-variant-numeric: tabular-nums; }

/* — a selectable row list (vendors on the send screen) — */
.pick-table { width: 100%; border-collapse: collapse; border: 2px solid var(--color-text); }
.pick-table th {
  text-align: left;
  font-size: 10.5px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--color-neutral-700);
  padding: 12px 14px;
  background: var(--color-surface);
  border-bottom: 2px solid var(--color-text);
}
.pick-table td {
  padding: 14px;
  border-bottom: 1px solid var(--color-divider);
  vertical-align: middle;
}
.pick-table tbody tr:last-child td { border-bottom: none; }
/* §8 rule 6 — numeric columns are tabular and right-aligned. */
.pick-table .num { text-align: right; font-variant-numeric: tabular-nums; }

/* The shop's quote address in Settings — a value to hand out, not a field.
   user-select: all so a double-click takes the whole address, never half. */
.quote-address { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.quote-address code {
  font-size: 14px;
  background: var(--color-neutral-100);
  border: 1px solid var(--color-divider);
  padding: 8px 12px;
  user-select: all;
  word-break: break-all;
}
.pick-table tbody tr.is-picked { background: var(--color-surface); }

/* — /settings/resources/ — what a shop hands its vendors —
   A rail of vendors beside the note addressed to the selected one. The rail
   states who has already been told, because that is the population whose
   first RFQ gets junked; the stage holds the note at the width a mail client
   will actually give it. */
.res-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 14px;
  text-decoration: none;
  color: inherit;
}
.res-row + .res-row { border-top: 1px solid var(--color-divider); }
.res-row:hover { background: color-mix(in srgb, var(--color-text) 4%, transparent); }
/* Selection is the design's one affordance for it (§8's .ring-selected),
   stated here as ink weight because these rows sit flush in a rail. */
.res-row.is-selected {
  background: var(--color-neutral-100);
  box-shadow: inset 3px 0 0 var(--color-text);
}
.res-row-name {
  font-weight: 600;
  font-size: 13px;
  line-height: 1.35;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.res-row-body { flex: 1; min-width: 0; }
/* Told / not told as a square, filled when satisfied — the same vocabulary
   as the PO timeline's markers. Never colour alone: the row carries the
   word too (§8 rule 5). */
.res-mark {
  flex: none;
  width: 9px;
  height: 9px;
  border: 1.5px solid var(--color-text);
  background: var(--color-bg);
}
.res-mark.is-told { background: var(--color-text); }
.res-state {
  flex: none;
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--color-text) 62%, transparent);
}
/* The stage: the note sits on the surface tone at its real width, so what
   the buyer copies is what they see. */
.res-stage {
  background: var(--color-surface);
  border: 1px solid var(--color-divider);
  padding: var(--space-4);
  display: flex;
  justify-content: center;
}
.res-frame {
  width: 520px;
  max-width: 100%;
  height: 460px;
  border: 0;
  background: var(--color-bg);
}
.res-actions {
  display: flex;
  gap: 10px;
  align-items: center;
  flex-wrap: wrap;
  margin-top: var(--space-3);
}

/* — the shop's quote inbox (/mail/) —
   Two jobs on one screen: "needs filing" is a to-do list, "all" is the
   record. Both are scanned, not read — so a row states who wrote and
   whether it's placed, and the email itself waits behind a disclosure.
   Dumping every body inline made 26 messages a 42kB wall nobody could
   scan (2026-09-01). */
/* What the screen is, said once, as the page's sub (2026-09-04). Kept from
   the block added 2026-09-01 at Sam's request — §3's carve-out — but no
   longer a boxed callout with an icon: on a screen with nothing in it that
   box was one of five stacked frames, and it read as a help panel. */
.mail-intro { max-width: 62ch; }
/* With mail on the screen the address is a reference line above the list. */
.mail-address-line { margin-bottom: var(--space-4); }
/* With nothing on the screen the address IS the screen: the empty state
   carries it large, with Copy and the door to the supplier note. Same
   dashed frame as every other .empty, left-aligned because an address is
   read, not admired. */
.mail-empty {
  border: 1px dashed var(--color-neutral-500);
  padding: var(--space-6);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-3);
  max-width: 720px;
}
.mail-empty p { margin: 0; font-size: 13.5px; max-width: 52ch; color: var(--color-neutral-800); }
.mail-empty-address code { font-size: 16px; padding: 10px 14px; }
/* — the forward line: the safety layer on every sent pair —
   Quiet on purpose. It is not the path we want (that is the vendor
   replying), it is the one that works when the vendor writes to the buyer
   instead, and reads as available rather than as a task. One line inside
   the card's head since 2026-10-12, under the sent/replied line: as a ruled
   band at the card's foot it doubled every card, and five vendors made it
   the loudest repeated thing on a screen whose job is who answered. */
.card-forward {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: 6px;
  font-size: 12px;
}
.card-forward-ask { color: var(--color-neutral-700); }
.card-forward code {
  font-size: 12px;
  color: var(--color-neutral-800);
  user-select: all;
  word-break: break-all;
}
.card-forward .btn { padding: 3px 6px; }

/* The list is one 2px frame with hairline rows (2026-09-04) — it was a
   stack of framed cards, one 2px border per message, which the design's
   depth system (one 2px frame per screen) and the eye both refused: eleven
   messages read as eleven boxes, not a list. Scoped to .mail-screen so the
   per-RFQ split view's .mail-list rail keeps its own shape. */
.mail-screen .mail-list { display: block; border: 2px solid var(--color-text); background: var(--color-surface); }
.mail-screen .mail-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  column-gap: var(--space-4);
  row-gap: 8px;
  align-items: start;
  padding: 12px 16px 12px 18px;
  border-bottom: 1px solid var(--color-divider);
}
.mail-screen .mail-row:last-child { border-bottom: 0; }
/* Unplaced mail is the whole point of the screen; the record is backdrop.
   An ink rule at the row's left edge, inside the one frame — never accent
   (brand rule 3) — and the "not filed" tag says it in words (rule 5). */
.mail-screen .mail-row.is-unfiled { box-shadow: inset 4px 0 0 var(--color-text); }
.mail-row-head {
  grid-column: 1 / -1;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 14px;
  flex-wrap: wrap;
}
.mail-who { min-width: 0; flex: 1 1 320px; }
/* The sender leads: it is what a person matches a vendor on. When the
   header carried a display name, the script splits it: name in ink, the
   address after it in the quieter weight. */
.mail-from {
  font-size: 13.5px;
  font-weight: 700;
  color: var(--color-text);
  overflow-wrap: anywhere;
}
.mail-from .mail-addr {
  font-weight: 400;
  font-size: 12.5px;
  color: var(--color-neutral-700);
  margin-left: 6px;
}
.mail-subject {
  font-size: 12.5px;
  color: var(--color-neutral-700);
  margin-top: 2px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.mail-side { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.mail-when { font-size: 12px; color: var(--color-neutral-700); white-space: nowrap; font-variant-numeric: tabular-nums; }
/* The disclosure takes the left column; the body it opens stays in that
   column, beside the filing form rather than under it. */
.mail-open { grid-column: 1; min-width: 0; }
.mail-open > summary {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--color-neutral-700);
}
.mail-open .mail-body { margin-top: 8px; border: 1px solid var(--color-divider); }
.mail-open .notice { margin-top: 8px; }
.mail-files { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }
/* Filing is one line at the row's right: a real label, the grouped select,
   the button. */
.mail-file {
  grid-column: 2;
  grid-row: 2;
  display: flex;
  gap: 8px;
  align-items: center;
  justify-self: end;
}
/* .input is full-width by default; here the select is one word in a
   sentence (File under [request · vendor] File it), so it takes its own
   width and the line never wraps. */
.mail-file select.input { width: 300px; flex: none; }
.mail-file label { font-size: 12px; font-weight: 600; white-space: nowrap; color: var(--color-neutral-700); }
/* The subject named one open request, so the select opened on that
   request's vendors alone; this puts the rest back (see mail.html). */
.mail-all-requests {
  font-size: 12px;
  color: var(--color-accent-700);
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 3px;
}
@media (max-width: 720px) {
  .mail-screen .mail-row { grid-template-columns: 1fr; }
  .mail-file { grid-column: 1; grid-row: auto; justify-self: stretch; flex-wrap: wrap; }
  .mail-file select.input { width: auto; min-width: 0; flex: 1 1 200px; }
}
.mail-capped {
  font-size: 12px;
  color: var(--color-neutral-700);
  margin: 0 0 var(--space-3);
}


.pick-name { font-weight: 700; font-size: 14px; }
.pick-meta { font-size: 12px; color: var(--color-neutral-700); margin-top: 2px; }
.pick-materials { font-size: 12.5px; color: var(--color-neutral-700); }
/* "They said no last time" on the vendor picker (ADR 0022). Accent ink on the
   accent tint — the design's own warning idiom (brand rule 4), never red text
   on the page ground. The tag beside it carries the state, not the colour
   (brand rule 5). */
.pick-declined {
  /* One quiet line — tag · date — "their words", ellipsised; the title
     attribute carries the full sentence. The old accent-100 fill grew into
     a paragraph-sized blob when vendors wrote long. */
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 6px;
  max-width: 380px;
  color: var(--color-accent-700);
  font-size: 12px;
  line-height: 1.45;
}
.pick-declined .tag { flex: none; }
.pick-declined-words {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* — the outbound email panel — */
.mail-panel { border: 2px solid var(--color-text); background: var(--color-surface); }
.mail-panel-head {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px 14px;
  border-bottom: 2px solid var(--color-text);
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  font-weight: 700;
}
.mail-body {
  margin: 0;
  padding: 16px;
  font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: 12.5px;
  line-height: 1.65;
  white-space: pre-wrap;
  max-height: 460px;
  overflow-y: auto;
  color: var(--color-text);
}
textarea.mail-body {
  width: 100%;
  background: var(--color-bg);
  border: none;
  border-top: 1px solid var(--color-divider);
  resize: vertical;
  min-height: 320px;
}
textarea.mail-body:focus-visible { outline: 2px solid var(--color-accent); outline-offset: -2px; }

/* — per-vendor response cards on the RFQ screen — */
.vendor-card {
  border: 2px solid var(--color-text);
  background: var(--color-surface);
  margin-bottom: var(--space-3);
}
/* A vendor who declined or won't quote (Sam, 2026-10-14: "blur that card out
   saying they can't fill"). It recedes through the depth system — a 1px rule
   on the page ground instead of the 2px frame on surface — not through blur
   or opacity, which would take their sentence below 4.5:1 while the card
   still carries live controls. The extra 1px of padding keeps the subgrid
   columns aligned with the framed cards above. */
.vendor-card.is-out {
  border: 1px solid var(--color-divider);
  padding: 1px;
  background: var(--color-bg);
}
.vendor-card-head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: 14px 16px;
  flex-wrap: wrap;
}
/* The name column gives way before the figures do: with the forward line
   inside it, its natural width pushed the total and the actions onto a
   second row. It shrinks to a 300px basis and wraps its own content
   instead; the figures only drop below on genuinely narrow cards. */
.vendor-card-head > :first-child { flex: 1 1 300px; min-width: 0; }
/* An h3 since 2026-10-12, so a screen reader can jump card to card; the
   resets keep it looking exactly like the span it replaced. */
.vendor-card-name {
  font-family: var(--font-heading);
  font-weight: 800;
  font-size: 17px;
  margin: 0;
  line-height: inherit;
  letter-spacing: inherit;
}
.vendor-card-sub { font-size: 12px; color: var(--color-neutral-700); margin-top: 2px; }
/* The card's columns (2026-10-13): who, total, lead time, actions. Wide
   screens line them up across every card through subgrid (below); narrow
   ones wrap. Labels top-align and the Total numeral is the big figure. */
.vendor-card-total,
.vendor-card-lead { min-width: 104px; }
.vendor-card-lead { display: flex; gap: 34px; align-items: flex-start; }
.vendor-card-lead:empty,
.vendor-card-total:empty { min-width: 0; }
/* The card's controls as one row: a button's width apart, so four of them
   fit beside the figures. */
.vendor-card-actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
}
.vendor-card-actions .card-quiet-link { margin-right: var(--space-2); }
/* One grid for the list, each card and its head a subgrid of it: the total
   on every card sits in the same column, so a buyer reads down the page the
   way the hidden compare screen let them read across. */
@media (min-width: 900px) {
  .vendor-card-list {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto auto auto;
    column-gap: 34px;
  }
  .vendor-card-list > * { grid-column: 1 / -1; }
  .vendor-card-list > .vendor-card { display: grid; grid-template-columns: subgrid; column-gap: 34px; }
  .vendor-card-list > .vendor-card > * { grid-column: 1 / -1; }
  /* Labels top-align across the figure columns; the buttons centre. */
  .vendor-card-list .vendor-card-head {
    display: grid;
    grid-template-columns: subgrid;
    column-gap: 34px;
    align-items: start;
  }
  .vendor-card-list .vendor-card-actions { align-self: center; }
  .vendor-card-list .vendor-card-said { grid-column: span 2; }
  .vendor-card-list .vendor-card-actions { justify-content: flex-end; }
}
/* A secondary path on a card, said quietly (2026-10-12): ink, not accent,
   because red is the card's one thing to do (rule 3), and "Edit numbers" is
   a correction, not a step. */
.card-quiet-link {
  color: var(--color-neutral-800);
  font-size: 13px;
  text-underline-offset: 3px;
}
.card-quiet-link:hover { color: var(--color-text); }
/* The same quiet link when it opens something on the card rather than
   leaving it ("Already ordered"): a button, so it reads as one to a screen
   reader, dressed as the link it sits beside. */
button.card-quiet-link {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  font-size: 13px;
  text-decoration: underline;
  cursor: pointer;
}
button.card-quiet-link:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
/* "Already ordered": one labelled field and Save, under the card's head. */
.card-inline-form {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  padding: 0 16px 14px;
  margin: 0;
}
.card-inline-form label { font-size: 13px; font-weight: 600; }
.card-inline > summary { padding: 0 16px 12px; }
.card-inline-form .input { width: 200px; max-width: 100%; }
.figure-label {
  font-size: 10.5px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  font-weight: 700;
  color: var(--color-neutral-700);
}
.figure-value {
  font-weight: 700;
  font-size: 14px;
  font-variant-numeric: tabular-nums;
  padding-top: 3px;
}
.figure-value.is-big {
  font-family: var(--font-heading);
  font-weight: 800;
  font-size: 21px;
  line-height: 1.2;
  padding-top: 0;
}
.figure-value.is-absent {
  font-weight: 400;
  font-size: 13px;
  color: var(--color-neutral-700);
}
/* The waiting card's price slot (2026-10-12): the shape of the figure that
   will land, in the figures column. A solid neutral bar, no gradient
   (depth is ruling, never gradient), and a slow opacity breathe rather than
   a shimmer or a spinner: the wait is days, so the motion has to read as
   "open", not "any second now". Still under reduced motion. */
.pending-bar {
  width: 92px;
  height: 18px;
  margin-top: 5px;
  background: var(--color-neutral-300);
}
@media (prefers-reduced-motion: no-preference) {
  .pending-bar { animation: pending-breathe 3.2s ease-in-out infinite; }
}
@keyframes pending-breathe {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.45; }
}
/* Their email, folded under the card (2026-10-13). Neutral ground, ink
   label: the card's one red is Send PO, and a red frame plus a red label
   on every unread email spent it on a state the figure already states. */
.vendor-card-raw {
  border-top: 1px solid var(--color-divider);
  background: var(--color-neutral-100);
  padding: 12px 16px;
}
.raw-label {
  font-size: 10.5px;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--color-neutral-800);
  font-weight: 700;
}
.vendor-card-raw[open] > .raw-label { margin-bottom: 8px; }
summary.raw-label {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  min-height: 24px;
}
summary.raw-label::-webkit-details-marker { display: none; }
summary.raw-label::after { content: "+"; margin-left: 10px; font-weight: 700; }
.vendor-card-raw[open] > summary.raw-label::after { content: "\2212"; }
summary.raw-label:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

/* "No response" (Sam, 2026-10-13): vendors asked over a day ago and silent
   since, folded under the answers. A section heading that opens, not a
   card: the same 2px rule the page uses between blocks sits above it. */
.card-group { margin: var(--space-6) 0 var(--space-3); }
.card-group > summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 10px 0;
  border-top: 2px solid var(--color-text);
  margin-bottom: var(--space-3);
}
.card-group > summary::-webkit-details-marker { display: none; }
.card-group > summary::after { content: "+"; font-weight: 700; color: var(--color-accent); }
.card-group[open] > summary::after { content: "\2212"; }
.card-group > summary:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.card-group-title {
  font-family: var(--font-heading);
  font-weight: 800;
  font-size: 17px;
  text-transform: lowercase;
  margin: 0;
}
.card-group-count { font-size: 13px; font-variant-numeric: tabular-nums; color: var(--color-neutral-700); }
/* The sender's address rides inside the label; an address is not a label
   and must not be shouted in caps and tracking (QUOTES@ALLOYANDBAR…). */
.raw-label .raw-from {
  text-transform: none;
  letter-spacing: 0;
  font-weight: 600;
  color: var(--color-neutral-800);
  margin-left: 8px;
}

/* — the mail view: list beside a reading pane — */
.mail-view {
  display: grid;
  grid-template-columns: minmax(280px, 0.42fr) 1fr;
  border: 2px solid var(--color-text);
  min-height: 540px;
}
@media (max-width: 900px) { .mail-view { grid-template-columns: 1fr; } }
/* Scoped to the split view — the shop inbox reuses .mail-list as a plain
   page-level stack and must not inherit the rail's border and scroller. */
.mail-view > .mail-list { border-right: 2px solid var(--color-text); max-height: 720px; overflow-y: auto; }
.mail-item {
  display: block;
  padding: 12px 14px;
  border-bottom: 1px solid var(--color-divider);
  text-decoration: none;
  color: inherit;
}
.mail-item:hover { background: var(--color-surface); }
.mail-item.is-current { background: var(--color-accent-100); border-left: 3px solid var(--color-accent); }
.mail-dir {
  font-size: 10px;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  font-weight: 700;
  color: var(--color-accent-700);
}
.mail-item-subject { font-size: 13px; margin: 3px 0; }
.mail-item-snip {
  font-size: 12px;
  color: var(--color-neutral-700);
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}
.mail-read { background: var(--color-surface); }
.mail-read-head { padding: var(--space-4); border-bottom: 2px solid var(--color-text); }
.mail-headers {
  display: grid;
  grid-template-columns: 70px 1fr;
  gap: 4px 12px;
  font-size: 12.5px;
  margin-top: var(--space-3);
}
.mail-headers dt { color: var(--color-neutral-700); }
.mail-headers dd { margin: 0; font-variant-numeric: tabular-nums; }

/* — segmented control (All / Sent / Received · sort) — */
.segmented { display: inline-flex; border: 2px solid var(--color-text); }
.segmented > * {
  padding: 8px 16px;
  font-size: 12.5px;
  font-family: var(--font-heading);
  font-weight: var(--font-heading-weight);
  background: var(--color-surface);
  color: var(--color-text);
  border: none;
  border-right: 1px solid var(--color-divider);
  cursor: pointer;
  text-decoration: none;
}
.segmented > *:last-child { border-right: none; }
.segmented > .is-on { background: var(--color-action); color: var(--color-on-action); }

/* — quote cards on Compare — the spec's eight bands (layout Part IX).
   1px divider cards; the 27px badge bar (or an equal spacer — the
   alignment keystone, so every vendor name shares a y); the winner is
   marked by the overlay ring, never by the card's own border; and the
   action is pinned to the bottom with margin-top:auto so Select buttons
   align across cards of unequal height. Bands rule with border-top so
   no junction can double. */
.quote-grid {
  display: grid;
  gap: 20px;
  align-items: start;
}
.quote-card {
  border: 1px solid var(--color-divider);
  background: var(--color-surface);
  display: flex;
  flex-direction: column;
  position: relative;
}
.quote-banner,
.quote-banner-spacer { height: 27px; flex: none; }
.quote-banner {
  display: flex;
  align-items: center;
  background: var(--color-action);
  color: var(--color-on-action);
  font-size: 10px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  font-weight: 800;
  padding: 0 14px;
}
.quote-head { padding: 18px 18px 14px; }
.quote-vendor {
  font-family: var(--font-heading);
  font-weight: 800;
  font-size: 20px;
  letter-spacing: -0.02em;
  margin-bottom: 5px;
}
.quote-sub { font-size: 11.5px; color: var(--color-neutral-700); }
.quote-total-row {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 14px;
  padding: 16px 18px;
  border-top: 1px solid var(--color-divider);
}
.quote-total-label {
  font-size: 10.5px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  font-weight: 700;
  color: var(--color-neutral-700);
  margin-bottom: 4px;
}
/* Under the figure, in its own right: the tax the vendor printed inside
   the total. Ink, not accent — it is a fact about the number, not a
   warning about it. */
.quote-total-note {
  font-size: 11.5px;
  color: var(--color-neutral-700);
  font-variant-numeric: tabular-nums;
  margin-top: 2px;
}
.quote-total {
  font-family: var(--font-heading);
  font-weight: 800;
  font-size: 31px;
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}
.quote-lead { font-size: 11.5px; color: var(--color-neutral-700); margin-top: 7px; }
/* The quoted lead time measured against the shop's needed-by date. Never
   colour alone (§8 rule 5) — the words say "late" or "to spare"; the ink
   only reinforces them. Accent-700 on paper, not accent: 11.5px in accent
   does not hold contrast (§8 rule 4). */
.quote-eta { font-size: 11.5px; font-weight: 600; margin-top: 3px; }
.quote-eta.is-ontime { color: var(--color-neutral-700); }
.quote-eta.is-late { color: var(--color-accent-700); }
.quote-caveat {
  background: var(--color-accent-100);
  color: var(--color-accent-700);
  font-size: 12px;
  line-height: 1.55;
  padding: 12px 18px;
  border-top: 1px solid var(--color-divider);
}
.quote-line {
  display: flex;
  justify-content: space-between;
  gap: 14px;
  padding: 8px 18px;
  border-top: 1px solid var(--color-divider);
  font-size: 12px;
  line-height: 1.45;
}
.quote-line-meta { font-size: 11px; color: var(--color-neutral-700); margin-top: 2px; }
.quote-line-amount {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  font-size: 12.5px;
  font-weight: 700;
}
.quote-line-amount.is-absent { font-weight: 400; color: var(--color-neutral-700); }
.quote-sums {
  padding: 14px 18px;
  border-top: 1px solid var(--color-divider);
  font-size: 12px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.quote-sum-row { display: flex; justify-content: space-between; gap: 14px; }
.quote-sum-row span:first-child { color: var(--color-neutral-700); }
.quote-sum-row span:last-child { font-variant-numeric: tabular-nums; }
.quote-note {
  padding: 12px 18px;
  font-size: 12px;
  line-height: 1.55;
  color: var(--color-neutral-700);
  border-top: 1px solid var(--color-divider);
}
.quote-action {
  padding: 16px 18px;
  border-top: 1px solid var(--color-divider);
  margin-top: auto;
}

/* — the PO document sheet on screen — */
.doc-sheet {
  border: 2px solid var(--color-text);
  background: var(--color-surface);
  padding: var(--space-6);
  /* the design's folded corner */
  clip-path: polygon(0 0, calc(100% - 26px) 0, 100% 26px, 100% 100%, 0 100%);
}
.doc-frame { width: 100%; border: 2px solid var(--color-text); background: var(--color-paper); }
/* A vendor's PDF inside a .module (review screen): the module already owns
   the outer border, so the frame only draws its divider. */
.pdf-frame {
  display: block;
  width: 100%;
  height: 560px;
  border: 0;
  border-top: 1px solid var(--color-divider);
  background: var(--color-paper);
}

/* — send RFQ: the vendor list beside what the vendor receives — */
.send-grid {
  display: grid;
  grid-template-columns: minmax(320px, 0.9fr) minmax(420px, 1.1fr);
  gap: var(--space-6);
  align-items: start;
}
@media (max-width: 1000px) { .send-grid { grid-template-columns: 1fr; } }

.vendor-card-head { row-gap: var(--space-3); }

/* An icon beside text inside a plain link — buttons get their gap from .btn,
   bare links had none, so the glyph jammed against the first letter. */
.link-icon {
  display: inline-flex;
  align-items: center;
  gap: 7px;
}
.link-icon svg { flex: none; }
/* Any stray inline icon in body copy still keeps its distance. */
.page-sub svg,
.stat svg { vertical-align: -2px; margin-right: 6px; }

/* Consistent vertical rhythm between a screen's stacked blocks, so no
   template has to hand-roll a margin to avoid a collision. */
.page > .empty,
.page > .table-scroll,
.page > .module,
.page > .stats,
.page > .review-grid,
.page > .send-grid,
.page > .quote-grid,
.page > .mail-view,
.page > form,
.page > iframe { margin-bottom: var(--space-6); }
.page > *:last-child { margin-bottom: 0; }
/* The stat strip is a header element: it hugs what it describes. */
.stats + .vendor-card,
.stats + #rfq-inbox { margin-top: 0; }

/* Name and status badge as one flex row: a single controlled gap, instead
   of collapsed whitespace plus an inline margin (which stacked into a
   ragged space that changed with the font). */
.vendor-card-title {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}

/* — "use my words": the struck-through line (entry screen) —
   The strike sweeps in by color transition; the row fades and locks. The
   remove button (last cell) stays live. Reduced motion gets the end state. */
.grid-table tr.is-written td:not(:last-child) {
  opacity: 0.45;
  pointer-events: none;
  transition: opacity 0.35s ease;
}
.grid-table tr.is-written .cell-input,
.grid-table tr.is-written select.cell-input {
  text-decoration: line-through;
  text-decoration-color: var(--color-accent);
  text-decoration-thickness: 2px;
}
.grid-table .cell-input,
.grid-table select.cell-input {
  transition: text-decoration-color 0.35s ease;
  text-decoration-color: transparent;
}
@media (prefers-reduced-motion: reduce) {
  .grid-table tr.is-written td:not(:last-child),
  .grid-table .cell-input,
  .grid-table select.cell-input {
    transition: none;
  }
}

/* — suggested suppliers (vendor book): registry cards a shop can act on.
   Same idiom as the compare quote cards — 1px divider card on surface,
   internal band rules, no shadows, radius 0. The reasons are sentences the
   suggestion engine wrote; nothing here may add usage numbers. — */
.suggest-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: var(--space-3);
  margin-top: var(--space-4);
}
.suggest-card {
  border: 1px solid var(--color-divider);
  background: var(--color-surface);
  display: flex;
  flex-direction: column;
}
.suggest-head {
  padding: 14px 16px 12px;
}
.suggest-name {
  font-weight: 700;
  font-size: 13.5px;
  line-height: 1.3;
}
.suggest-place {
  font-size: 11.5px;
  color: color-mix(in srgb, var(--color-text) 55%, transparent);
  margin-top: 2px;
}
.suggest-reasons {
  list-style: none;
  margin: 0;
  padding: 11px 16px 12px;
  border-top: 1px solid var(--color-divider);
  font-size: 12.5px;
  line-height: 1.75;
  flex: 1;
}
.suggest-reasons .is-unverified {
  color: color-mix(in srgb, var(--color-text) 55%, transparent);
}
.suggest-actions {
  display: flex;
  gap: 8px;
  padding: 12px 16px;
  border-top: 1px solid var(--color-divider);
}
/* The panel is a small disclosure, closed by default — a shop opens it when
   it wants ideas; it never competes with the book itself. */
.suggest-panel > summary {
  list-style: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 10px;
}
.suggest-panel > summary::-webkit-details-marker { display: none; }
.suggest-panel > summary::after { content: "+"; font-weight: 700; color: var(--color-accent); }
.suggest-panel[open] > summary::after { content: "\2212"; }
.suggest-panel > summary:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}
/* The blurb takes the first line; the radius control (and Refresh, where a
   page has one) sits under it, flush left with everything else — not pushed
   to the far edge (2026-08-26 review). */
.suggest-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 16px;
  margin-top: 6px;
}
.suggest-bar > p { flex: 1 1 100%; }
/* How far "near you" reaches — the shop's call, sitting with the suggestions
   it governs rather than buried in Settings. */
.radius-form {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  flex: none;
}
.radius-form label {
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-neutral-700);
  margin: 0;
}
.radius-form output {
  font-size: 12.5px;
  font-variant-numeric: tabular-nums;
  min-width: 52px;
  font-weight: 600;
}
/* Square track, square thumb — brand rule 1 holds for form controls too, so
   the native rounded slider is restyled rather than accepted. */
.radius-range {
  -webkit-appearance: none;
  appearance: none;
  width: 132px;
  height: 2px;
  background: var(--color-divider);
  margin: 0;
  cursor: pointer;
}
.radius-range::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 12px;
  height: 16px;
  border-radius: 0;
  background: var(--color-accent);
  border: none;
}
.radius-range::-moz-range-thumb {
  width: 12px;
  height: 16px;
  border-radius: 0;
  background: var(--color-accent);
  border: none;
}
.radius-range:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 4px;
}
.suggest-scroll {
  max-height: 340px;
  overflow-y: auto;
  margin-top: var(--space-3);
}
.suggest-scroll .suggest-grid { margin-top: 0; }

/* — tooltip: hover/focus bubble for icon-only controls (data-tip) — */
.tip { position: relative; }
.tip::after {
  content: attr(data-tip);
  position: absolute;
  bottom: calc(100% + 8px);
  left: 0;
  background: var(--color-text);
  color: var(--color-bg);
  font-size: 11.5px;
  line-height: 1.4;
  padding: 5px 9px;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.12s ease;
  z-index: 20;
}
.tip:hover::after,
.tip:focus-visible::after { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .tip::after { transition: none; }
}

/* — form-control upgrades: required marks, ink helpers, select chevrons — */
label.req::after,
.req-mark {
  content: "*";
  color: var(--color-accent);
  font-weight: 700;
  margin-left: 3px;
}
.req-mark { margin-left: 0; margin-right: 3px; }
/* Helper text reads in ink — neutral is reserved for true secondary metadata. */
.field-help {
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--color-text);
  margin-top: 4px;
}
/* Selects read as selectable: every dropdown wears the same Lucide chevron
   as select.input (drawn above; the data URI can't read the ink token). */
.field select,
form select {
  appearance: none;
  -webkit-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23201e1d' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 8px center;
  background-size: 13px;
  padding-right: 28px;
}

/* — nav additions: shop name + open-RFQ badge — */
.nav-shop {
  font-size: 12px;
  letter-spacing: 0.03em;
  max-width: 200px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* For a staff operator the shop name is a link to the switcher (ADR 0037).
   It stays ink: accent is for action and emphasis (brand rule 3), and the
   name of the shop you are already in is neither. The underline on hover is
   what says it is clickable. */
a.nav-shop { color: inherit; text-decoration: none; }
a.nav-shop:hover { color: inherit; text-decoration: underline; }
.nav-badge {
  display: inline-block;
  min-width: 16px;
  text-align: center;
  margin-left: 6px;
  padding: 1px 5px;
  font-size: 10.5px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  background: var(--color-action);
  color: var(--color-on-action);
}

/* — chip multi-select: checkbox groups that read as selectable tags — */
.chip-select {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.chip {
  display: inline-flex;
  align-items: center;
  margin: 0;
  cursor: pointer;
}
.chip input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}
.chip span {
  font-size: 12px;
  letter-spacing: 0.02em;
  padding: 4px 10px;
  border: 1px solid var(--color-neutral-400);
  background: transparent;
  color: var(--color-neutral-700);
  transition: border-color 0.15s ease, color 0.15s ease;
}
.chip:hover span { border-color: var(--color-text); color: var(--color-text); }
.chip input:checked + span {
  border-color: var(--color-text);
  background: var(--color-text);
  color: var(--color-bg);
}
.chip input:focus-visible + span {
  outline: 2px solid var(--color-accent);
  outline-offset: 1px;
}

/* — RFQ list additions: link segments, red row action, vertical scroll — */
a.seg-opt { color: inherit; text-decoration: none; }
.seg-opt.is-on {
  background: var(--color-bg);
  box-shadow: inset 0 0 0 2px var(--color-accent);
}
.row-action {
  color: var(--color-accent-700);
  font-weight: 800;
  font-size: 13px;
  text-decoration: none;
}
.row-action:hover { text-decoration: underline; }
/* Icon form of the red row action: underline says nothing on an svg, so
   hover is the same wash the ghost buttons use. */
.row-action-icon { display: inline-flex; align-items: center; justify-content: center; vertical-align: middle; width: 28px; height: 28px; }
.row-action-icon:hover { text-decoration: none; background: color-mix(in srgb, var(--color-text) 7%, transparent); }
/* Long lists scroll instead of stretching the page; the header stays put. */
.list-scroll { max-height: 62vh; overflow-y: auto; }
.list-scroll thead th { position: sticky; top: 0; background: var(--color-bg); z-index: 1; }

/* — field micro-descriptions: 3–4 ink words under a header or label — */
.th-sub {
  display: block;
  font-size: 10px;
  font-weight: 400;
  letter-spacing: 0.01em;
  text-transform: none;
  color: color-mix(in srgb, var(--color-text) 62%, transparent);
  margin-top: 2px;
}

/* The nav's CTA keeps button ink — the nav-link hover/current accent must
   not repaint a filled primary button's label. */
.nav a.btn-primary,
.nav a.btn-primary:hover,
.nav a.btn-primary[aria-current="page"] {
  color: var(--color-on-action);
}

/* — dense suggestion rows (send screen, onboarding step 2): name over meta,
   action pinned right, hairline rules, quiet scroll — */
.suggest-rows {
  border: 1px solid var(--color-divider);
  background: var(--color-surface);
  max-height: 240px;
  overflow-y: auto;
  margin-top: var(--space-2);
}
.suggest-row {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 10px 14px;
}
.suggest-row + .suggest-row { border-top: 1px solid var(--color-divider); }
/* Inside a dense list the offset shadow repeats into noise — the border
   still says button; the shadow belongs to buttons standing alone. */
.suggest-row .btn { box-shadow: none; }
.suggest-row-body { flex: 1; min-width: 0; }
.suggest-row-name {
  font-weight: 600;
  font-size: 13px;
  line-height: 1.35;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.suggest-row-meta {
  font-size: 11.5px;
  line-height: 1.4;
  color: color-mix(in srgb, var(--color-text) 62%, transparent);
  margin-top: 1px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.suggest-row .btn { flex: none; }

/* ---- Site footer + legal pages (privacy, terms) ---- */
body { min-height: 100vh; min-height: 100dvh; display: flex; flex-direction: column; }
main { flex: 1; }
.site-footer { border-top: 1px solid var(--color-divider); }
.site-footer-inner {
  max-width: 1180px;
  margin: 0 auto;
  padding: 16px 26px;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 20px;
  font-size: 12px;
  color: var(--color-neutral-700);
}
.site-footer-inner a { color: inherit; }

/* ---- The analytics-cookie question (ADR 0029) ----
 * Borrows the .ob-footer idiom exactly — surface ground, one 2px ink rule,
 * sticky to the bottom — because this is the same kind of object: a bar that
 * states a consequence and carries the buttons that answer it. Fixed rather
 * than sticky, since it has to sit over whatever page a visitor landed on
 * instead of joining one page's flow.
 *
 * No accent anywhere in here. Brand rule 3 keeps red for action, and the
 * .ob-poster band is the one sanctioned field of it; a consent bar is not a
 * second exception. Both answers are .btn-secondary for a reason that
 * outlives the styling: a choice whose buttons are weighted is not freely
 * given, so Allow and Decline look identical and only the words differ.
 */
.cookie-bar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 40;
  background: var(--color-surface);
  border-top: 2px solid var(--color-text);
}
.cookie-bar-inner {
  max-width: 1180px;
  margin: 0 auto;
  padding: var(--space-4) 26px;
  display: flex;
  align-items: flex-start;
  gap: var(--space-6);
}
.cookie-bar-body { flex: 1 1 auto; }
.cookie-bar h2 {
  font-size: 16px;
  letter-spacing: 0;
  margin: 0 0 var(--space-1);
}
.cookie-bar p {
  margin: 0;
  font-size: 12.5px;
  line-height: 1.55;
  max-width: 78ch;
  color: var(--color-neutral-800);
}
.cookie-bar-actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex: none;
  padding-top: 2px;
}
/* The policy link is the third thing here and must not read as a button.
   Inherited ink rather than accent: an underlined word among two buttons is
   already unambiguous, and accent here would put red beside the actions. */
.cookie-bar-actions a {
  font-size: 12.5px;
  color: var(--color-neutral-700);
  white-space: nowrap;
}
@media (max-width: 720px) {
  .cookie-bar-inner {
    flex-direction: column;
    gap: var(--space-3);
  }
  .cookie-bar-actions { padding-top: 0; }
}

/* A legal page is a document, not a screen: one column at a reading measure,
   quiet hierarchy, no cards or panels competing with the prose. */
.legal { max-width: 660px; }
.legal .page-title { font-size: 30px; }
.legal-lede { font-size: 16px; line-height: 1.65; margin: var(--space-6) 0 0; }
.legal h2 {
  font-size: 19px;
  margin: 40px 0 10px;
  /* Anchor targets land below the sticky nav, not under it. */
  scroll-margin-top: 76px;
}
.legal p, .legal li { font-size: 14.5px; line-height: 1.7; }
.legal p { margin: 0 0 13px; }
.legal ul { margin: 0 0 13px; padding-left: 20px; }
.legal li { margin-bottom: 5px; }
/* Labelled blocks inside a section (what we collect, who we share with):
   the label leads, the explanation follows — no box around either. */
.legal dl { margin: 0 0 13px; }
.legal dt { font-weight: 700; font-size: 14.5px; margin-top: 13px; }
.legal dd { margin: 2px 0 0; font-size: 14.5px; line-height: 1.7; }
/* Warranty and liability disclaimers must be *conspicuous* (UCC 2-316) —
   traditionally shouted in all caps, which nobody reads. Contrasting weight
   on a ruled panel satisfies the same requirement and stays legible. */
.legal-conspicuous {
  border-left: 2px solid var(--color-text);
  padding-left: 16px;
  margin: 0 0 13px;
}
.legal-conspicuous p { font-weight: 600; }
.legal-conspicuous p:last-child { margin-bottom: 0; }

/* ---- Export-control notice + point-of-entry reminders (ADR 0023) ---- */
/* Policy text is content people are being asked to agree to, so it is set in
   ink at reading size — not the muted grey used for incidental help. */
.policy-body { max-width: 68ch; margin: var(--space-4) 0 var(--space-6); }
.policy-body p { font-size: 15px; line-height: 1.7; margin: 0 0 var(--space-3); }
.policy-check {
  align-items: flex-start;
  gap: 10px;
  font-size: 14.5px;
  font-weight: 600;
  padding: var(--space-3);
  border: 1px solid var(--color-text);
}
.policy-check input { margin-top: 3px; }
.policy-check + .policy-check { margin-top: var(--space-2); }
/* The reminder wherever a file or free text enters. Quiet, permanent, and
   never a dialog — a modal on every upload is a thing people click through
   without reading. Ink on the page ground, so it stays legible without
   shouting; the icon carries the meaning alongside the words (rule 5). */
.entry-reminder {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin: var(--space-2) 0;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--color-text);
  max-width: 70ch;
}
.entry-reminder svg { flex: none; margin-top: 2px; color: var(--color-accent-700); }

/* ---- Polish pass (taste-skill audit, 2026-08-26) ---- */

/* Keyboard users' first tab stop: invisible until focused, then a plain
   block over the nav (z above everything else on the page — scale tops
   out at 80). */
.skip-link {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 90;
  padding: 10px 16px;
  background: var(--color-text);
  color: var(--color-bg);
  font-size: 13px;
  font-weight: 600;
  transform: translateY(-110%);
}
.skip-link:focus {
  transform: translateY(0);
}

/* No orphaned single words under the big lowercase headlines. */
.page-title,
.empty h3 {
  text-wrap: balance;
}
.page-sub,
.empty p {
  text-wrap: pretty;
}

/* The press has weight: 1px of physical travel on activation, on top of
   the existing colour shifts. Guarded — motion is opt-out (§8). */
@media (prefers-reduced-motion: no-preference) {
  .btn:active:not(:disabled) {
    transform: translateY(1px);
  }
}

/* ---- Visual upgrades (taste-skill round 2, 2026-08-26) ---- */

/* The PO's history as a rail: square markers (radius 0) joined by a 1px
   rule. The latest event is filled, earlier ones outlined — emphasis by
   ink, never colour (§8 rule 5 pairs the label with it). */
.timeline { padding: 12px 16px; }
.timeline-item { position: relative; padding: 0 0 16px 22px; }
.timeline-item:last-child { padding-bottom: 2px; }
.timeline-item::before {
  content: "";
  position: absolute;
  left: 0;
  top: 4px;
  width: 8px;
  height: 8px;
  border: 1.5px solid var(--color-text);
  background: var(--color-bg);
}
.timeline-item:last-child::before { background: var(--color-text); }
.timeline-item::after {
  content: "";
  position: absolute;
  left: 3.5px;
  top: 16px;
  bottom: 2px;
  width: 1px;
  background: var(--color-divider);
}
.timeline-item:last-child::after { display: none; }

/* First paint cascades: list children rise in 60ms apart. Entry motion
   only — a reply landing later keeps rm-land (excluded so this rule can
   never override its flash). */
@media (prefers-reduced-motion: no-preference) {
  .rm-cascade > :not(.rm-land) { animation: rm-fade 0.3s ease both; }
  .rm-cascade > :nth-child(2):not(.rm-land) { animation-delay: 60ms; }
  .rm-cascade > :nth-child(3):not(.rm-land) { animation-delay: 120ms; }
  .rm-cascade > :nth-child(4):not(.rm-land) { animation-delay: 180ms; }
  .rm-cascade > :nth-child(5):not(.rm-land) { animation-delay: 240ms; }
  .rm-cascade > :nth-child(n + 6):not(.rm-land) { animation-delay: 300ms; }
}

/* Flat design's lift: interactive surfaces answer the cursor with ink,
   not shadow. */
.quote-card:hover { border-color: var(--color-text); }
.suggest-row:hover { background: color-mix(in srgb, var(--color-text) 4%, transparent); }

/* OS-default scrollbars (chunky gray on Windows) read as someone else's
   UI inside the theme's containers. */
.table-scroll,
.list-scroll,
.suggest-rows,
.mail-body {
  scrollbar-width: thin;
  scrollbar-color: var(--color-neutral-400) transparent;
}

/* Empty states carry one quiet icon above the heading. */
.empty > svg { color: var(--color-neutral-500); }

/* "Who's working on this" — the person filter on the RFQ and order lists.
   Sits inline beside the open/closed segment, so it has to read as part of
   the same control strip rather than a form dropped into the header. */
.who-filter {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin: 0;
}
/* A label's weight, not .field-help's — this names the control. */
.who-filter > label { white-space: nowrap; font-size: 12px; font-weight: 600; }
.who-filter > select { height: 34px; padding-top: 0; padding-bottom: 0; }
/* The no-JS submit. Scripting on, onchange already fired — a button that
   repeats what just happened is clutter, so JS removes it (see
   _who_filter_script.html). It ships visible so it works without JS. */
.who-filter-go { flex: none; }

/* ── entrance brand panel: the loop as the pitch ──────────────────────────
 * The same rail idiom as the PO history timeline, plus one drawn rule —
 * the panel uses the system's own instruments, nothing new. */
.entrance-rule {
  width: 72px;
  height: 2px;
  background: var(--color-text);
}
@media (prefers-reduced-motion: no-preference) {
  .entrance-rule {
    transform-origin: left;
    animation: rm-rule-draw 0.5s 0.15s cubic-bezier(0.16, 1, 0.3, 1) both;
  }
}
@keyframes rm-rule-draw {
  from { transform: scaleX(0); }
}

.entrance-loop { margin-top: var(--space-2); }
.entrance-step {
  position: relative;
  padding: 0 0 18px 26px;
  font-size: 13.5px;
  text-transform: lowercase;
}
.entrance-step:last-child { padding-bottom: 0; font-weight: 700; }
.entrance-step::before {
  content: "";
  position: absolute;
  left: 0;
  top: 4px;
  width: 8px;
  height: 8px;
  border: 1.5px solid var(--color-text);
  background: var(--color-surface);
}
.entrance-step::after {
  content: "";
  position: absolute;
  left: 3.5px;
  top: 16px;
  bottom: 2px;
  width: 1px;
  background: var(--color-divider);
}
.entrance-step:last-child::after { display: none; }
/* Red is for action (§8 rule 3): the ordering step is the one action here,
   and the colour is paired with its bold label, never alone (rule 5). */
.entrance-step:last-child::before {
  border-color: var(--color-accent);
  background: var(--color-accent);
}
.entrance-step-num {
  display: inline-block;
  min-width: 24px;
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  color: var(--color-neutral-700);
}

/* ── onboarding frame (2026-08-27, per the delivered onboarding design) ───
 * Two-part frame shared by every welcome screen: the step band — four
 * equal cells over a 1px-gap grid so the gaps read as hairlines — between
 * two 2px rules, and a sticky consequence footer (below) so the primary
 * action never sinks under the fold. Replaces the left vertical rail,
 * which spent a full column on four words and pushed the form off the
 * optical center. */
.ob-band {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 1px;
  background: var(--color-divider);
  border-top: 2px solid var(--color-text);
  border-bottom: 2px solid var(--color-text);
}
.ob-band-cell {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  background: var(--color-bg);
  padding: 11px 12px;
  font-size: 12px;
  text-transform: lowercase;
  color: var(--color-neutral-700);
  text-decoration: none;
}
/* The status chip: solid ink when done, accent when current — always
   paired with weight and fill, never colour alone (§8 rule 5). */
.ob-band-chip {
  width: 9px;
  height: 9px;
  flex: none;
  border: 1.5px solid var(--color-neutral-400);
  background: var(--color-surface);
}
.ob-band-num {
  font-size: 11px;
  font-variant-numeric: tabular-nums;
}
.ob-band-label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ob-band-cell.is-done { color: var(--color-text); }
.ob-band-cell.is-done .ob-band-chip {
  border-color: var(--color-text);
  background: var(--color-text);
}
/* Completed cells are links — the band is a position indicator that also
   works backwards, never a shortcut forward. */
a.ob-band-cell:hover .ob-band-label { text-decoration: underline; }
.ob-band-cell.is-current {
  background: var(--color-accent-100);
  color: var(--color-text);
  font-weight: 700;
}
.ob-band-cell.is-current .ob-band-chip {
  border-color: var(--color-accent);
  background: var(--color-accent);
}
@media (max-width: 640px) {
  .ob-band-cell:not(.is-current) .ob-band-label { display: none; }
}

/* The sticky footer: consequence line left, actions right. Surface over a
   2px rule; z-index above content, below the nav. */
.ob-footer {
  position: sticky;
  bottom: 0;
  z-index: 5;
  display: flex;
  align-items: center;
  gap: var(--space-4);
  flex-wrap: wrap;
  background: var(--color-surface);
  border-top: 2px solid var(--color-text);
  margin-top: var(--space-8);
  padding: 12px 0;
}
.ob-footer-note {
  flex: 1 1 auto;
  min-width: 220px;
  font-size: 12.5px;
  color: var(--color-neutral-700);
}
.ob-footer-actions {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}

/* The working split under the band: left is the job, right is the
   consequence rail, divided by a 2px vertical rule. Column weights come
   from the page (--ob-left / --ob-right). */
.ob-split { display: grid; gap: var(--space-6); margin-top: var(--space-6); }
@media (min-width: 980px) {
  .ob-split {
    grid-template-columns: var(--ob-left, 1.15fr) var(--ob-right, 0.85fr);
    gap: 0;
    align-items: start;
  }
  .ob-split > * + * {
    border-left: 2px solid var(--color-text);
    padding-left: var(--space-6);
    margin-left: var(--space-6);
  }
}

/* Inside a cascade, the rule still draws — the generic child-fade above
   would otherwise swallow rm-rule-draw (same specificity, source order).
   The cascade's nth-child delay rules still stagger its start. */
@media (prefers-reduced-motion: no-preference) {
  .rm-cascade > .entrance-rule {
    animation: rm-rule-draw 0.5s cubic-bezier(0.16, 1, 0.3, 1) both;
  }
}

/* On the full-width home page the rail gets room to breathe — the compact
   rhythm belongs to the entrance sidebar it was born in. */
.loop-roomy .entrance-loop { margin-top: var(--space-6); }
.loop-roomy .entrance-step { padding-bottom: 26px; font-size: 14.5px; }
.loop-roomy .entrance-step:last-child { padding-bottom: 0; }
.loop-roomy .entrance-step::before { top: 5px; }
.loop-roomy .entrance-step::after { top: 18px; }

/* App form cards (module-pad) space their fields the way the entrance card
   always has — the welcome/settings/vendor forms used to borrow .auth-card
   for exactly this margin (and got its full-width buttons with it). */
.module-pad .field { margin-bottom: var(--space-3); }

/* ── form-sheet: fields as a numbered spec sheet (2026-08-27) ─────────────
 * The intake form in the product's own voice: every field is a numbered
 * writing line — the numbering idiom the kickers, the entrance loop and
 * the welcome rail already speak — not a pile of gray boxes. Opt-in via
 * class; dense grids and islands keep the boxed .input. */
.form-sheet { counter-reset: sheet-field; }
.form-sheet .field {
  position: relative;
  padding-left: 34px;
  margin-bottom: var(--space-4);
  counter-increment: sheet-field;
}
/* The margin numbers count themselves — a template can't misnumber. */
.form-sheet .field::before {
  content: counter(sheet-field, decimal-leading-zero);
  position: absolute;
  left: 0;
  top: 1px;
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  color: var(--color-neutral-500);
}
/* A multi-line entry needs its frame back — a paragraph on a bare
   baseline reads as floating text. */
.form-sheet textarea.input {
  background: var(--color-neutral-100);
  border: 1px solid var(--color-neutral-600);
  border-bottom-color: var(--color-neutral-600);
  padding: 6px 10px;
}
.form-sheet .input {
  background: transparent;
  border: 0;
  border-bottom: 2px solid var(--color-neutral-600);
  padding: 2px 0 6px;
  font-size: 15px;
  min-height: 32px;
}
.form-sheet .input:hover:not(:focus-visible) { border-color: var(--color-neutral-700); }
.form-sheet .input:focus-visible {
  border-bottom-color: var(--color-accent);
  outline: none;
}
.form-sheet .input[aria-invalid="true"] { border-bottom-color: var(--color-accent); }

/* ── the way back (2026-08-27): no page is a dead end ─────────────────────
 * Detail pages carry a quiet arrow back to their list, above the kicker.
 * On the onboarding band, a completed step is a link — the band is also
 * the back button. */
.back-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12.5px;
  text-decoration: none;
  color: var(--color-neutral-700);
  margin-bottom: var(--space-2);
}
.back-link:hover { color: var(--color-text); text-decoration: underline; }

/* While the radius fetch is in flight the rows say so — §8: indicators
   on genuine async only. Under reduced motion the pulse is disabled and
   the dimming alone carries the state (with the live region's text). */
.suggest-refreshing { pointer-events: none; }
.suggest-refreshing .suggest-row,
.suggest-refreshing .disc-row {
  opacity: 0.55;
  animation: rm-pulse 1.2s ease-in-out infinite;
}

/* ── step 2: the letterhead preview ───────────────────────────────────────
 * The right half renders the fields live as the header a vendor receives —
 * the form stops being abstract admin, and a bad shop name is visibly bad.
 * Unfilled fields hold their place as greyed strings so the card keeps its
 * shape from first paint. */
.ob-form-group {
  display: grid;
  grid-template-columns: 120px minmax(0, 1fr);
  gap: 0 var(--space-4);
  padding: var(--space-4) 0;
  border-top: 1px solid var(--color-divider);
}
.ob-form-group:first-of-type { border-top: 0; padding-top: 0; }
/* One rhythm for stacked fields on the onboarding surfaces — outside
   .module-pad/.form-sheet the .field class carries no bottom margin, and
   the fields glued together (2026-08-27 review). */
.ob-form-group .field,
.ob-pane .field { margin-bottom: var(--space-4); }
.ob-form-gutter {
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--color-neutral-700);
  padding-top: 8px;
}
@media (max-width: 640px) {
  .ob-form-group { grid-template-columns: 1fr; gap: var(--space-2) 0; }
  .ob-form-gutter { padding-top: 0; }
}
.ob-pane { background: var(--color-neutral-100); padding: var(--space-6); }
.lh-card {
  background: var(--color-surface);
  border: 2px solid var(--color-text);
  padding: var(--space-6);
}
.lh-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-3);
  border-bottom: 2px solid var(--color-text);
  padding-bottom: 8px;
  margin-bottom: var(--space-4);
}
.lh-title {
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}
.lh-num {
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  color: var(--color-neutral-700);
  white-space: nowrap;
}
.lh-shop { font-size: 21px; font-weight: 800; line-height: 1.2; overflow-wrap: anywhere; }
.lh-meta { font-size: 12.5px; line-height: 1.55; color: var(--color-neutral-700); margin-top: 4px; }
.lh-kicker {
  font-size: 10px;
  font-weight: 800;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--color-accent-700);
  margin: var(--space-4) 0 2px;
}
.lh-ghost { color: var(--color-neutral-500); }
/* The accent-ruled aside: which fields are still blank. Not a blocker —
   the cost of skipping is just stated. */
.ob-aside {
  border-left: 3px solid var(--color-accent);
  padding: 6px 0 6px 14px;
  font-size: 12.5px;
  margin-top: var(--space-4);
}

/* ── step 3: supplier discovery ───────────────────────────────────────────
 * One framed list, full length — the page scrolls once, no nested scroll
 * well. Every row states the materials carried (what decides whether an
 * RFQ ever reaches them), a fixed-width distance column, and an Add button
 * whose edge draws a straight line down the list. */
.disc-frame { border: 2px solid var(--color-text); margin-top: var(--space-4); }
.disc-filter {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
  border-bottom: 1px solid var(--color-divider);
}
.disc-filter:focus-within { box-shadow: inset 0 -2px 0 var(--color-accent); }
.disc-filter label {
  font-size: 10px;
  font-weight: 800;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--color-neutral-700);
}
.disc-filter input {
  flex: 1;
  min-width: 0;
  border: 0;
  background: transparent;
  font: inherit;
  font-size: 14px;
}
.disc-filter input:focus-visible { outline: none; }
.disc-count {
  font-size: 11px;
  color: var(--color-neutral-700);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.disc-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 56px minmax(96px, auto);
  gap: 3px 14px;
  align-items: center;
  padding: 11px 14px;
}
.disc-row + .disc-row { border-top: 1px solid var(--color-divider); }
.disc-row.is-added { background: var(--color-accent-100); }
.disc-name { font-size: 15px; font-weight: 700; min-width: 0; }
.disc-town { font-weight: 400; font-size: 12.5px; color: var(--color-neutral-700); margin-left: 8px; white-space: nowrap; }
.disc-mats { grid-column: 1; font-size: 12px; color: var(--color-neutral-700); }
.disc-dist {
  grid-row: 1 / span 2;
  grid-column: 2;
  text-align: right;
  font-size: 12.5px;
  font-variant-numeric: tabular-nums;
  color: var(--color-neutral-700);
}
.disc-row > form { grid-row: 1 / span 2; grid-column: 3; margin: 0; }
.disc-row .btn { min-width: 96px; box-shadow: none; }
.disc-empty { font-size: 13px; color: var(--color-neutral-700); padding: 14px; margin: 0; }
button.seg-opt { font: inherit; cursor: pointer; }

/* The book rail: the live count, the added vendors, the by-hand fallback. */
.ob-rail-count { font-size: 26px; font-weight: 800; line-height: 1; font-variant-numeric: tabular-nums; }
.book-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
  padding: 9px 0;
  border-top: 1px solid var(--color-divider);
  font-size: 13.5px;
}
.book-row:first-of-type { border-top: 0; }
.book-remove {
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-neutral-700);
  background: none;
  border: 0;
  padding: 2px 0;
  cursor: pointer;
}
.book-remove:hover { color: var(--color-text); text-decoration: underline; }

/* ── step 4: the poster band and the run-through ──────────────────────────
 * The one place in the system red runs as a field rather than an accent
 * (delivered design, 2026-08-27): it marks the seam between setup and the
 * product. 800-weight display on accent clears the large-text bar (§8
 * rule 4). */
.ob-poster {
  background: var(--color-accent);
  color: var(--color-surface);
  padding: var(--space-8) var(--space-6);
  margin-top: var(--space-6);
}
.ob-poster-kicker {
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}
.ob-poster-title {
  font-size: clamp(40px, 6vw, 66px);
  font-weight: 800;
  letter-spacing: -0.04em;
  text-transform: lowercase;
  line-height: 1;
  margin: 8px 0 0;
  color: var(--color-surface);
}
.run-row {
  display: grid;
  grid-template-columns: 52px minmax(0, 1fr) auto;
  gap: 0 12px;
  align-items: start;
  padding: 14px 0;
  border-top: 1px solid var(--color-divider);
}
.run-row:first-of-type { border-top: 0; padding-top: 4px; }
.run-num {
  font-size: 13px;
  font-weight: 800;
  color: var(--color-accent-700);
  font-variant-numeric: tabular-nums;
  padding-top: 3px;
}
.run-title { font-size: 17px; font-weight: 700; }
.run-note { font-size: 12.5px; color: var(--color-neutral-700); margin-top: 3px; max-width: 52ch; }
.run-actor {
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  border: 1px solid var(--color-divider);
  padding: 3px 8px;
  color: var(--color-neutral-700);
  white-space: nowrap;
  margin-top: 2px;
}
/* ── material entry (2026-08-27 redesign) ─────────────────────────────────
 * The job name is the page title — a display-size writing line, not a small
 * boxed field. Beside it, a stat band in the onboarding band's idiom:
 * ready / need info / pieces, each a divided cell. The two entry paths sit
 * behind honest tabs; the export reminder keeps its ADR 0023 wording. */
.job-kicker {
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--color-accent-700);
  margin-bottom: 6px;
}
.job-name-input {
  display: block;
  width: 100%;
  max-width: 560px;
  background: transparent;
  border: 0;
  border-bottom: 2px solid var(--color-text);
  padding: 0 0 8px;
  font-family: var(--font-heading);
  font-weight: 800;
  font-size: clamp(26px, 4vw, 40px);
  letter-spacing: -0.035em;
  line-height: 1.05;
  color: var(--color-text);
}
.job-name-input:focus-visible { outline: none; border-bottom-color: var(--color-accent); }
.job-name-input:disabled { border-bottom-color: var(--color-neutral-400); }
.job-name-input::placeholder { color: var(--color-neutral-500); }
.stat-band {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(88px, 1fr);
  gap: 1px;
  background: var(--color-divider);
  border: 2px solid var(--color-text);
}
.stat-cell { background: var(--color-bg); padding: 10px 14px; }
.stat-num {
  font-family: var(--font-heading);
  font-weight: 800;
  font-size: 24px;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
}
.stat-label {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-neutral-700);
  white-space: nowrap;
}
/* The two entry paths. A tab is a real button; the active one is solid ink
   (never colour alone — the fill and the weight both say "current"). */
.entry-tabs { display: inline-flex; border: 1px solid var(--color-text); }
.entry-tab {
  font: inherit;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  padding: 9px 16px;
  background: var(--color-surface);
  color: var(--color-text);
  border: 0;
  cursor: pointer;
}
.entry-tab + .entry-tab { border-left: 1px solid var(--color-text); }
.entry-tab[aria-pressed="true"] { background: var(--color-text); color: var(--color-bg); }
.entry-tab:focus-visible { outline: 2px solid var(--color-accent); outline-offset: -2px; }
/* Paste area with its margin note past a rule — the note explains the one
   thing the box needs (one item per line). */
.paste-grid { display: grid; gap: var(--space-4); margin-top: var(--space-4); }
@media (min-width: 900px) {
  .paste-grid { grid-template-columns: minmax(0, 1fr) 250px; }
  .paste-note {
    border-left: 1px solid var(--color-divider);
    padding-left: var(--space-4);
    align-self: start;
  }
}
.paste-note { font-size: 13px; color: var(--color-neutral-700); }
.paste-box {
  width: 100%;
  min-height: 96px;
  border: 2px solid var(--color-text);
  background: var(--color-neutral-100);
  padding: 10px 12px;
  font-family: var(--font-mono, monospace);
  font-size: 13px;
}
.paste-box:focus-visible { outline: none; border-color: var(--color-accent); }
.paste-box::placeholder { color: var(--color-neutral-500); }
/* Per-line status: words, never a colour alone (§8 rule 5). It names what's
   missing ("Needs length, quantity"), so it may wrap. */
.line-status {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  line-height: 1.5;
}
.line-status.is-open { color: var(--color-accent-700); }

/* ── the list itself: header and rows share one grid template ─────────────
 * (2026-08-27 review: the table header lied — MATERIAL & GRADE spanned
 * three controls and the dimensions label sat offset from its inputs.)
 * Columns: # · material · grade (alloy+temper) · form · cross-section ·
 * length · qty · status · remove. */
.entry-scroll { overflow-x: auto; margin-top: var(--space-6); }
.entry-list {
  border: 2px solid var(--color-text);
  background: var(--color-surface);
  min-width: 1120px;
}
.entry-cols {
  display: grid;
  grid-template-columns:
    28px 118px minmax(150px, 1.1fr) 118px minmax(160px, 1.25fr)
    minmax(180px, 1.15fr) 82px minmax(100px, 118px) 32px;
  gap: 0 10px;
  align-items: center;
  padding: 8px 12px;
}
.entry-head {
  padding: 12px;
  border-bottom: 2px solid var(--color-text);
}
.entry-head > span {
  font-size: 10.5px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--color-neutral-700);
  font-weight: 700;
  white-space: nowrap;
}
.entry-line { padding: 4px 0 8px; }
.entry-line + .entry-line { border-top: 1px solid var(--color-divider); }
/* The tint travels with the named gaps in the status cell — colour and
   words together, so the footer count is actionable row by row. */
.entry-line.is-open { background: var(--color-accent-100); }
.entry-num {
  text-align: center;
  font-variant-numeric: tabular-nums;
  color: var(--color-neutral-700);
  font-size: 12.5px;
}
.entry-cell { display: flex; gap: 6px; min-width: 0; align-items: center; }
.entry-detail { padding: 0 12px 4px 50px; }
.entry-detail:empty { display: none; }
/* Boxed inputs, same treatment the review table's cells get. */
.entry-list .cell-input,
.entry-list .input,
.entry-list select.cell-input,
.entry-list select.input {
  width: 100%;
  min-width: 0;
  font-family: var(--font-body);
  font-size: 13px;
  padding: 9px 11px;
  color: var(--color-text);
  background: var(--color-bg);
  border: 1px solid var(--color-neutral-600);
}
.entry-list .cell-input:focus-visible,
.entry-list .input:focus-visible {
  outline: none;
  border-color: var(--color-accent);
  box-shadow: inset 0 0 0 1px var(--color-accent);
}
.entry-list .cell-input.is-missing { border-color: var(--color-accent); border-width: 2px; }
.entry-list select.cell-input,
.entry-list select.input {
  padding-right: 26px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23201e1d' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 8px center;
  background-size: 12px;
  appearance: none;
}
/* "use my words": the row fades and locks; status and remove stay live. */
.entry-line.is-written .entry-cols > span:not(:nth-last-child(-n + 2)) {
  opacity: 0.45;
  pointer-events: none;
  transition: opacity 0.35s ease;
}
.entry-line.is-written .cell-input,
.entry-line.is-written select.cell-input {
  text-decoration: line-through;
  text-decoration-color: var(--color-accent);
  text-decoration-thickness: 2px;
}
.entry-list .cell-input,
.entry-list select.cell-input {
  transition: text-decoration-color 0.35s ease;
  text-decoration-color: transparent;
}
@media (prefers-reduced-motion: reduce) {
  .entry-line.is-written .entry-cols > span:not(:nth-last-child(-n + 2)),
  .entry-list .cell-input,
  .entry-list select.cell-input {
    transition: none;
  }
}
/* The square remove control the reference draws — quiet until hovered. */
.x-btn {
  width: 26px;
  height: 26px;
  border: 1px solid var(--color-neutral-600);
  background: var(--color-surface);
  color: var(--color-neutral-700);
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.x-btn:hover:not(:disabled) { border-color: var(--color-text); color: var(--color-text); }
.x-btn:disabled { opacity: 0.4; cursor: default; }
/* The extras disclosure: "+ spec · certs · tolerance" with the current
   values read back beside it, so a closed row still states what it holds. */
.extras summary { display: flex; align-items: baseline; gap: 10px; cursor: pointer; list-style: none; }
.extras summary::-webkit-details-marker { display: none; }
.extras-toggle {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--color-accent-700);
  white-space: nowrap;
}
.extras-toggle::before { content: "+ "; }
.extras[open] .extras-toggle::before { content: "− "; }
.extras summary:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.extras-readback { font-size: 12px; color: var(--color-neutral-700); }

/* The read-back rail: what steps 02 and 03 actually produced. */
.rb-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 12px;
  padding: 10px 0;
  border-top: 1px solid var(--color-divider);
}
.rb-row:first-of-type { border-top: 0; }
.rb-key {
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-neutral-700);
  white-space: nowrap;
}
.rb-val {
  font-size: 13.5px;
  font-weight: 600;
  text-align: right;
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}

/* ── shop-profile form groups (2026-09-02) ────────────────────────────────
 * The same three shapes in the two places that render ShopProfileForm —
 * onboarding step 1 and Settings. Each was inline `style=` before, the
 * address row typed out three times across the two templates, so the two
 * screens could drift apart on a gap nobody would notice. §8: a primitive
 * exists once. */
/* Numbering restarts at each group. One run through a grouped form counts
   "13 PO prefix", which is a number about nothing — the writing-line idiom
   numbers a list, and this form is four lists. A CSS counter-reset here
   rescopes the counter for every field that follows. */
.sheet-group { margin: var(--space-4) 0 var(--space-2); counter-reset: sheet-field; }
.sheet-group:first-child { margin-top: 0; }
.field-row { display: grid; gap: 0 18px; }
.field-row-pair { grid-template-columns: 1fr 1fr; }
.field-row-place { grid-template-columns: 2fr 1fr 1fr; }
/* City / state / ZIP on a phone is three unusable boxes, not three columns.
   Tables still scroll rather than reflow (§8) — a form is not a table. */
@media (max-width: 640px) {
  .field-row-pair,
  .field-row-place { grid-template-columns: 1fr; }
}

/* The generated material list, named where the shop's own wording wraps
   around it. Neutral rather than accent: it labels output the shop cannot
   edit, not a section they can. */
.sheet-generated {
  margin: var(--space-3) 0;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-neutral-700);
}

/* A row of doors out of a settings section (the vendor book, the vendor
   collateral). Buttons, because they are navigation, not prose. */
.link-row { display: flex; flex-wrap: wrap; gap: 10px; }

/* ── settings cards: what the shop is, stated ────────────────────────────
 * 2026-09-04. Settings held four stacked forms, so facts that are set once
 * and afterwards only read arrived as a page of open inputs. The cards
 * state each section instead, and exactly one becomes a form when its Edit
 * is pressed.
 *
 * Same frame idiom as .ob-band — equal cells over a 1px-gap grid, so the
 * gaps read as hairlines inside one 2px rule, rather than six separately
 * boxed modules stacked down the page.
 */
.fact-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1px;
  background: var(--color-divider);
  border: 2px solid var(--color-text);
}
@media (max-width: 860px) {
  .fact-grid { grid-template-columns: minmax(0, 1fr); }
}
.fact-cell {
  min-width: 0;
  background: var(--color-surface);
  padding: var(--space-4);
}
/* The cell's own action sits on the kicker's line, right, where the eye
   already stops. Neutral, not accent: six accent chips down one screen
   would be a decorative use of red (§8 rule 3). */
.fact-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  min-height: 26px;
  margin-bottom: var(--space-3);
}
.fact-list { margin: 0; }
.fact-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
  padding: 4px 0;
  font-size: 13px;
}
.fact-row dt,
.fact-row dd { margin: 0; }
.fact-label { color: var(--color-neutral-700); }
.fact-value {
  min-width: 0;
  font-weight: 600;
  text-align: right;
  overflow-wrap: anywhere;
}
/* An unset value is a value, so it prints — quietly, and never as a zero or
   a guess (§9 copy rules). Weight carries the difference as well as colour. */
.fact-value.is-unset { font-weight: 400; color: var(--color-neutral-700); }
/* Prose (the RFQ opening and closing) reads left-aligned under its label:
   right-aligning two sentences against a ragged left edge is unreadable. */
.fact-prose { padding: 4px 0 var(--space-3); font-size: 13px; }
.fact-prose:last-child { padding-bottom: 0; }
.fact-prose dt { margin: 0 0 3px; color: var(--color-neutral-700); }
.fact-prose dd { margin: 0; line-height: 1.55; }
.fact-code {
  display: inline-block;
  font-size: 13px;
  background: var(--color-neutral-100);
  border: 1px solid var(--color-divider);
  padding: 7px 10px;
  user-select: all;
  word-break: break-all;
}
/* Two of the six cards are not short fact lists: the quote address is one
   long string to hand out, and the RFQ wording is two paragraphs. They take
   the full width as bands under the 2-up rows, which is also what keeps
   every row's cells near the same height — a 7-line card beside a 2-line
   one leaves the short one mostly empty. Prose still stops at a readable
   measure rather than running the whole band. */
.fact-cell.is-wide { grid-column: 1 / -1; }
.fact-cell.is-wide .fact-prose dd,
.fact-cell.is-wide textarea.input { max-width: 68ch; }

/* The open card: neutral-100 marks it as the one working surface on the
   page, the same way .ob-pane does on the onboarding split. */
.fact-cell.is-editing { background: var(--color-neutral-100); }
.fact-cell.is-editing .field { margin-bottom: var(--space-3); }
.fact-cell.is-editing .field:last-of-type { margin-bottom: 0; }
.fact-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: var(--space-4);
}


/* ═════════════════════════════════════════════════════════════════════════
 * Layout and type utilities (2026-09-04). The templates carried ~446 inline
 * style="…" attributes — the same dozen declarations, hand-typed on every
 * screen, which is why screens drifted from each other and why `style-src`
 * still needs 'unsafe-inline' (ADR 0030's one open finding). These classes
 * are the declarations that recurred, named once. Spacing steps are the
 * --space-* tokens; type sizes are the design's own scale (11.5 / 12 / 12.5 /
 * 13 / 13.5). tests/test_ui_tokens.py ratchets the inline count down.
 * ═════════════════════════════════════════════════════════════════════════ */
.m0 { margin: 0; }
.mt-0 { margin-top: 0; }
.mt-1 { margin-top: var(--space-1); }
.mt-2 { margin-top: var(--space-2); }
.mt-3 { margin-top: var(--space-3); }
.mt-4 { margin-top: var(--space-4); }
.mt-6 { margin-top: var(--space-6); }
.mt-8 { margin-top: var(--space-8); }
.mb-0 { margin-bottom: 0; }
.mb-2 { margin-bottom: var(--space-2); }
.mb-3 { margin-bottom: var(--space-3); }
.mb-4 { margin-bottom: var(--space-4); }
.my-3 { margin-top: var(--space-3); margin-bottom: var(--space-3); }
.ml-2 { margin-left: var(--space-2); }
.pt-8 { padding-top: var(--space-8); }

.fs-11-5 { font-size: 11.5px; }
.fs-12 { font-size: 12px; }
.fs-12-5 { font-size: 12.5px; }
.fs-13 { font-size: 13px; }
.fs-13-5 { font-size: 13.5px; }
.fw-600 { font-weight: 600; }
.ta-right { text-align: right; }
.nowrap { white-space: nowrap; }
.w-full { width: 100%; }
.flex-1 { flex: 1; }
.wrap-any { min-width: 0; overflow-wrap: anywhere; }
.icon-fixed { flex: none; }

/* Reading measures, in characters. */
.measure-15 { max-width: 15ch; }
.measure-24 { max-width: 24ch; }
.measure-42 { max-width: 42ch; }
.measure-44 { max-width: 44ch; }
.measure-52 { max-width: 52ch; }
.measure-62 { max-width: 62ch; }

/* Block widths: page frames and the odd form column. */
.max-w-320 { max-width: 320px; }
.max-w-340 { max-width: 340px; }
.max-w-520 { max-width: 520px; }
.max-w-560 { max-width: 560px; }
.max-w-640 { max-width: 640px; }
.max-w-880 { max-width: 880px; }
.max-w-980 { max-width: 980px; }
.max-w-760 { max-width: 760px; }
.max-w-1080 { max-width: 1080px; }
.max-w-1200 { max-width: 1200px; }

/* Table floors (the wrapper scrolls) and fixed column widths. */
.min-w-560 { min-width: 560px; }
.min-w-640 { min-width: 640px; }
.min-w-720 { min-width: 720px; }
.w-44 { width: 44px; }
.w-64 { width: 64px; }
.w-70 { width: 70px; }
.w-76 { width: 76px; }
.w-86 { width: 86px; }
.w-90 { width: 90px; }
.w-110 { width: 110px; }
.w-120 { width: 120px; }
.w-130 { width: 130px; }
.w-150 { width: 150px; }
.w-170 { width: 170px; }
/* `select.input` sets width:100% at element+class specificity, which the
   .w-* utilities cannot beat — so a select in a flex row ignored its width
   and ate the whole line (2026-09-08, the order's Documents row). */
select.w-170 { width: 170px; }
.w-215 { width: 215px; }

/* Flex rows and stacks. `.row` is a centred row with the design's 10px gap;
   the modifiers change one thing each. */
/* A vendor's reply attached to an order: their words and their files,
   stacked. Also the hook that lets a test tell this card's label from the
   Documents section's dropdown, which lists the same words as choices. */
.reply-card { display: flex; flex-direction: column; gap: 10px; margin-bottom: var(--space-4); }
.row { display: flex; align-items: center; gap: 10px; }
/* A row is one line: a field's stacked-form bottom margin (.module-pad
   .field) would push its input above a button sharing the line. Declared
   after that rule so it wins the specificity tie. */
.row > .field { margin-bottom: 0; }
.row-8 { gap: 8px; }
.row-14 { gap: 14px; }
.row-16 { gap: var(--space-4); }
.row-wrap { flex-wrap: wrap; }
.row-between { justify-content: space-between; }
.row-end { align-items: flex-end; }
.row-baseline { align-items: baseline; }
.spread { display: flex; justify-content: space-between; gap: 10px; }
.stack { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; }
.stack-4 { gap: 4px; }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0 18px; }
/* City / state / ZIP on one line. */
.grid-place { display: grid; grid-template-columns: 1.5fr 0.6fr 0.8fr; gap: 0 18px; }

/* — small components that were inline — */
/* The 2px ink frame around a scrolling table (one per screen — the spec's
   depth system is that 2px frames are rare). */
.frame { border: 2px solid var(--color-text); }
.rule-ink { border: 0; border-top: 2px solid var(--color-text); margin: var(--space-6) 0 var(--space-4); }
.inline-form { display: inline; margin: 0; }
/* Table cells: the row's name, and the row's identifier (PO / RFQ number). */
.cell-name { font-weight: 600; font-size: 13px; }
.cell-key { font-weight: 700; font-size: 13px; }
.tag-inline { margin: 1px 2px 1px 0; }
.grid-table.borderless { border: none; }
.empty.borderless { border: none; margin: 0; }
.mail-body.unclamped { max-height: none; }
.mail-body-bare { padding: 0; max-height: 220px; background: none; }
/* A <summary> that is an action, not an aside. */
.summary-action { font-size: 12.5px; font-weight: 600; cursor: pointer; }
/* The quiet notice — the send screen's aside. */
.notice-quiet {
  background: var(--color-surface);
  border: 1px solid var(--color-divider);
  padding: var(--space-2) var(--space-3);
}
/* A warning that sits flush inside a framed reading pane. */
.notice-flush {
  margin: 0;
  max-width: none;
  border-left: 0;
  border-right: 0;
  border-top: 0;
  padding: 12px var(--space-4);
}
/* Toasts (2026-10-13): the click's answer, floating top-right under the
   nav, clear of the cookie bar at the bottom. The dialog's frame (2px ink,
   square), no shadow: depth is ruling, never glow. */
.toasts {
  position: fixed;
  top: 72px;
  right: var(--space-6);
  z-index: 50;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  width: min(380px, calc(100% - 2 * var(--space-4)));
}
/* A phone's nav wraps to two rows, so the toast sits at the bottom there. */
@media (max-width: 640px) {
  .toasts { top: auto; bottom: var(--space-4); right: var(--space-4); }
}
.toast {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  padding: 10px 8px 10px 14px;
  background: var(--color-surface);
  color: var(--color-text);
  border: 2px solid var(--color-text);
  font-size: 13px;
  line-height: 1.45;
  transition: opacity 0.2s ease;
}
.toast > span { flex: 1 1 auto; padding-top: 3px; }
.toast > svg { flex: none; margin-top: 4px; }
.toast-warn {
  color: var(--color-accent-700);
  background: var(--color-accent-100);
  border-color: var(--color-accent);
}
.toast-close {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  cursor: pointer;
}
.toast-close:hover { background: color-mix(in srgb, currentColor 10%, transparent); }
.toast-close:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 1px; }
.toast.is-leaving { opacity: 0; }
@media (prefers-reduced-motion: no-preference) {
  .toast { animation: rm-fade 0.16s ease both; }
}
@media (prefers-reduced-motion: reduce) {
  .toast { transition: none; }
}
.start-card { max-width: 560px; border: 2px solid var(--color-text); padding: 26px; }
.section-title { font-size: 22px; margin: 0 0 var(--space-3); }
.page-title-lg { font-size: 38px; }
.lede { font-size: 15px; max-width: 44ch; margin: 0 0 var(--space-4); }
/* Label and its side link (Password / Forgot your password?) on one line. */
.label-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 12px;
  margin-bottom: 5px;
}
.auth-actions { display: flex; gap: 8px; align-items: stretch; }
/* The card's own buttons carry this margin; a .btn sharing the row has to
   match it or the row's tops disagree. */
.auth-card .btn { margin-top: var(--space-2); }
.auth-card .btn-code {
  display: inline-flex;
  align-items: center;
  padding: 0 14px;
}
.plain-list { list-style: none; margin: 6px 0 0; padding: 0; font-size: 12px; }
.check-row { display: flex; align-items: center; gap: 8px; font-size: 13px; color: inherit; }
.import-log { margin: 0; font-size: 12.5px; line-height: 1.8; white-space: pre-wrap; }
.who-filter .who-select { width: auto; min-width: 150px; }
.mail-read-title { font-size: 20px; margin: 0; }
.mail-read-files { padding: 0 var(--space-4) var(--space-4); display: flex; gap: 8px; flex-wrap: wrap; }
.mail-read-foot { padding: 0 var(--space-4) var(--space-4); }
.review-foot { padding: var(--space-4); border-top: 2px solid var(--color-text); }
/* The onboarding split's column ratios, per step. */
.ob-split-vendors { --ob-left: 1.6fr; --ob-right: 0.9fr; }
.ob-split-shop { --ob-left: 1.15fr; --ob-right: 0.85fr; }
.ob-split-resources { --ob-left: 340px; --ob-right: minmax(0, 1fr); }
.ob-split-ready { --ob-left: 1.35fr; --ob-right: 0.65fr; }

/* — confirm dialog — a native <dialog>, opened by base.html for any
   form[data-confirm]. It replaced `onsubmit="return confirm(…)"`: the nonce
   CSP (ADR 0030) refuses inline handlers, so from 2026-09-03 those forms
   submitted with no confirmation at all. Same skin as the island's .dialog;
   `display` is left to the UA so a closed dialog stays closed. */
.confirm-dialog {
  width: min(440px, calc(100% - 2 * var(--space-4)));
  padding: var(--space-4);
  border: 2px solid var(--color-text);
  background: var(--color-surface);
  color: var(--color-text);
}
.confirm-dialog[open] { display: flex; flex-direction: column; gap: var(--space-3); }
.confirm-dialog::backdrop { background: color-mix(in srgb, var(--color-neutral-900) 50%, transparent); }
.confirm-dialog .dialog-body { margin: 0; }
@media (prefers-reduced-motion: no-preference) {
  .confirm-dialog[open] { animation: rm-fade 0.16s ease both; }
}
/* The PDF viewer (2026-10-12): the vendor's document over the page, as large
   as the window allows, in the same 2px-ink frame. The PDF sits on paper
   (--color-paper is for exactly this: surfaces emulating a document). */
.pdf-dialog {
  width: min(1000px, calc(100% - 2 * var(--space-4)));
  height: calc(100vh - 2 * var(--space-6));
  max-height: none;
  padding: 0;
  border: 2px solid var(--color-text);
  background: var(--color-surface);
  color: var(--color-text);
}
.pdf-dialog[open] { display: flex; flex-direction: column; }
.pdf-dialog::backdrop { background: color-mix(in srgb, var(--color-neutral-900) 50%, transparent); }
.pdf-dialog-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--color-divider);
}
.pdf-dialog-title { font-size: 17px; margin: 0; }
.pdf-dialog-frame { flex: 1; width: 100%; border: 0; background: var(--color-paper); }
@media (prefers-reduced-motion: no-preference) {
  .pdf-dialog[open] { animation: rm-fade 0.16s ease both; }
}

/* — page transitions — the browser cross-fades one document into the next
   (cross-document View Transitions). The nav and footer are identical on
   every page, so they hold still while the content changes; with this on,
   the per-page .rm-screen rise would run on top of it, so it yields. Not a
   progress indicator (§8) — nothing spins; the page simply arrives. */
@view-transition { navigation: auto; }
.nav { view-transition-name: site-nav; }
.site-footer { view-transition-name: site-footer; }
@supports (view-transition-name: none) {
  .rm-screen { animation: none; }
}
@media (prefers-reduced-motion: reduce) {
  @view-transition { navigation: none; }
}

/* ── settings rows (/account/, 2026-10-13) ────────────────────────────────────
 * Grouped lists, one 2px ink frame per group with hairlines between rows:
 * the fact-grid idiom in a single column. A row is icon · label + value ·
 * one control. Icons mark the row and never carry state; the value line is
 * the state, in words (rule 5).
 */
.set-group { margin-bottom: var(--space-6); }
.set-group-title {
  font-family: var(--font-body);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--color-accent-700);
  margin: 0 0 var(--space-2);
}
.set-list {
  display: flex;
  flex-direction: column;
  gap: 1px;
  background: var(--color-divider);
  border: 2px solid var(--color-text);
}
.set-row {
  display: grid;
  grid-template-columns: 20px minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-3) var(--space-4);
  min-height: 60px;
  padding: 12px var(--space-4);
  background: var(--color-surface);
  color: var(--color-text);
  text-decoration: none;
}
.set-row.is-editing,
.set-row.is-tall { align-items: start; }
.set-row.is-tall .set-icon { padding-top: 2px; }
.set-icon { display: flex; color: var(--color-neutral-700); }
.set-text { min-width: 0; }
.set-label { display: block; font-size: 14px; font-weight: 600; }
.set-value {
  font-size: 13px;
  color: var(--color-neutral-700);
  margin-top: 2px;
  overflow-wrap: anywhere;
}
/* A setting nobody has made yet reads as absence, not as a value. */
.set-value.is-unset { font-style: italic; }
.set-control { display: flex; align-items: center; gap: var(--space-2); color: var(--color-neutral-700); }
/* A row that is a door: the whole row is the target, the chevron says so. */
.set-row.is-link:hover { background: color-mix(in srgb, var(--color-text) 4%, var(--color-surface)); }
.set-row.is-link:hover .set-control { color: var(--color-text); }
.set-row.is-link:focus-visible { outline: 2px solid var(--color-accent); outline-offset: -2px; }
.set-fields {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-3);
  margin-top: var(--space-3);
}
.set-fields .field { margin: 0; }
/* Email takes the room, the role select stays a select's width. */
.set-fields-invite { grid-template-columns: minmax(0, 1fr) 160px; }
/* An opened row lands clear of the sticky nav (60px) when ?edit= jumps to
   its anchor — it used to open underneath it. */
.set-row { scroll-margin-top: 76px; }
.set-code { font-size: 13px; color: var(--color-text); overflow-wrap: normal; }
/* The RFQ wording, in full: what a vendor reads, quoted, at a reading
   measure, with the generated list marked between the two halves. */
.set-quote {
  margin-top: var(--space-3);
  padding-left: var(--space-3);
  border-left: 1px solid var(--color-divider);
  max-width: 68ch;
  font-size: 13px;
  line-height: 1.55;
}
.set-quote p { margin: 0; white-space: pre-line; }
.set-quote .sheet-generated { margin: var(--space-2) 0; }
/* Reset sits under the open wording form, apart from Save. */
.set-row-sub { min-height: 0; padding-top: 0; }
/* The radius: the vendor book's square slider, ink-thumbed here like the
   switch — a setting's state, not the page's next action. */
.set-range { gap: var(--space-3); }
.set-range .radius-range { width: 220px; }
.set-range .radius-range::-webkit-slider-thumb { background: var(--color-text); }
.set-range .radius-range::-moz-range-thumb { background: var(--color-text); }
.set-range output { font-size: 13px; font-weight: 600; min-width: 52px; text-align: right; color: var(--color-text); }
.set-actions { display: flex; gap: var(--space-2); margin-top: var(--space-3); }
/* Delete sits apart: space and a quieter 1px frame, never red (rule 3). */
.set-group-danger { margin-top: var(--space-8); }
.set-group-danger .set-list { border-width: 1px; border-color: var(--color-divider); }

/* The switch: square (radius 0, rule 1), ink when on — "on" is a state, not
   the next action, so it never spends the red. The word beside it carries
   the state for anyone who can't read the fill (rule 5); role=switch with
   aria-checked carries it to a screen reader. A real submit button, so it
   works with no JS; the page reloads with the new state. */
.set-switch { gap: 10px; }
.switch-state { font-size: 13px; font-weight: 600; min-width: 2ch; text-align: right; }
.switch {
  position: relative;
  width: 44px;
  height: 24px;
  padding: 0;
  flex: none;
  border: 2px solid var(--color-text);
  background: var(--color-neutral-100);
  cursor: pointer;
}
.switch-thumb {
  position: absolute;
  top: 3px;
  left: 3px;
  width: 14px;
  height: 14px;
  background: var(--color-text);
}
.switch[aria-checked="true"] { background: var(--color-text); }
.switch[aria-checked="true"] .switch-thumb { left: 23px; background: var(--color-bg); }
.switch:hover { box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-text) 10%, transparent); }
.switch:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

@media (max-width: 640px) {
  .set-row { gap: var(--space-2) var(--space-3); padding: 12px var(--space-3); }
  .set-fields { grid-template-columns: minmax(0, 1fr); }
  /* A long control (Send confirmation email) drops under the text. */
  .set-row > form.set-control:has(.btn-primary) { grid-column: 2 / -1; }
  /* The slider takes the full row under its label on a phone. */
  .set-row > .set-range { grid-column: 2 / -1; }
  .set-range .radius-range { flex: 1 1 auto; width: auto; }
  .set-fields-invite { grid-template-columns: minmax(0, 1fr); }
}
