/* fallow-ignore-file unused-file
   Loaded by src/allianceos/web/templates/base.html as /static/{name}.
   Fallow resolves imports, not Jinja-rendered href/src attributes, so it
   reads every one of these as unreachable. They are served by the
   StaticFiles mount in web/app.py and driven by tests/test_web.py. */
/* Alliance OS — application shell.
   Native-platform-first per the DESIGN.md cost ledger: CSS grid, :has(),
   clamp(), color-mix() and backdrop-filter do the work. No framework. */

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

body {
  margin: 0;
  font-family: var(--font-body);
  font-size: 1rem;
  line-height: 1.6;
  color: var(--ink);
  background: var(--surface);
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3 { font-family: var(--font-display); font-weight: 800; margin: 0; }
h1 { font-size: clamp(1.6rem, 1.35rem + 1vw, 2.25rem); letter-spacing: -.015em; color: var(--deep); }
h2 { font-size: 1.25rem; letter-spacing: -.01em; }
h3 { font-size: 1rem; }
a { color: var(--deep); }

/* Mono is the spec voice: model numbers, prices, stats, phone. */
.mono, .num, .sku { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.num { text-align: right; }

.eyebrow {
  font-weight: 700; font-size: .75rem; letter-spacing: .14em;
  text-transform: uppercase; color: var(--ink-soft);
}

/* ---------- shell ---------- */

.shell { display: grid; grid-template-columns: var(--sidebar-w) 1fr; min-height: 100vh; }

.sidebar {
  /* Midnight Electric, theme-invariant. The nav rail is always the dark
     spine, so the OS keeps one constant landmark in either theme. */
  background: var(--surface-hero);
  color: var(--hero-ink);
  border-right: 1px solid var(--hero-border);
  display: flex; flex-direction: column;
  position: sticky; top: 0; height: 100vh;
}

.brand {
  height: var(--topbar-h);
  display: flex; align-items: center; gap: var(--space-3);
  padding: 0 var(--space-4);
  border-bottom: 1px solid var(--hero-border);
  flex-shrink: 0;
}
.brand img { height: 24px; width: auto; }
.brand .os-tag {
  font-family: var(--font-mono); font-size: .58rem; font-weight: 600;
  letter-spacing: .12em; color: var(--verde);
  border: 1px solid color-mix(in srgb, var(--verde) 40%, transparent);
  border-radius: var(--radius-pill); padding: 1px 7px;
}

.nav { padding: var(--space-4) var(--space-3); overflow-y: auto; flex: 1; }
.nav-group + .nav-group { margin-top: var(--space-5); }
.nav-group-label {
  font-family: var(--font-mono); font-size: .62rem; font-weight: 600;
  letter-spacing: .14em; text-transform: uppercase;
  color: var(--hero-ink-soft); opacity: .72;
  padding: 0 var(--space-3) var(--space-2);
}

.nav a {
  display: flex; align-items: center; gap: var(--space-3);
  min-height: var(--touch-min);
  padding: 0 var(--space-3);
  border-radius: var(--radius-sm);
  color: var(--hero-ink-soft);
  text-decoration: none;
  font-size: .9rem;
  position: relative;
  transition: background var(--motion-fast) var(--ease-standard),
              color var(--motion-fast) var(--ease-standard);
}
.nav a:hover { background: var(--hero-elevated); color: var(--hero-ink); }
.nav a:focus-visible { outline: 2px solid var(--verde); outline-offset: 2px; }

/* Current page. Verde is the signature and is rationed: exactly one nav item
   can wear it, as a 3px rail — a fill, never loose text. */
.nav a[aria-current="page"] { background: var(--hero-elevated); color: var(--hero-ink); }
.nav a[aria-current="page"]::before {
  content: ""; position: absolute; left: 0; top: 50%;
  translate: 0 -50%; width: 3px; height: 20px;
  background: var(--verde); border-radius: var(--radius-pill);
}
.nav svg { width: 18px; height: 18px; flex-shrink: 0; stroke-width: 1.6; }

.nav-foot {
  padding: var(--space-3);
  border-top: 1px solid var(--hero-border);
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-2);
  font-size: .78rem; color: var(--hero-ink-soft);
}

/* ---------- main ---------- */

.main { min-width: 0; display: flex; flex-direction: column; }

.topbar {
  height: var(--topbar-h);
  display: flex; align-items: center; gap: var(--space-4);
  padding: 0 var(--space-5);
  border-bottom: 1px solid var(--border);
  background: color-mix(in srgb, var(--surface) 86%, transparent);
  backdrop-filter: blur(10px);
  position: sticky; top: 0; z-index: 10;
}
.topbar .spacer { flex: 1; }

.search { flex: 1; max-width: 460px; position: relative; display: flex; align-items: center; }
.search input {
  width: 100%; min-height: var(--touch-min);
  padding: 0 var(--space-4) 0 38px;
  border: 1px solid var(--border); border-radius: var(--radius-pill);
  background: var(--panel); color: var(--ink);
  font: inherit; font-size: .9rem;
}
.search input:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }
/* The magnifier ONLY — direct child. Was `.search svg`, which from 2026-08-24
   also caught the microphone button's icon and pinned it on top of the
   magnifier with pointer-events off, i.e. an invisible dead button. */
.search > svg {
  position: absolute; left: 13px; width: 16px; height: 16px;
  color: var(--ink-soft); pointer-events: none;
}



.content { padding: var(--space-5); max-width: 1280px; width: 100%; }
.page-head { display: flex; align-items: flex-start; gap: var(--space-4); margin-bottom: var(--space-5); flex-wrap: wrap; }
.page-head .spacer { flex: 1; }
.page-head p { margin: var(--space-2) 0 0; color: var(--ink-soft); max-width: 62ch; }

/* ---------- components ---------- */

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--elevation-card);
  padding: var(--space-5);
}

.grid { display: grid; gap: var(--space-4); }
.grid-stats { grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); }
.grid-two { grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); }

.stat .label { font-size: .78rem; color: var(--ink-soft); display: block; }
.stat .value {
  font-family: var(--font-mono); font-weight: 600;
  font-size: clamp(1.5rem, 1.2rem + 1vw, 2rem);
  letter-spacing: -.02em; color: var(--deep); display: block;
  margin-top: var(--space-1);
}
.stat .foot { font-size: .78rem; color: var(--ink-soft); margin-top: var(--space-1); }

.btn {
  display: inline-flex; align-items: center; justify-content: center;
  gap: var(--space-2);
  min-height: var(--touch-min); padding: 0 var(--space-4);
  border-radius: var(--radius-sm); border: 1px solid transparent;
  font: inherit; font-size: .9rem; font-weight: 600;
  cursor: pointer; text-decoration: none;
  transition: filter var(--motion-instant) var(--ease-standard),
              translate var(--motion-instant) var(--ease-standard);
}
.btn:active { filter: brightness(.92); translate: 0 1px; }
.btn:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.btn[disabled] { background: var(--panel); color: var(--muted); cursor: not-allowed; filter: none; translate: none; }

.btn-primary   { background: var(--primary); color: var(--primary-ink); }
.btn-signature { background: var(--accent); color: var(--accent-ink); }
.btn-quiet     { background: var(--panel); color: var(--ink); border-color: var(--border); }
.btn-sm { min-height: 34px; padding: 0 var(--space-3); font-size: .82rem; }

.chip {
  display: inline-flex; align-items: center; gap: 5px;
  font-family: var(--font-mono); font-weight: 600; font-size: .72rem;
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--radius-pill); padding: 3px 9px; color: var(--ink);
  white-space: nowrap;
}
.chip-ok    { color: var(--ok); }
.chip-warn  { color: var(--warn); }
.chip-alert { color: var(--alert); }
.chip .dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; flex-shrink: 0; }

/* Every unverified figure carries this by construction (DESIGN.md sample
   discipline), so screenshots stay defensible. */
.sample-badge {
  font-family: var(--font-mono); font-weight: 600; font-size: .62rem;
  text-transform: uppercase; letter-spacing: .08em;
  border: 1.5px dashed var(--muted); color: var(--ink-soft);
  border-radius: var(--radius-sm); padding: 1px 6px; white-space: nowrap;
}

table { width: 100%; border-collapse: collapse; font-size: .9rem; }
th {
  text-align: left; font-size: .72rem; font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase; color: var(--ink-soft);
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--border); white-space: nowrap;
}
td { padding: var(--space-3); border-bottom: 1px solid var(--border); }
tbody tr { transition: background var(--motion-fast) var(--ease-standard); }
tbody tr:hover { background: var(--panel); }
.table-wrap { overflow-x: auto; }

.empty { text-align: center; padding: var(--space-6) var(--space-5); color: var(--ink-soft); }
.empty h3 { color: var(--ink); margin-bottom: var(--space-2); }
.empty p { max-width: 46ch; margin: 0 auto var(--space-4); }

/* ---------- motion: light is additive ----------
   Surfaces arrive fully legible and GAIN presence. Nothing starts dim. */
.reveal { animation: reveal var(--motion-moderate) var(--ease-entrance) both; }
@keyframes reveal {
  from { opacity: .82; translate: 0 6px; }
  to   { opacity: 1;   translate: 0 0; }
}
.reveal:nth-child(2) { animation-delay: calc(var(--motion-stagger-step) * 1); }
.reveal:nth-child(3) { animation-delay: calc(var(--motion-stagger-step) * 2); }
.reveal:nth-child(4) { animation-delay: calc(var(--motion-stagger-step) * 3); }
.reveal:nth-child(5) { animation-delay: calc(var(--motion-stagger-step) * 4); }

/* ---------- responsive: the rail becomes a drawer ---------- */
.scrim { display: none; }

@media (max-width: 900px) {
  .shell { grid-template-columns: 1fr; }
  .sidebar {
    position: fixed; inset: 0 auto 0 0; width: min(84vw, var(--sidebar-w));
    z-index: 40; translate: -100% 0;
    transition: translate var(--motion-moderate) var(--ease-standard);
    box-shadow: var(--elevation-pop);
  }
  .scrim {
    display: block; position: fixed; inset: 0; z-index: 30;
    background: rgb(3 0 32 / .5); opacity: 0; pointer-events: none;
    transition: opacity var(--motion-moderate) var(--ease-standard);
    border: 0; padding: 0;
  }
  .shell.nav-open .sidebar { translate: 0 0; }
  .shell.nav-open .scrim { opacity: 1; pointer-events: auto; }
  .nav-btn { display: inline-flex; }
  .content { padding: var(--space-4); }
  .topbar { padding: 0 var(--space-4); gap: var(--space-2); }
  /* The rail is a drawer here, so its language switch is off-screen; the
     top-bar copy takes over. */
  .lang-switch-top { display: inline-flex; }
}
@media (min-width: 901px) { .lang-switch-top { display: none; } }
/* The top bar sits on the page surface, not the hero, so the pill takes the
   page's own colours there. */
.topbar .lang-switch { background: var(--panel); border-color: var(--border); }
.topbar .lang-switch a { color: var(--ink-soft); }
.topbar .lang-switch a:hover { color: var(--ink); }

/* A password field with a Show/Hide button beside it. */
/* A password field with its eye INSIDE the box, as every modern app draws it
   (Hanuman, 7 Sep 2026, law). The field keeps room on the right for it. */
.field-reveal { position: relative; }
.field-reveal .field { width: 100%; padding-right: 3rem; }
.reveal-eye {
  position: absolute; top: 50%; right: .35rem; transform: translateY(-50%);
  display: inline-flex; align-items: center; justify-content: center;
  width: 2.5rem; height: 2.5rem; padding: 0; margin: 0;
  border: 0; border-radius: var(--radius-md); background: transparent;
  color: var(--ink-soft); cursor: pointer;
}
.reveal-eye:hover { color: var(--ink); background: var(--panel-soft, rgba(127,127,127,.12)); }
.reveal-eye:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }
.reveal-eye .eye-shut { display: none; }
.reveal-eye[aria-pressed="true"] .eye-open { display: none; }
.reveal-eye[aria-pressed="true"] .eye-shut { display: inline; }

@media (min-width: 901px) { .nav-btn { display: none; } }

.nav-btn {
  align-items: center; justify-content: center;
  width: var(--touch-min); height: var(--touch-min);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--panel); color: var(--ink); cursor: pointer;
  flex-shrink: 0;
}

.visually-hidden {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}
.skip-link {
  position: absolute; left: var(--space-3); top: -60px; z-index: 100;
  background: var(--surface); color: var(--deep); padding: var(--space-3);
  border-radius: var(--radius-sm); border: 1px solid var(--border);
  transition: top var(--motion-fast) var(--ease-standard);
}
.skip-link:focus { top: var(--space-3); }

/* Form fields — one class so every input reads the same across pages. */
.field {
  width: 100%;
  min-height: var(--touch-min);
  padding: var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--panel);
  color: var(--ink);
  font: inherit;
  font-size: .9rem;
}
textarea.field { line-height: 1.5; resize: vertical; }
.field.mono { font-family: var(--font-mono); font-size: .84rem; }
.field:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }
.field::placeholder { color: var(--ink-soft); opacity: .7; }

code.mono, code {
  font-family: var(--font-mono); font-size: .84em;
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--radius-sm); padding: 1px 5px;
}

/* ---------- sortable / filterable tables ---------- */
.table-tools {
  display: flex; align-items: center; gap: var(--space-3);
  margin-bottom: var(--space-3); flex-wrap: wrap;
}
.table-filter { max-width: 320px; min-height: 38px; padding: 0 var(--space-3); }
.table-count { font-size: .78rem; color: var(--ink-soft); }

th.sortable { cursor: pointer; user-select: none; position: relative; padding-right: 22px; }
th.sortable:hover { color: var(--ink); }
th.sortable:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }

/* The indicator sits in reserved space so sorting never shifts the layout. */
th.sortable::after {
  content: "↕"; position: absolute; right: var(--space-2); top: 50%;
  translate: 0 -50%; opacity: .35; font-size: .8em;
  transition: opacity var(--motion-fast) var(--ease-standard);
}
th[aria-sort="ascending"]::after  { content: "↑"; opacity: 1; color: var(--primary); }
th[aria-sort="descending"]::after { content: "↓"; opacity: 1; color: var(--primary); }

.table-no-match { text-align: center; color: var(--ink-soft); padding: var(--space-5); }

/* Source-document link on a price: the whole point of the provenance chain. */
.source-link {
  display: inline-flex; align-items: center; gap: 4px;
  color: var(--ink-soft); text-decoration: none;
  border-radius: var(--radius-sm); padding: 2px 4px;
  transition: color var(--motion-fast) var(--ease-standard),
              background var(--motion-fast) var(--ease-standard);
}
.source-link:hover { color: var(--primary); background: var(--panel); }
.source-link:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }
.source-link svg { width: 14px; height: 14px; }

/* ---------- forms ---------- */
.field-row {
  display: grid; grid-template-columns: 120px 1fr;
  gap: var(--space-3); align-items: center;
  margin-bottom: var(--space-3);
}
.field-row > label { font-size: .85rem; color: var(--ink-soft); font-weight: 600; }
.checkline { display: flex; align-items: center; gap: var(--space-2); font-size: .88rem; }
.checkline input { width: 18px; height: 18px; accent-color: var(--primary); }
.sheet-foot {
  display: flex; align-items: center; gap: var(--space-4);
  margin-top: var(--space-5); padding-top: var(--space-4);
  border-top: 1px solid var(--border); flex-wrap: wrap;
}
@media (max-width: 560px) {
  .field-row { grid-template-columns: 1fr; gap: var(--space-1); }
}

/* ---------- popover sheet (browser-native modal) ---------- */
.sheet {
  border: 1px solid var(--border); border-radius: var(--radius-lg);
  background: var(--surface); color: var(--ink);
  box-shadow: var(--elevation-pop);
  padding: 0; width: min(560px, calc(100vw - 32px)); max-height: 88vh;
  overflow: auto;
}
.sheet-body { padding: var(--space-5); }
.sheet::backdrop { background: rgb(3 0 32 / .45); backdrop-filter: blur(2px); }

/* Entrance only. Light is additive — the sheet arrives, it does not fade in
   from invisible, and it rests in its final state under reduced-motion. */
.sheet:popover-open { animation: sheet-in var(--motion-moderate) var(--ease-entrance); }
@keyframes sheet-in {
  from { opacity: .6; translate: 0 8px; scale: .99; }
  to   { opacity: 1;  translate: 0 0;   scale: 1; }
}

/* ---------- segmented control ---------- */
.segmented {
  display: inline-flex; background: var(--panel);
  border: 1px solid var(--border); border-radius: var(--radius-pill);
  padding: 3px; gap: 2px;
}
.segmented a {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 7px var(--space-4); border-radius: var(--radius-pill);
  font-size: .85rem; font-weight: 600; text-decoration: none;
  color: var(--ink-soft); white-space: nowrap;
  transition: background var(--motion-fast) var(--ease-standard),
              color var(--motion-fast) var(--ease-standard);
}
.segmented a:hover { color: var(--ink); }
.segmented a[aria-current="true"] {
  background: var(--surface); color: var(--deep);
  box-shadow: var(--elevation-card);
}
.segmented a:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.segmented .mono { font-size: .78rem; opacity: .75; }

.notice {
  background: var(--panel); border: 1px solid var(--border);
  border-left: 3px solid var(--primary);
  border-radius: var(--radius-md); padding: var(--space-3) var(--space-4);
  font-size: .88rem; color: var(--ink-soft); margin: 0 0 var(--space-4);
}

/* ---------- source document viewer ---------- */
.viewer-layout {
  display: grid; gap: var(--space-4);
  grid-template-columns: minmax(0, 2fr) minmax(280px, 1fr);
  align-items: start;
}
@media (max-width: 900px) { .viewer-layout { grid-template-columns: 1fr; } }

.viewer-bar {
  display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--border); background: var(--panel);
  flex-wrap: wrap;
}
.viewer { padding: var(--space-4); }
.page-frame { position: relative; display: inline-block; max-width: 100%; }
.page-frame img {
  display: block; max-width: 100%; height: auto;
  border: 1px solid var(--border); border-radius: var(--radius-md);
}
.viewer-note { font-size: .82rem; color: var(--ink-soft); margin: var(--space-3) 0 0; max-width: 60ch; }

/* The highlight marks ONE occurrence — the SKU asked about, nothing else. */
.hl {
  position: absolute; pointer-events: none;
  left: calc(var(--l) * 100%); top: calc(var(--t) * 100%);
  width: calc(var(--w) * 100%); height: calc(var(--h) * 100%);
  border: 2px solid var(--accent); border-radius: 3px;
  background: color-mix(in srgb, var(--accent) 22%, transparent);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 20%, transparent);
  animation: hl-arrive var(--motion-slow) var(--ease-expressive) both;
}
@keyframes hl-arrive {
  from { opacity: 0; scale: 1.35; }
  to   { opacity: 1; scale: 1; }
}

dl.facts { display: grid; grid-template-columns: auto 1fr; gap: var(--space-2) var(--space-4); margin: 0; }
dl.facts dt { font-size: .78rem; color: var(--ink-soft); font-weight: 600; }
dl.facts dd { margin: 0; font-size: .88rem; word-break: break-word; }

/* ---------- wide table: horizontal scroll, sticky first column ---------- */
table.wide { min-width: 1080px; }
table.wide th:first-child, table.wide td:first-child {
  position: sticky; left: 0; z-index: 1; background: var(--surface);
}
table.wide tbody tr:hover td:first-child { background: var(--panel); }
table.wide th:first-child { background: var(--surface); }
.table-wrap { overflow-x: auto; scrollbar-width: thin; }

.linklike {
  background: none; border: 0; padding: 0; font: inherit;
  color: var(--deep); cursor: pointer; text-align: left;
  text-decoration: underline; text-decoration-color: var(--border);
  text-underline-offset: 3px;
  transition: text-decoration-color var(--motion-fast) var(--ease-standard);
}
.linklike:hover { text-decoration-color: var(--primary); }
.linklike:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; border-radius: 3px; }

/* ---------- side pane ---------- */
.pane {
  position: fixed; top: 0; right: 0; bottom: 0; z-index: 50;
  width: min(400px, 92vw);
  background: var(--surface); border-left: 1px solid var(--border);
  box-shadow: var(--elevation-pop);
  display: flex; flex-direction: column;
  translate: 100% 0;
  transition: translate var(--motion-moderate) var(--ease-standard);
}
.pane.open { translate: 0 0; }
.pane-head {
  display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-4); border-bottom: 1px solid var(--border);
  min-height: var(--topbar-h);
}
.pane-head .spacer { flex: 1; }
.pane-body { overflow-y: auto; flex: 1; }
.pane-section {
  padding: var(--space-4);
  border-bottom: 1px solid var(--border);
}
.pane-scrim {
  position: fixed; inset: 0; z-index: 45; border: 0; padding: 0;
  background: rgb(3 0 32 / .35);
  animation: scrim-in var(--motion-fast) var(--ease-entrance);
}
@keyframes scrim-in { from { opacity: 0; } to { opacity: 1; } }

/* A chip used as a link must not inherit link underlines — it is a badge. */
a.chip { text-decoration: none; }
a.chip:hover { filter: brightness(.94); }
a.chip:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

/* Prose columns need room. Without a floor, a narrow column collapses to one
   word per line — found in the update-requests table during a visual pass. */
td.prose, th.prose { min-width: 18ch; }
td.tight, th.tight { white-space: nowrap; }

/* Only scroll horizontally when the content actually overflows. `auto` on a
   grid child can reserve a scrollbar it never needs; min-width:0 lets the wrap
   shrink to its container first. */
.table-wrap { min-width: 0; }

/* The decide control stacks so the action column never clips the buttons —
   found during a visual pass after the prose columns were widened. */
.decide { display: grid; gap: var(--space-2); min-width: 190px; }
.decide .field { min-height: 34px; padding: 0 var(--space-2); font-size: .8rem; }
.decide-actions { display: flex; gap: var(--space-2); }

/* EMPHASIS RULE (fixes AOS-3).
   --primary is the leading colour in BOTH themes: blue on light, Verde on dark.
   So the recommended action on any screen is always .btn-primary, and it stays
   the loudest thing whichever theme the viewer is in.

   .btn-signature is the Verde-on-light signature from DESIGN.md, RATIONED to
   the revenue spine — a quote, a price the counter is about to give. It is not
   a generic "important" button, which is how it drifted onto the export page. */

/* A draft is a fixed-width column layout: QTY, CATALOG, EACH, TOTAL only line
   up if the lines are never wrapped. Scroll sideways instead (fixes AOS-4). */
textarea.draft {
  white-space: pre;
  overflow-x: auto;
  line-height: 1.45;
  font-size: .78rem;
}

/* The draft is the output that matters, so it gets the wider half. */
.draft-layout { grid-template-columns: minmax(280px, 2fr) minmax(340px, 3fr); }
@media (max-width: 900px) { .draft-layout { grid-template-columns: 1fr; } }

/* A declared N/A and a real gap must never look the same (fixes AOS-5).
   N/A is quiet and dashed — an exemption on the record. A gap is amber — work
   nobody has done yet. */
.chip-na {
  color: var(--ink-soft); border-style: dashed; background: transparent;
  font-style: italic;
}
.chip-gap { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 35%, var(--border)); }

/* A card that represents one thing should be clickable anywhere on it, but the
   link must still be a real link for keyboard and screen-reader users. The
   stretched pseudo-element covers the card; the anchor keeps its own focus ring
   on the title text where it belongs. (Fixes AOS-6.) */
.vendor-card { position: relative; transition: box-shadow var(--motion-fast) var(--ease-standard); }
.vendor-card:hover { box-shadow: var(--elevation-pop); }
.vendor-card:has(a.stretched:focus-visible) { outline: 2px solid var(--primary); outline-offset: 2px; }
a.stretched { text-decoration: none; }
a.stretched::after { content: ""; position: absolute; inset: 0; }
a.stretched:focus-visible { outline: none; text-decoration: underline; }

.card-go {
  margin: var(--space-4) 0 0; font-size: .84rem; font-weight: 600;
  color: var(--primary);
}

/* ---------- Ananda-carried rules (D35, D39, D42) ---------- */

/* D35 — one spacing step. The container owns the gap; children own no margin. */
.form-stack { display: grid; gap: var(--gap-form); }
.form-stack > * { margin: 0; }

/* D39 — connected controls align as a unit. */
.control-row {
  display: flex; align-items: stretch; gap: var(--space-2);
  flex-wrap: wrap; min-width: 0;
}
.control-row > * { min-width: 0; }
.control-row .field, .control-row .btn { min-height: var(--control-h); }

.control-group { display: flex; align-items: stretch; }
.control-group > *:not(:first-child) { border-top-left-radius: 0; border-bottom-left-radius: 0; }
.control-group > *:not(:last-child) { border-top-right-radius: 0; border-bottom-right-radius: 0; margin-right: -1px; }

/* D42 — text is never hard-clipped.
   Truncation is always ellipsis + a title attribute, so the full value is one
   hover away and never simply lost. Every flex/grid child gets min-width:0,
   because without it a long word forces the whole row wider instead. */
.truncate {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  min-width: 0; display: block; max-width: 100%;
}
td .truncate { max-width: 34ch; }
.grid > *, .page-head > *, .card > * { min-width: 0; }

/* "+N" for a set that does not fit, rather than a silent cut. */
.overflow-count {
  display: inline-flex; align-items: center;
  font-family: var(--font-mono); font-size: .72rem; font-weight: 600;
  color: var(--ink-soft); background: var(--panel);
  border: 1px solid var(--border); border-radius: var(--radius-pill);
  padding: 2px 7px; white-space: nowrap;
}

/* ---------- column confirmation ---------- */
.col-index { font-size: .62rem; color: var(--ink-soft); font-weight: 400; }

/* The chosen columns light up in the sample as you pick them, so the mapping is
   checked against real values rather than against column numbers. */
[data-role="catalog"]     { background: color-mix(in srgb, var(--primary) 12%, transparent); }
[data-role="cost"]        { background: color-mix(in srgb, var(--accent) 18%, transparent); }
[data-role="description"] { background: color-mix(in srgb, var(--ink-soft) 10%, transparent); }
[data-role="stock"]       { background: color-mix(in srgb, var(--warn) 12%, transparent); }
th[data-role] { box-shadow: inset 0 -2px 0 currentColor; }

.preview-line {
  font-family: var(--font-mono); font-size: .82rem;
  color: var(--deep); background: var(--panel);
  border: 1px solid var(--border); border-radius: var(--radius-md);
  padding: var(--space-3); min-height: var(--control-h);
}

.notice-warn {
  border-left-color: var(--warn);
  color: var(--ink);
}
.notice-warn ul { margin: var(--space-2) 0 0; padding-left: 1.1rem; }
.notice-warn li { margin-bottom: var(--space-1); }

/* ═══════════════════════════════════════════════════════════════════════════
   THE SIGNATURE — LIGHT ARRIVING
   Implements DESIGN.md's motion law rather than a generic fade-in.

   The law, quoted: "every section is fully legible from the first frame, and
   scrolling ADDS light — a soft Verde halo blooms around each arriving section
   and headlines catch a luminous sweep. Nothing ever starts dim — light is
   additive, never a gate on legibility."

   That single sentence rules out the usual scroll-reveal pattern, where content
   starts at opacity 0 and animation is a GATE on reading. Here every surface is
   readable at full contrast before any animation runs; motion only adds glow on
   top. Turn the animation off and you lose shine, never information.

   Built on scroll-driven animations (animation-timeline: view()) — a native CSS
   feature, no observer, no JS, no library. Per the DESIGN.md cost ledger that is
   the zero-cost path. Browsers without support simply show the lit end state,
   which is the correct fallback because the end state IS the design.
   ═══════════════════════════════════════════════════════════════════════════ */

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {

    /* A card gains a Verde halo as it travels through the viewport, and settles
       back once it is centred. The halo is a box-shadow, so it never moves the
       card and never triggers layout. */
    .card {
      animation: halo-bloom linear both;
      animation-timeline: view();
      animation-range: entry 10% cover 42%;
    }

    /* Headlines catch a luminous sweep — a gradient clipped to the text, so the
       shine travels across the letters themselves. Colour is inherited at both
       ends, so the text is never lighter than its token value. */
    .content h1 {
      background: linear-gradient(
        100deg,
        currentColor 0%, currentColor 38%,
        color-mix(in srgb, var(--accent) 62%, currentColor) 50%,
        currentColor 62%, currentColor 100%
      );
      background-size: 260% 100%;
      background-position: 190% 0;
      -webkit-background-clip: text;
      background-clip: text;
      /* Without this the gradient paints BEHIND an opaque text fill and is
         never seen — the sweep looks implemented and does nothing. Safe here
         because both ends of the gradient are currentColor: the letters keep
         their exact token colour, and only a brighter band travels across.
         Legibility never drops, which is the whole law. */
      -webkit-text-fill-color: transparent;
      animation: headline-sweep var(--motion-slower) var(--ease-expressive) both;
      animation-delay: var(--motion-quick);
    }

    /* A stat that matters gets one slow ambient breath on arrival, at the
       ambient token — never a loop that keeps pulling the eye. */
    .stat .value { animation: value-lift var(--motion-slow) var(--ease-entrance) both; }
  }
}

@keyframes halo-bloom {
  from {
    box-shadow: var(--elevation-card);
  }
  to {
    box-shadow:
      var(--elevation-card),
      0 0 0 1px color-mix(in srgb, var(--accent) 22%, transparent),
      0 8px 42px -10px color-mix(in srgb, var(--accent) 34%, transparent);
  }
}

@keyframes headline-sweep {
  from { background-position: 190% 0; }
  to   { background-position: -30% 0; }
}

@keyframes value-lift {
  from { opacity: .72; translate: 0 4px; }
  to   { opacity: 1;   translate: 0 0; }
}

/* Stock-ready dot: "Verde dot + glow" (DESIGN.md components.stock-line).
   The glow breathes at the ambient token because it reports a live condition —
   this is the one loop in the system, and it stops for reduced-motion. */
.chip-ok .dot { box-shadow: 0 0 0 0 color-mix(in srgb, var(--ok) 60%, transparent); }
@media (prefers-reduced-motion: no-preference) {
  .chip-ok .dot {
    animation: ready-glow var(--motion-duration-ambient) var(--ease-standard) infinite;
  }
}
@keyframes ready-glow {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--ok) 55%, transparent); }
  50%      { box-shadow: 0 0 0 4px color-mix(in srgb, var(--ok) 0%, transparent); }
}

/* Nav travel: the Verde rail slides to the current item rather than blinking. */
.nav a[aria-current="page"]::before {
  animation: rail-arrive var(--motion-moderate) var(--ease-expressive) both;
}
@keyframes rail-arrive {
  /* The rail is wayfinding, not decoration, so it is PRESENT from the first
     frame and only grows into place. Starting it invisible would make the
     animation the thing that tells you where you are. */
  from { height: 6px; opacity: .55; }
  to   { height: 20px; opacity: 1; }
}

/* A row lights under the pointer instead of merely greying. */
tbody tr { transition: background var(--motion-fast) var(--ease-standard),
                       box-shadow var(--motion-fast) var(--ease-standard); }
tbody tr:hover {
  background: var(--panel);
  box-shadow: inset 3px 0 0 color-mix(in srgb, var(--accent) 70%, transparent);
}

/* Buttons carry a brief lift on hover — additive light, never a size change
   that would nudge neighbouring controls (D39: controls align as a unit). */
.btn-primary, .btn-signature {
  transition: filter var(--motion-instant) var(--ease-standard),
              translate var(--motion-instant) var(--ease-standard),
              box-shadow var(--motion-quick) var(--ease-standard);
}
.btn-primary:hover  { box-shadow: 0 4px 18px -6px color-mix(in srgb, var(--primary) 70%, transparent); }
.btn-signature:hover { box-shadow: 0 4px 18px -6px color-mix(in srgb, var(--accent) 75%, transparent); }

/* ── REDUCED MOTION ──────────────────────────────────────────────────────────
   A duration reset does NOT stop a scroll-driven timeline, so every one is
   turned off explicitly (DESIGN.md motion.reduced-motion). Each surface rests in
   its finished, fully legible state — which is why nothing here starts dim. */
@media (prefers-reduced-motion: reduce) {
  .card, .content h1, .stat .value, .chip-ok .dot,
  .nav a[aria-current="page"]::before {
    animation: none !important;
    animation-timeline: none !important;
  }
  .content h1 {
    background: none !important;
    -webkit-background-clip: initial !important;
    background-clip: initial !important;
    -webkit-text-fill-color: currentColor !important;
    color: var(--deep);
  }
  tbody tr:hover { box-shadow: none; }
}

/* ---------- save confirmation ----------
   A save that redirects in silence leaves you wondering whether the click
   registered. This is a real element rendered by the server, so it works with
   JS off; only the fade-away is CSS, and reduced-motion keeps it on screen
   rather than making it vanish faster than it can be read. */
.toast {
  position: fixed; z-index: 60;
  left: 50%; translate: -50% 0; top: calc(var(--topbar-h) + var(--space-4));
  display: flex; align-items: center; gap: var(--space-3);
  max-width: min(520px, calc(100vw - 32px));
  padding: var(--space-3) var(--space-5);
  background: var(--surface-hero); color: var(--hero-ink);
  border: 1px solid var(--hero-border); border-radius: var(--radius-pill);
  box-shadow: var(--elevation-pop);
  font-size: .9rem; font-weight: 600;
}
.toast svg { width: 18px; height: 18px; color: var(--verde); flex-shrink: 0; }
.toast span { min-width: 0; }

@media (prefers-reduced-motion: no-preference) {
  .toast { animation: toast-arrive var(--motion-moderate) var(--ease-expressive) both,
                      toast-leave var(--motion-slow) var(--ease-exit) 4.4s both; }
}
@keyframes toast-arrive {
  from { opacity: .4; translate: -50% -10px; }
  to   { opacity: 1;  translate: -50% 0; }
}
@keyframes toast-leave {
  from { opacity: 1; translate: -50% 0; }
  to   { opacity: 0; translate: -50% -8px; visibility: hidden; }
}

/* Language switch — two plain links, so it works with JS off and a person can
   bookmark the page in their own language. */
.lang-switch {
  display: inline-flex; gap: 2px; padding: 2px;
  background: var(--hero-elevated); border-radius: var(--radius-pill);
  border: 1px solid var(--hero-border);
}
.lang-switch a {
  font-family: var(--font-mono); font-size: .68rem; font-weight: 600;
  letter-spacing: .04em; text-decoration: none;
  color: var(--hero-ink-soft); padding: 3px 8px;
  border-radius: var(--radius-pill);
  transition: background var(--motion-fast) var(--ease-standard),
              color var(--motion-fast) var(--ease-standard);
}
.lang-switch a:hover { color: var(--hero-ink); }
.lang-switch a[aria-current="true"] { background: var(--verde); color: var(--accent-ink); }
.lang-switch a:focus-visible { outline: 2px solid var(--verde); outline-offset: 2px; }

/* ═══════════════════════════════════════════════════════════════════════════
   SIDE PANE EDITING
   Gordon: "As much editing as possible should be able to happen in a side pane
   overlay that pops up instead of redirecting to a whole new page. The Main
   table needs to be both horizontal and vertical scrollable when the side pane
   appears."

   The objection is to LOSING YOUR PLACE, not to panes. So the table stays
   exactly where it was — same scroll position, same row under the cursor — and
   the pane opens beside it. Both scrollbars keep working, because the reason to
   keep the table visible is to keep reading it.
   ═══════════════════════════════════════════════════════════════════════════ */

/* While a pane is open the content column narrows rather than being covered, so
   the table is still readable and still scrollable in both directions. */
@media (min-width: 1100px) {
  .shell.pane-open .content { padding-right: calc(var(--pane-w) + var(--space-5)); }
  .shell.pane-open .table-wrap { max-height: calc(100vh - var(--topbar-h) - 220px); overflow: auto; }
}
:root { --pane-w: 400px; }

/* Below that the pane covers, because there is not room for both — and a scrim
   makes it obvious the table is behind rather than gone. */
@media (max-width: 1099px) {
  .shell.pane-open .pane-scrim { display: block; }
}

/* A sticky header keeps the column names visible while the body scrolls. */
.table-wrap thead th {
  position: sticky; top: 0; z-index: 2;
  background: var(--surface);
  box-shadow: inset 0 -1px 0 var(--border);
}
table.wide thead th:first-child { z-index: 3; }

.pane-form { padding: var(--space-4); }
.pane-foot {
  position: sticky; bottom: 0;
  padding: var(--space-4);
  background: var(--surface);
  border-top: 1px solid var(--border);
  display: flex; gap: var(--space-2); align-items: center;
}

/* ---------- hover tooltip ----------
   Gordon: "On-hover Tooltips would be helpful for Vendors/Reps with Contact
   Information/Addresses etc in the table. Clicking would again open the side
   pane."

   Hover ANSWERS the question; click COMMITS to the pane. The tooltip is a real
   element rather than a title attribute, because a title cannot show a phone
   number you can click, and it appears after a delay so it never flashes past
   while somebody is simply moving the pointer across the table. */
.has-tip { position: relative; }
.tip {
  position: absolute; z-index: 40;
  left: 0; top: calc(100% + 6px);
  min-width: 220px; max-width: 300px;
  padding: var(--space-3);
  background: var(--surface-hero); color: var(--hero-ink);
  border: 1px solid var(--hero-border); border-radius: var(--radius-md);
  box-shadow: var(--elevation-pop);
  font-size: .82rem; line-height: 1.45;
  opacity: 0; visibility: hidden; translate: 0 -4px;
  transition: opacity var(--motion-fast) var(--ease-standard),
              translate var(--motion-fast) var(--ease-standard),
              visibility var(--motion-fast);
  transition-delay: 0s;
  pointer-events: none;
}
.has-tip:hover .tip, .has-tip:focus-within .tip {
  opacity: 1; visibility: visible; translate: 0 0;
  /* A delay so it does not flash past while crossing the table. */
  transition-delay: 380ms;
  pointer-events: auto;
}
.tip dt { color: var(--hero-ink-soft); font-size: .7rem; text-transform: uppercase;
          letter-spacing: .06em; }
.tip dd { margin: 0 0 var(--space-2); }
.tip a { color: var(--verde); }
.tip .tip-foot { color: var(--hero-ink-soft); font-size: .74rem;
                 border-top: 1px solid var(--hero-border);
                 padding-top: var(--space-2); margin-top: var(--space-2); }

@media (prefers-reduced-motion: reduce) {
  .tip { transition: none; }
  .has-tip:hover .tip, .has-tip:focus-within .tip { transition-delay: 0s; }
}

/* A touch device has no hover, so the tooltip never appears there and the tap
   goes straight to the pane — which is the better interaction anyway. */
@media (hover: none) {
  .tip { display: none; }
}

/* ---------- inline cell editing ----------
   Editable where it is read. The affordance is quiet — a dotted underline that
   firms up on hover — because a table of 500 rows covered in edit buttons is
   harder to read, and reading is what the table is for. */
td.editable { cursor: text; position: relative; }
td.editable::after {
  content: ""; position: absolute; left: var(--space-3); right: var(--space-3);
  bottom: 6px; border-bottom: 1px dashed transparent;
  transition: border-color var(--motion-fast) var(--ease-standard);
}
td.editable:hover::after, td.editable:focus-visible::after {
  border-bottom-color: var(--muted);
}
td.editable:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }

.cell-input {
  width: 100%; min-height: 30px; padding: 2px var(--space-2);
  font-size: .88rem; border-radius: var(--radius-sm);
}
.cell-saving { color: var(--ink-soft); }
td.cell-saved   { background: color-mix(in srgb, var(--ok) 14%, transparent); }
td.cell-failed  { background: color-mix(in srgb, var(--alert) 14%, transparent); }
td.cell-saved, td.cell-failed { transition: background var(--motion-slow) var(--ease-exit); }

/* ---------- reorderable list ----------
   Drag to rearrange, Alt+arrows if a drag is impractical — one-handed, on a
   trackpad, or with a screen reader. A feature that exists for convenience must
   not be the one nobody can reach. */
ul.reorder { list-style: none; margin: 0; padding: 0; }
ul.reorder.saving { opacity: .6; }
ul.reorder.saved .reorder-row { border-color: color-mix(in srgb, var(--ok) 45%, var(--border)); }

.reorder-row {
  display: flex; align-items: flex-start; gap: var(--space-3);
  padding: var(--space-3);
  border: 1px solid var(--border); border-radius: var(--radius-md);
  background: var(--surface); margin-bottom: var(--space-2);
  cursor: grab;
  transition: border-color var(--motion-fast) var(--ease-standard),
              box-shadow var(--motion-fast) var(--ease-standard);
}
.reorder-row:hover { box-shadow: var(--elevation-card); }
.reorder-row:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.reorder-row.dragging { opacity: .45; cursor: grabbing; }
.reorder-body { flex: 1; min-width: 0; }
.grip { color: var(--muted); flex-shrink: 0; padding-top: 2px; }
.grip svg { width: 16px; height: 16px; }

.row-remove {
  flex-shrink: 0; width: 26px; height: 26px;
  border: 1px solid transparent; border-radius: var(--radius-sm);
  background: none; color: var(--ink-soft);
  font-size: 1.1rem; line-height: 1; cursor: pointer;
  transition: background var(--motion-fast) var(--ease-standard),
              color var(--motion-fast) var(--ease-standard);
}
.row-remove:hover { background: color-mix(in srgb, var(--alert) 12%, transparent); color: var(--alert); }
.row-remove:focus-visible { outline: 2px solid var(--alert); outline-offset: 1px; }

/* ---------- resizable, legible, frozen-first-column tables ----------------

   Gordon, on tables across the app:

     "Column headers should be resizable via dragging the header, but the
      default should be full legibility of the title/content (with ... for
      overly long content with on-hover tooltip revealing full text). Content
      should not be compromised. Horizontal scroll Bar needed for accessing
      full content, with Product Number Sticky/Frozen on the Left for context."

   Four separate promises, and they pull against each other:

   1. FULL LEGIBILITY BY DEFAULT. Columns size to their content, so nothing is
      cut off until it is genuinely long. That is why cells do not wrap: a
      wrapped cell is legible but destroys the scan down a column, which is the
      only reason to use a table at all.
   2. NOTHING HARD-CLIPPED. Past a generous cap, content ellipses and keeps its
      full text in a tooltip (Ananda D42). A clipped word with no way to read it
      is the failure this rules out.
   3. THE FULL WIDTH IS REACHABLE. The wrapper scrolls sideways rather than the
      page, so the rail and the header stay put.
   4. CONTEXT SURVIVES THE SCROLL. The first column — the product number, the
      name, whatever identifies the row — freezes to the left, because a row of
      numbers with no idea what they belong to is worse than no table. */

.table-wrap {
  overflow-x: auto;
  scrollbar-width: thin;
  /* Sticky children need a scroll container that is not also a clip on the
     block axis, or the header shadow gets cut. */
  overscroll-behavior-x: contain;
}

.table-wrap table { table-layout: auto; }
/* Once a column has been dragged, explicit widths take over completely —
   auto layout would keep negotiating and the drag would feel like it slipped. */
/* No `min-width: 100%` here, deliberately. With it, a table forced to fill its
   container redistributes any width freed by narrowing one column back across
   the others — so dragging a column 140px narrower moved it only 58px and the
   drag felt broken. The table is now exactly as wide as its columns say. */
.table-wrap table.resized { table-layout: fixed; width: max-content; }

.table-wrap th,
.table-wrap td {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  /* The cap, not a fixed width: short content still sizes to itself. Long
     content ellipses here and carries its full text in a title. */
  max-width: 44ch;
}

/* Cells that are all number stay narrow and right-aligned so a column of money
   reads down the decimal point. */
.table-wrap td.num,
.table-wrap th.num { text-align: right; font-variant-numeric: tabular-nums; }

/* ---- the frozen identity column ---- */
.table-wrap th:first-child,
.table-wrap td:first-child {
  position: sticky;
  left: 0;
  z-index: 2;
  background: var(--panel);
}
.table-wrap thead th:first-child { z-index: 3; background: var(--surface); }
.table-wrap tbody tr:hover td:first-child { background: var(--surface); }

/* The edge that tells you the column is pinned rather than merely first. It
   only appears once there is something hidden to the left. */
.table-wrap th:first-child::before,
.table-wrap td:first-child::before {
  content: "";
  position: absolute;
  inset: 0 -1px 0 auto;
  width: 1px;
  background: var(--border);
}

/* ---- the drag handle ---- */
.table-wrap th { position: relative; }
.col-grip {
  position: absolute;
  top: 0;
  right: 0;
  width: 9px;
  height: 100%;
  cursor: col-resize;
  /* Sits above the sort target so a drag never fires a sort. */
  z-index: 4;
  touch-action: none;
}
.col-grip::after {
  content: "";
  position: absolute;
  top: 22%;
  bottom: 22%;
  right: 3px;
  width: 2px;
  border-radius: 1px;
  background: transparent;
  transition: background var(--motion-quick, 120ms) ease;
}
.col-grip:hover::after,
.col-grip.dragging::after { background: var(--primary); }

/* While dragging, the whole document takes the resize cursor and stops
   selecting text — otherwise the pointer flickers and the drag selects the
   table contents behind it. */
body.col-resizing { cursor: col-resize; user-select: none; }

/* A column dragged narrower must still be readable as a column: below this it
   is a sliver that cannot show even an ellipsis. */
.table-wrap col { min-width: 4ch; }

@media (max-width: 720px) {
  /* Freezing a column is worth less than the width it costs on a phone. */
  .table-wrap th:first-child,
  .table-wrap td:first-child { position: static; }
  .table-wrap th, .table-wrap td { max-width: 22ch; }
}

/* ---------- priced draft tables ---------- */
.draft-table:empty { display: none; }
.draft-table-card:has(.draft-table:empty) { display: none; }

.draft-table tfoot td {
  border-top: 2px solid var(--border);
  font-weight: 600;
  background: var(--panel);
}

/* A markup that came from no rule, and a price the markup did not decide.
   Both are marked because both change what margin somebody is agreeing to,
   and neither is visible in the number alone. */
.draft-table td.is-default { color: var(--ink-soft); font-style: italic; }
.draft-table td.is-bound::after {
  content: "*";
  color: var(--warn);
  font-weight: 700;
}
.draft-table tr.line-stale td { background: color-mix(in srgb, var(--warn) 12%, transparent); }
.draft-table tr.line-problem td { color: var(--ink-soft); }
.draft-table tr.line-problem td:first-child { color: var(--alert); }

/* The priced table gets the whole width, not half of it.

   Seven columns inside a half-width card meant Price and Total sat past the
   card edge and the footer total was off-screen entirely — a table you have to
   scroll to find the total in is a table that will be read wrong. The input and
   the paste-text stay side by side above it; the thing you CHECK gets the room.
*/
.draft-table-card { grid-column: 1 / -1; margin-top: var(--space-5); }
.draft-table-card .table-wrap { max-width: 100%; }

/* Why a line has no price, on the line itself. A cost with no price looks the
   same as missing data, and the two want opposite responses: chase the vendor,
   or accept this cannot be sold at a profit. */
.draft-table .row-why {
  display: block;
  font-size: .74rem;
  color: var(--alert);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* How close this order is to free freight. Both numbers were already on screen,
   on two different pages, and nobody was putting them together. */
.freight-note {
  margin-top: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm, 6px);
  font-size: .86rem;
  font-weight: 600;
}
.freight-note.is-free { background: color-mix(in srgb, var(--ok) 14%, transparent); color: var(--ok); }
.freight-note.is-short { background: color-mix(in srgb, var(--warn) 14%, transparent); color: var(--warn); }

/* ---------- transcripts: three colours, two owners -----------------------

   Gordon: "Red = outstanding question; Yellow = AI Unclarity; Green = Answered.
   Highlights can be toggle on/off. Maybe AI Unclarity is a highlight, the rest
   are font color."

   Yellow is a HIGHLIGHT because it is a property of the WORDS — the machine is
   not sure it read those characters. Red and green are FONT COLOUR because they
   are the state of a conversation about those words, not of the words. Marking
   both the same way would say they are the same kind of fact. */

.transcript-layout { grid-template-columns: minmax(280px, 2fr) minmax(360px, 3fr); }
@media (max-width: 980px) { .transcript-layout { grid-template-columns: 1fr; } }

.transcript-original {
  width: 100%;
  border-radius: var(--radius-sm, 6px);
  border: 1px solid var(--border);
  /* Checking a reading means looking closely at the original. */
  cursor: zoom-in;
}

.transcript { list-style: none; margin: 0; padding: 0; }
.tline {
  display: flex;
  gap: var(--space-3);
  padding: var(--space-3) 0;
  border-top: 1px solid var(--border);
}
.tline:first-child { border-top: 0; }
.tline-no { color: var(--ink-soft); font-size: .78rem; padding-top: 2px; }
.tline-body { flex: 1; min-width: 0; }
.tline-text { margin: 0; word-break: break-word; }

/* YELLOW — the machine's doubt, on the words themselves. */
mark.unsure {
  background: color-mix(in srgb, var(--warn) 34%, transparent);
  color: inherit;
  padding: 0 2px;
  border-radius: 3px;
}
/* Toggled off, the words stay exactly where they were — only the paint goes.
   Hiding the text would change what the transcript SAYS, which is the one
   thing a transcript may never do. */
.hide-unsure mark.unsure { background: transparent; }

.tcomment {
  margin-top: var(--space-2);
  padding-left: var(--space-3);
  border-left: 2px solid currentColor;
  font-size: .86rem;
}
/* RED — a question nobody has answered. */
.tcomment.is-open { color: var(--alert); }
/* GREEN — the same question, once somebody has. */
.tcomment.is-answered { color: var(--ok); }
.tcomment-body, .tcomment-answer { margin: 0 0 2px; }
.tcomment-quote { color: var(--ink-soft); font-size: .8rem; }

.tline-ask, .tcomment-reply {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-2);
  flex-wrap: wrap;
}
.tline-ask .field, .tcomment-reply .field { min-height: 32px; font-size: .84rem; flex: 1 1 8rem; }

/* A line with an open question earns a mark in the margin — otherwise finding
   the three reds in forty lines is a scroll-and-squint job. */
.tline.has-open { box-shadow: inset 3px 0 0 var(--alert); padding-left: var(--space-3); }
.tline.all-answered { box-shadow: inset 3px 0 0 var(--ok); padding-left: var(--space-3); }

.legend { display: flex; gap: var(--space-3); font-size: .76rem; color: var(--ink-soft); }
.legend-item { display: inline-flex; align-items: center; gap: 4px; }
.swatch { width: 10px; height: 10px; border-radius: 2px; display: inline-block; }
.swatch.is-open { background: var(--alert); }
.swatch.is-answered { background: var(--ok); }

.share-list { list-style: none; margin: var(--space-3) 0 0; padding: 0; }
.share-list li {
  display: flex; align-items: center; gap: var(--space-2);
  flex-wrap: wrap; padding: var(--space-2) 0;
  border-top: 1px solid var(--border);
}
.share-token { font-size: .74rem; overflow: hidden; text-overflow: ellipsis; max-width: 18ch; }
.share-list li.is-revoked { opacity: .55; text-decoration: line-through; }

.correct-block { margin-top: var(--space-5); }
.correct-block summary { cursor: pointer; font-weight: 600; }

/* ---------- the layout for people outside Alliance ----------------------

   A shared transcript is a link emailed to a customer. Rendered in the internal
   layout it carried the whole navigation rail with it, so the customer was one
   click from Products, Vendors and the PO builder — every cost Alliance pays.
   This base has no rail in it at all, so that cannot come back by somebody
   forgetting a condition. */
body.public {
  background: var(--surface);
  color: var(--ink);
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}
.public-head {
  padding: var(--space-4) var(--space-5);
  /* Dark because the delivered logo is white-on-dark artwork and the rule is
     that it is never recoloured. */
  background: var(--deep-bg, #030020);
}
.public-brand { display: inline-flex; align-items: center; }
.public-main {
  flex: 1;
  width: 100%;
  max-width: 60rem;
  margin: 0 auto;
  padding: var(--space-6) var(--space-5);
}
.public-foot {
  padding: var(--space-4) var(--space-5);
  border-top: 1px solid var(--border);
  font-size: .8rem;
  color: var(--ink-soft);
}

/* Show only the lines worth checking. Forty lines with six flagged is a
   scroll-and-squint job; these are the same class-toggle mechanism the
   highlight switch uses, so the page still works with JS off. */
.transcript.only-unsure .tline:not(:has(mark.unsure)) { display: none; }
.transcript.only-questions .tline:not(.has-open):not(.all-answered) { display: none; }
[data-show].is-on { background: var(--panel); color: var(--ink); font-weight: 600; }

/* A probable match is priced, because a quote is a draft either way — but it
   never looks like a certainty. */
.tier-counts { display: flex; gap: var(--space-2); flex-wrap: wrap; }
tr.is-probable td { background: color-mix(in srgb, var(--warn) 8%, transparent); }
.candidate { background: var(--panel); padding: 1px 5px; border-radius: 3px; }

/* Why a line is not ready to price, on the line. A line that names the right
   product can still be unsafe to order — nobody stated a quantity, the list
   counts boxes, the reading was unsure — and none of that shows in the tier. */
.line-caution {
  margin: 2px 0 0;
  font-size: .8rem;
  color: var(--warn);
}

/* A dropped line stays visible, struck through rather than removed — "why
   isn't that one in here" is a question somebody will ask, and a row that
   vanished cannot answer it. */
tr.is-skipped td { opacity: .5; text-decoration: line-through; }
tr.is-skipped td:last-child { text-decoration: none; opacity: 1; }

/* ---------- the shipping route map ---------------------------------------
   Three colours, three meanings, never mixed: the depot, a vendor we collect
   from, a client we deliver to. The numbered list beside the map carries the
   same information in the same order, because a map is an aid and the list is
   the record — a driver with no signal or a printed sheet loses nothing. */
.route-layout { grid-template-columns: minmax(320px, 3fr) minmax(300px, 2fr); }
@media (max-width: 980px) { .route-layout { grid-template-columns: 1fr; } }

.route-map { height: 560px; width: 100%; background: var(--panel); }
@media (max-width: 980px) { .route-map { height: 380px; } }

.route-pin-dot {
  display: flex; align-items: center; justify-content: center;
  width: 100%; height: 100%; border-radius: 50%;
  color: #fff; font-size: .72rem; font-weight: 700;
  border: 2px solid #fff; box-shadow: 0 1px 4px rgb(0 0 0 / .35);
}

.run-list { list-style: none; margin: 0; padding: 0; }
.run-stop {
  display: flex; align-items: flex-start; gap: var(--space-3);
  padding: var(--space-3) 0; border-top: 1px solid var(--border);
}
.run-stop:first-child { border-top: 0; }
.run-no {
  flex: 0 0 auto; width: 24px; height: 24px; border-radius: 50%;
  background: var(--primary); color: var(--primary-ink);
  display: flex; align-items: center; justify-content: center;
  font-size: .74rem; font-weight: 700;
}
/* A stop nobody could place is still a stop. It keeps its number and says why
   it has no pin, rather than quietly not being driven to. */
.run-stop.is-unplaced .run-no { background: var(--warn); color: #1a1200; }

/* Leaflet paints its own controls; keep them inside the card's rounding. */
.route-map .leaflet-control-attribution { font-size: .68rem; }

/* The whole matched address, wrapped. Truncating it cut off the city, state and
   country — which the map service puts at the END, and which is the only part
   that reveals a match in the wrong state. */
.matched-as {
  font-size: .74rem; color: var(--ink-soft);
  display: block; word-break: break-word;
}
.remembered { display: block; font-size: .78rem; color: var(--ok); margin-top: 4px; }
.run-actions { display: flex; align-items: center; gap: var(--space-2); flex-shrink: 0; }
.run-stop.is-done { opacity: .62; }
.run-stop.is-done .run-no { background: var(--ok); color: #04231a; }
.delivered-block { margin-top: var(--space-4); }
.delivered-block summary { cursor: pointer; font-size: .85rem; color: var(--ink-soft); }

/* Who is signed in, and the way out. On a shared floor terminal the absence of
   this is the difference between a shift ending and a session ending. */
.nav-who {
  display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap;
  padding: var(--space-3) var(--space-4);
  border-top: 1px solid rgb(255 255 255 / .08);
  font-size: .8rem;
}
.nav-who-name { color: #fff; font-weight: 600; }
.nav-who-role {
  color: var(--primary); font-size: .7rem; text-transform: uppercase;
  letter-spacing: .04em;
}

/* ---------- the matched part of a search result --------------------------
   Gordon: "And have the 'Br' highlighted in all cases."

   A background rather than a colour change, because the highlight has to
   survive being read at a glance on a warehouse screen — and because colour
   alone would be invisible to anybody who cannot distinguish it. <mark> is
   also announced by screen readers, which a styled <span> is not. */
mark {
  background: var(--mark);
  color: inherit;
  padding: 0 .1em;
  border-radius: 2px;
}

/* ---------- the board ----------------------------------------------------
   Columns that scroll sideways rather than squeezing, because four columns at
   200px each is four unreadable columns. On a phone they stack. */
.board {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(17rem, 1fr);
  gap: var(--space-4);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  padding-bottom: var(--space-3);
  align-items: start;
}
.board-column { min-width: 0; }
.board-card { padding: var(--space-3); margin-bottom: var(--space-3); }
/* A derived card is the system's own knowledge, not somebody's note, and it
   reads differently so nobody wonders why it cannot be dragged. */
.board-card-derived {
  border-style: dashed;
  background: color-mix(in srgb, var(--accent) 5%, transparent);
}
@media (max-width: 720px) {
  .board { grid-auto-flow: row; grid-auto-columns: auto; overflow-x: visible; }
}

/* ── The sign-in box ────────────────────────────────────────────────────────
   The logo goes INSIDE the box (Gordon, 2026-08-24). The delivered artwork is
   white-on-dark and is never recoloured, so the band carries the dark rather
   than the logo carrying a light background it was not drawn for. */
.signin-card {
  max-width: 26rem;
  margin: 3rem auto;
  padding-top: 0;
  overflow: hidden;
}
.signin-brand {
  background: var(--surface-hero);
  margin: 0 calc(-1 * var(--space-5)) var(--space-5);
  padding: var(--space-4) var(--space-5);
  display: flex;
  justify-content: center;
}
.signin-brand img {
  height: 30px;
  width: auto;
  /* A logo that overflows its band on a narrow phone is the first thing
     somebody sees of the company. */
  max-width: 100%;
  object-fit: contain;
}

/* ── Things a thumb has to hit ──────────────────────────────────────────────
   Found 2026-08-24 by driving a phone-sized browser at every page
   (qa-notes/visual-audit.mjs). WCAG 2.2 puts the floor at 24px; these were
   17-23px, which is a miss and a re-tap on a moving forklift.

   The language switch is 1px under on every page in the application, and
   Spanish is not a preference here — it is how half the floor reads the
   screen. A control that is hard to hit is a control somebody stops using. */
.lang-switch a { min-height: 24px; display: inline-flex; align-items: center; }

/* Gated on any-pointer, NOT on width. These were all `max-width: 640px`,
   which is a phone — and the device this matters most on is the warehouse
   tablet at 820px or 1280px, entirely finger-driven, which got none of it.
   A phone number IS the action on a touch screen: "Call Brianna" ends with a thumb on
   a 17px-tall line of text. Only on small screens, because widening the row on
   a desktop table would push columns around for no gain. */
@media (any-pointer: coarse) {
  a[href^="tel:"], a[href^="mailto:"] {
    display: inline-flex;
    align-items: center;
    min-height: 24px;
    /* Widens the target without moving the text: the row keeps its height. */
    padding-block: 2px;
  }
}

/* A link in a table cell is a destination, not a word in a sentence — on a
   phone it is the whole reason the row is there. Search is the clearest case:
   Gordon asked for "Call Brianna" to land on a list of Briannas, and the point
   of that list is tapping one. Table rows already have room, so this costs
   nothing above the fold. */
@media (any-pointer: coarse) {
  td > a, th > a, .hit a, td > * > a {
    display: inline-flex;
    align-items: center;
    min-height: 24px;
  }
}

/* The line under a number on the dashboard is the way IN to the work it is
   counting — "4 stale prices" is a fact, "past their expiry date" is the link
   somebody actually wants. And a button styled as a link is still a button. */
@media (any-pointer: coarse) {
  .stat .foot a, button.linklike {
    display: inline-flex;
    align-items: center;
    min-height: 24px;
  }
}

/* ── The rail has to be findable to the bottom ──────────────────────────────
   Measured 2026-08-24 with a real browser: the rail wants 1083px and gets 714
   on a 1440x900 laptop. Six of nineteen pages were below the fold — Contacts,
   Quotes, PO builder, Who it's for, Shipping route, Who can get in — inside a
   container that scrolled with NO visible scrollbar. Even at 1080p three were
   hidden.

   Silently scrolled is the same as absent: somebody who does not already know
   the PO builder exists has no reason to drag a menu that looks finished. This
   is the "can I find everything" fault, and it hid a quarter of the
   application.

   Two changes, neither of which removes anything:
     1. A mouse does not need a 44px row. That target is for the warehouse
        tablet, so it is kept for coarse pointers and tightened for fine ones.
     2. The scrollbar is made permanently visible on the rail. An overlay
        scrollbar that appears on hover cannot tell you there is more, because
        you have to already suspect there is more to go looking. */
/* `not (any-pointer: coarse)`, NOT `pointer: fine`.
   `pointer` reports the PRIMARY pointer, which on a Windows touchscreen laptop
   is the trackpad — so `pointer: fine` tightened rows to 34px on a machine
   being poked with a finger. `any-pointer: coarse` is true whenever a finger
   is available at all, which is the question actually being asked: is anything
   here going to be touched? The warehouse tablet keeps its 44px rows. */
@media not all and (any-pointer: coarse) {
  .nav a { min-height: 34px; }
  .nav-group + .nav-group { margin-top: var(--space-3); }
  .nav-group-label { padding-bottom: var(--space-1); }
}

.nav {
  scrollbar-width: thin;
  scrollbar-color: var(--hero-border) transparent;
  /* Firefox/Chrome honour the two above; WebKit needs its own. */
  scrollbar-gutter: stable;
}
.nav::-webkit-scrollbar { width: 8px; }
.nav::-webkit-scrollbar-track { background: transparent; }
.nav::-webkit-scrollbar-thumb {
  background: var(--hero-border);
  border-radius: var(--radius-pill);
}
.nav::-webkit-scrollbar-thumb:hover { background: var(--hero-ink-soft); }

/* A stat card is a link now, so it must not inherit link underlines or the
   link colour — it is a card that happens to be clickable, and it has to keep
   showing the number in the colour that means something (red for stale). */
/* display:BLOCK, not flex. The stat card's own children are `display: block`
   (label over number over footnote); making the card a flex container put all
   three side by side and overlapping. */
a.card.stat { text-decoration: none; color: inherit; display: block; }
a.card.stat:hover { border-color: var(--primary); }
a.card.stat:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }


/* ── The microphone in the search box ───────────────────────────────────────
   Written AFTER .btn on purpose, and scoped through .search so it outranks
   .btn-quiet. Both of those come later in this file than the search rules do,
   so the first version of this lost twice: .btn's `padding: 0 16px` left a
   34px button with 2px of room and squeezed the icon to ZERO WIDTH, and
   .btn-quiet's panel grey put a grey circle on a grey pill. The button was
   there, enabled, correctly labelled — and invisible. */
.search .search-mic {
  position: absolute; right: 4px;
  width: 34px; min-height: 34px; height: 34px;
  padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 50%;
  background: transparent; border-color: transparent;
  color: var(--ink-soft);
}
.search .search-mic svg { width: 18px; height: 18px; flex-shrink: 0; }
.search .search-mic:hover:not(:disabled) {
  background: var(--surface); color: var(--ink);
}
.search .search-mic[aria-pressed="true"] { color: var(--primary); }
/* A control that cannot work is worse than no control for somebody who is
   already unsure — dictate.js disables this where the browser cannot listen,
   and a permanently dead grey button in the search box would be the one thing
   they remember. Typing is unaffected either way. */
.search .search-mic:disabled { display: none; }
.search input { padding-right: 42px; }
@media (any-pointer: coarse) {
  .search .search-mic { width: var(--touch-min); height: var(--touch-min); }
  .search input { padding-right: calc(var(--touch-min) + 6px); }
}

/* ── The Alliance mark, in any colour ───────────────────────────────────────
   Gordon, 2026-08-24: "converted into an SVG, and rendered into whatever color
   we want."

   `fill="currentColor"` inside logo-mark.svg only resolves when the SVG is
   INLINE in the page — through an <img> the browser renders it in its own
   document, where currentColor is black. So the general-purpose way to colour
   it is a mask: the shape comes from the file, the colour from `color`.

       <span class="alliance-mark" style="color:var(--verde);width:32px"></span>

   Applies to the MARK only. The full lock-up (mark + wordmark + ELECTRIC
   SUPPLY) is still the delivered PNG, unaltered, per DESIGN.md. */
.alliance-mark {
  display: inline-block;
  aspect-ratio: 207 / 179;
  background-color: currentColor;
  -webkit-mask: url("/static/logo-mark.svg") no-repeat center / contain;
  mask: url("/static/logo-mark.svg") no-repeat center / contain;
}

/* ── The language, on the sign-in card ──────────────────────────────────────
   Named in full — "English" and "Español", not EN and ES — because this is the
   one moment somebody might not be able to read the page they are looking at,
   and a two-letter code is a puzzle rather than an answer. Each option is
   marked `lang` so a screen reader says it in its own language.

   Sized for a thumb on the shared floor terminal. */
.signin-lang {
  display: flex;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}
.signin-lang a {
  flex: 1;
  min-height: var(--touch-min);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--panel);
  color: var(--ink-soft);
  font-size: .86rem;
  font-weight: 600;
  text-decoration: none;
}
.signin-lang a:hover { color: var(--ink); border-color: var(--muted); }
.signin-lang a[aria-current="true"] {
  background: var(--primary);
  border-color: var(--primary);
  color: var(--primary-ink);
}
.signin-lang a:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}

/* A chip you cannot clear is a trap. This is the × beside a due date or an
   assignment — sized for a thumb on the floor terminal, quiet until somebody
   goes for it, because taking something off is not the main action on a card. */
.chip-clear {
  border: 1px solid var(--border);
  background: transparent;
  color: var(--ink-soft);
  cursor: pointer;
  min-height: 24px;
  min-width: 24px;
  padding: 0 6px;
  line-height: 1;
}
.chip-clear:hover { color: var(--alert); border-color: var(--alert); }
.chip-clear:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
@media (any-pointer: coarse) {
  .chip-clear { min-height: var(--touch-min); min-width: var(--touch-min); }
}

/* ════════════════════════════════════════════════════════════════════════════
   ON PAPER

   Gordon, 2026-08-24: "All tables, pages, translations, and anything else you
   can think of should be printable/downloadable."

   Nothing here had a print stylesheet, so every page printed the navigation
   rail down the left, cut every wide table off at the paper's edge, and lost
   whatever a scroll container was holding. A price sheet printed for a
   customer arrived missing its right-hand columns — the ones with the money in
   them.

   The rules that matter on paper and nowhere else:

     * a screen scrolls, paper does not. Every scroll container opens up and
       the table breaks across pages instead of being clipped;
     * a link cannot be clicked. An external address prints beside its text;
     * a BUTTON CANNOT BE PRESSED. Everything that only acts goes, and — the
       one people forget — a TRANSLATION BEHIND A BUTTON MUST PRINT ANYWAY,
       because on paper there is nothing to press and the reader may be the one
       who needs the other language;
     * ink is expensive and the dark rail is a solid block of it.
   ══════════════════════════════════════════════════════════════════════════ */
@media print {
  /* Ink, and legibility on a page that may be photocopied. */
  :root, :root[data-theme="dark"] {
    --surface: #fff; --panel: #fff; --ink: #000; --ink-soft: #333;
    --border: #999; --muted: #999; --deep: #000; --primary: #000;
  }
  body { background: #fff !important; color: #000 !important; }

  /* The chrome. None of it can be used on paper and the rail is a solid
     block of dark ink down every page. */
  /* CONTROLS GO. DATA STAYS, even when it is a button or a link.

     `button` with no exception blanked the Vendor column on every printed
     product — a vendor name is rendered as `button.linklike`, which table.js
     was already careful about for CSV and this was not — and took the counter's
     phone number off the crew's printed page while leaving the sentence
     telling them to ring it. */
  .sidebar, .scrim, .topbar, .nav-btn, .search, .toast,
  .table-tools, .col-grip, .pane, .drawer,
  .lang-switch, #theme-toggle { display: none !important; }
  button:not(.linklike), .btn:not([href^="tel:"]) { display: none !important; }
  .btn[href^="tel:"] {
    border: none !important;
    padding: 0 !important;
    font-weight: 700;
  }

  /* A tooltip cannot be hovered on paper. These are laid out even while
     invisible, and 220px of them hanging off the products table pushed the
     sheet wider than the page — so the table fitted and the paper did not.
     Already hidden on touch devices for the same reason: no hover, no tip. */
  .tip { display: none !important; }

  /* A form cannot be filled in on paper. An empty answer box, a date picker
     and a half-truncated dropdown are three things nobody can use and a lot of
     white space — while what somebody HAS answered is rendered as text beside
     them, and prints. */
  /* The CONTROL cannot go on paper. Its VALUE must.
     Hiding `.field-row` took the label with it, so /confirm printed a preview
     of vendor rows with no mapping on it at all — not even the words saying
     what each column had been set to — and /products/{id}/edit printed its
     heading and nothing else. */
  input, select, textarea { display: none !important; }
  .segmented { display: none !important; }
  .print-value {
    display: inline !important;
    font-family: var(--font-mono);
    font-weight: 600;
  }
  .print-value[data-empty="1"]::after { content: "—"; font-weight: 400; }

  /* WHAT THE PAGE IS FOR STILL PRINTS.
     A read-only draft — the customer copy, the purchase-order text — is a
     textarea, so hiding controls hid the entire point of /quotes and /po while
     the internal cost table beside it printed happily. Alliance's markup on a
     customer's sheet, and the customer's sheet missing. */
  .print-copy {
    display: block !important;
    white-space: pre-wrap;
    font-size: 9pt;
    border: 1px solid var(--border);
    padding: 6px 8px;
  }

  /* And the table with the markup in it does NOT go to a customer. Pressing
     Print on a quote means the quote, not the workings behind it. */
  .draft-table-card { display: none !important; }

  /* The page itself takes the whole sheet. */
  .shell { display: block !important; }
  .main, .content { margin: 0 !important; padding: 0 !important;
                    max-width: none !important; }

  /* A screen scrolls; paper does not. Anything hidden inside a scroll
     container is simply gone from a print, which is how a price sheet loses
     the column with the money in it. */
  .table-wrap, .pane-body, [style*="overflow"] {
    overflow: visible !important;
    max-height: none !important;
    height: auto !important;
  }
  /* THE COLUMN WIDTHS FROM THE SCREEN DO NOT SURVIVE THE PAPER.

     The table is resizable, so its widths live inline on <col> and <th> — set
     for a 1440px window. On a Letter sheet those numbers crushed the headers
     to one letter per line ("V E N D O R") and split $94.00 across three. The
     browser lays the table out for the paper it is on, given the chance. */
  /* `table.wide` carries min-width: 1080px so a broad table SCROLLS rather
     than crushing itself on screen — right there, and exactly wrong on paper,
     where there is nothing to scroll and the overflow is simply cut off. It
     was holding /products, /requests, /vendors and /board 340-380px wider than
     the sheet, which is the last four columns of each gone. */
  table, table.wide {
    width: 100% !important;
    table-layout: auto !important;
    min-width: 0 !important;
  }
  /* A table marked `wide` has more columns than a sheet can hold at any type
     size — products is fifteen. Sharing the width and wrapping is not pretty,
     but every column is ON the paper; the alternative is the last four
     silently over the edge, and on products those are the competitor price,
     the vendor and when it was last confirmed. */
  table.wide { table-layout: fixed !important; }
  table.wide th, table.wide td { overflow-wrap: anywhere; }

  table col, table th, table td {
    width: auto !important;
    min-width: 0 !important;
    max-width: none !important;
  }
  th, td {
    white-space: normal !important;
    /* Truncation is a screen idea. On paper there is no hover to reveal the
       rest, so "PRODU… ID" is simply a column nobody can name. */
    text-overflow: clip !important;
    overflow: visible !important;
  }

  /* A fifteen-column table does not fit a Letter sheet at screen type. Smaller
     type and tighter cells let it wrap into the page instead of running off
     the right-hand edge — which on the products table is where the money is. */
  table { font-size: 8.5pt; }
  th, td { padding: 3px 5px !important; }
  .chip { font-size: 7.5pt !important; padding: 0 4px !important; }
  /* Let every cell break a long token rather than widen the sheet.
     `overflow-wrap: anywhere` only acts when a word genuinely will not fit, so
     ordinary prose still wraps at spaces — but a catalog number, a URL or a
     forty-character note stops being the reason four columns fall off the
     page. Applied only to .mono first, which left /proposals, /ingest and
     /bugs running off by up to 287px. */
  th, td { overflow-wrap: anywhere; }

  /* A row split across two sheets is a row nobody can read, and a header
     that does not repeat leaves the second page a wall of unlabelled
     numbers. */
  tr, .card, article { break-inside: avoid; page-break-inside: avoid; }
  thead { display: table-header-group; }

  .card { border: 1px solid var(--border) !important; box-shadow: none !important; }

  /* A link cannot be clicked, so an external address is worth printing beside
     its words — but NOT INSIDE A TABLE. A competitor-price column full of
     homedepot.com search URLs buried the row it belonged to and pushed the
     money off the sheet. In prose there is room; in a cell there is not. */
  p a[href^="http"]::after,
  dd a[href^="http"]::after,
  li a[href^="http"]::after {
    content: " (" attr(href) ")";
    font-size: .78em;
    overflow-wrap: anywhere;
  }

  /* THE TRANSLATION PRINTS TOO. On screen it is behind a button; on paper
     there is no button, and the person holding the sheet may be exactly the
     one who needs the other language. Both, labelled, rather than whichever
     half happened to be showing when somebody pressed Ctrl+P. */
  /* BOTH HALVES, whichever one somebody left showing.

     Only the translated half was restored — so if the counter had pressed
     Translate to read a stopped-work note and then printed, the sheet carried
     the machine's Spanish and NOT the foreman's actual words. "The original is
     the record" and "a translation says it is one" were both false on paper,
     in exactly the state somebody printing a translated note is in. */
  .either [data-either-original][hidden],
  .either [data-either-other][hidden] {
    display: inline !important;
    visibility: visible !important;
  }
  /* And LABELLED, which the comment claimed and a middot did not do. */
  .either [data-either-other]::before {
    content: " · " attr(data-language) ": ";
    color: var(--ink-soft);
    font-weight: 600;
  }
  details { display: block !important; }
  details > summary { display: none !important; }
  details > *:not(summary) { display: block !important; }
}


/* The printable twin of a read-only draft. Invisible on screen — the textarea
   is what somebody reads and scrolls there — and the only version on paper. */
.print-copy { display: none; }


/* The printable value of a field. Invisible on screen, where the field itself
   is what somebody reads and edits. */
.print-value { display: none; }

/* The eyebrow link above a page title ("Receiving" over a delivery) is the way
   back, and on a touch device it measured 77x17 - under a finger. Gated on the
   pointer, never the width (UX-08). */
@media (any-pointer: coarse) {
  .eyebrow a { display: inline-flex; align-items: center; min-height: 24px; padding-block: 2px; }
}

/* ---------- the receiving bench on a phone ---------- */
/* The camera preview: full width of the card, never taller than a third of the
   screen so the scan box and the button stay in reach of a thumb. */
/* The camera fills the screen (Hanuman, 2026-09-07). */
.scan-overlay {
  position: fixed; inset: 0; z-index: 100; background: #000;
  display: flex; align-items: center; justify-content: center;
}
.scan-open { overflow: hidden; }
.scan-overlay .scan-video {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; background: #000;
}
.scan-guide {
  position: relative; width: min(80vw, 28rem); aspect-ratio: 3 / 2;
  border: 3px solid rgba(255,255,255,.85); border-radius: 12px;
  box-shadow: 0 0 0 100vmax rgba(0,0,0,.35);
}
.scan-hud {
  position: absolute; left: 0; right: 0; bottom: max(1.5rem, env(safe-area-inset-bottom));
  margin: 0; padding: 0 1.5rem; text-align: center; color: #fff; font-size: 1rem;
  text-shadow: 0 1px 3px rgba(0,0,0,.8);
}
.scan-close {
  position: absolute; top: max(.75rem, env(safe-area-inset-top)); right: .75rem;
  width: 3rem; height: 3rem; border: 0; border-radius: 50%;
  background: rgba(0,0,0,.55); color: #fff; font-size: 2rem; line-height: 1; cursor: pointer;
}
.scan-close:focus-visible { outline: 2px solid #fff; }
.scan-busy .scan-video { filter: brightness(.35); }
.scan-busy .scan-guide { visibility: hidden; }
.scan-spinner {
  position: absolute; width: 3.5rem; height: 3.5rem; border-radius: 50%;
  border: 4px solid rgba(255,255,255,.25); border-top-color: #fff;
  animation: scan-spin .9s linear infinite;
}
@keyframes scan-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .scan-spinner { animation-duration: 2.5s; } }
@media (max-width: 560px) {
  /* Scan first. The card that takes the barcode pins to the top so a person
     holding a carton in one hand never scrolls to find where to scan. */
  .scan-card { position: sticky; top: 0; z-index: 3; }
  .scan-card .field { font-size: 1.35rem; }
  /* The three doors of "which item is this?" stack rather than crowd. */
  .control-row { flex-direction: column; align-items: stretch; }
}


/* ── receiving: who delivered it, and the paperwork (2026-09-07) ─────────── */
.pick-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr)); gap: var(--space-2) var(--space-4); }
.pick-grid .checkline { min-height: var(--touch-min, 2.75rem); }
.pick-scroll { max-height: 16rem; overflow-y: auto; padding: var(--space-1) 0; }
.radio-row { display: flex; gap: var(--space-4); flex-wrap: wrap; }
.radio-row .checkline { min-height: var(--touch-min, 2.75rem); }
.radio-row input { width: 20px; height: 20px; }
.pages { display: grid; grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr)); gap: var(--space-3); margin-bottom: var(--space-3); }
.page { margin: 0; font-size: .8rem; }
.page a { display: block; border: 1px solid var(--line, rgba(127,127,127,.35)); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg-soft, transparent); }
.page img { display: block; width: 100%; aspect-ratio: 3 / 4; object-fit: cover; }
.page-pdf { display: grid; place-items: center; aspect-ratio: 3 / 4; font-weight: 700; letter-spacing: .08em; color: var(--ink-soft); }
.page figcaption { margin-top: var(--space-1); }
@media (max-width: 560px) {
  .pick-grid { grid-template-columns: 1fr; }
  .radio-row { flex-direction: column; gap: var(--space-2); }
}

/* The hidden attribute wins over any display a class sets (app.js toggles it). */
[hidden] { display: none !important; }

.confirm-facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); gap: var(--space-3); margin: 0; }
.confirm-facts dt { font-size: .8rem; color: var(--ink-soft); font-weight: 600; margin-bottom: var(--space-1); }
.confirm-facts dd { margin: 0; font-size: 1.05rem; }

/* Company picker: chips + search box + results panel that opens while typing. */
.picked-chips { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-bottom: var(--space-2); }
.picked-chips[hidden] { display: none; }
.chip-pick { font-family: inherit; font-size: .85rem; font-weight: 500; padding: 4px 6px 4px 10px; gap: 6px; }
.chip-x { border: 0; background: transparent; color: var(--ink-soft); font-size: 1.1rem; line-height: 1; cursor: pointer; padding: 0 2px; min-width: 1.5rem; min-height: 1.5rem; border-radius: 50%; }
.chip-x:hover { color: var(--ink); background: rgba(127,127,127,.15); }
.picker-box { position: relative; }
.pick-results { position: absolute; left: 0; right: 0; top: calc(100% + 4px); z-index: 5;
  max-height: 16rem; overflow-y: auto; padding: var(--space-1) 0;
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius-md);
  box-shadow: var(--elevation-pop); }
.pick-results .pick-row { display: flex; align-items: center; gap: var(--space-2); width: 100%;
  min-height: var(--touch-min, 2.75rem); padding: 0 var(--space-3); margin: 0; font-size: .9rem; text-align: left; }
.pick-results .pick-row:hover { background: rgba(127,127,127,.1); }
.pick-add { border: 0; background: transparent; color: var(--primary); font: inherit; cursor: pointer; }
.pick-results:not([hidden]) .pick-row { cursor: pointer; }
@media (max-width: 560px) { .pick-results { max-height: 12rem; } }

/* ── wide tables on a whole-page scroll (Hanuman, 2026-09-07, /products) ──
   "Should be a whole page scroll instead of a sub-table scroll so we don't
   have embedded scroll bars." So the page scrolls; the toolbar (search box)
   sticks under the top bar; a copy of the column headers floats at the top of
   the screen while the real ones are scrolled away; and a slim scrollbar
   proxy sits at the bottom of the screen for the horizontal axis whenever the
   table's own bar is out of view. */
.table-tools.sticky {
  position: sticky; top: var(--topbar-h); z-index: 4;
  background: var(--surface); padding-top: var(--space-2); padding-bottom: var(--space-2);
}
.thead-float {
  position: fixed; z-index: 5; overflow: hidden; background: var(--surface);
  box-shadow: 0 1px 0 var(--border), 0 4px 10px rgba(0,0,0,.06);
}
.thead-float table { margin: 0; }
.thead-float[hidden] { display: none; }
.hscroll-proxy {
  position: fixed; bottom: 0; z-index: 5; height: 14px; overflow-x: auto; overflow-y: hidden;
  background: var(--surface); border-top: 1px solid var(--border);
}
.hscroll-proxy > div { height: 1px; }
.hscroll-proxy[hidden] { display: none; }
.markup-default { display: block; width: max-content; margin: 2px 0 0 auto; }
/* Tooltips for every pill: shown on hover, focus and tap, the same bubble
   everywhere, instead of the browser's slow native title. */
.tip-bubble {
  position: fixed; z-index: 200; max-width: 20rem; padding: 6px 10px;
  border-radius: var(--radius-md); background: var(--ink); color: var(--surface);
  font-size: .8rem; line-height: 1.35; box-shadow: var(--elevation-pop); pointer-events: none;
}
.tip-bubble[hidden] { display: none; }
.chip[data-tip], .sample-badge[data-tip] { cursor: help; }

/* Long card titles clamp to two lines with an ellipsis (Hanuman, 2026-09-07:
   "text going outside of card. Use ...'s as needed for long task names"). */
.card-title {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; overflow-wrap: anywhere;
}
/* A table page gets its head back: the title and its controls on one line,
   no paragraph, less space below (Hanuman, 2026-09-07: "space economy on
   the header of this space is poor"). */
.page-head-tight { margin-bottom: var(--space-3); align-items: center; }
.page-head-tight h1 { margin: 0; }
