/* Hunt Enterprises — Supply & Inventory.
 *
 * A supply-ledger aesthetic drawn from the DPH letterhead: paper ground, white
 * letterhead masthead, the logo's forest/leaf/slate palette, and ledger-ruled
 * tables. Serif display (Lora) echoes the DPH monogram; Libre Franklin (a
 * Franklin Gothic revival — the logo's own gothic caps) carries the UI.
 * One stylesheet, no framework. Fonts are self-hosted (strict same-origin CSP).
 */

@font-face {
  font-family: 'Libre Franklin';
  font-style: normal;
  font-weight: 400 700; /* variable file covers the whole range */
  font-display: swap;
  src: url('/assets/fonts/libre-franklin-latin.woff2') format('woff2');
}
@font-face {
  font-family: 'Lora';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('/assets/fonts/lora-600-latin.woff2') format('woff2');
}

:root {
  /* Clean-UI design system — the Round-10 comp approved at client UAT (Untitled-UI gray
   * scale + DPH-green swap). Same var NAMES as the old letterhead theme so every
   * screen-level rule re-tints for free; only the shell + a few primitives change. */
  --sidebar: #1b2e12;        /* dark forest rail */
  --sidebar-accent: #3e7a34; /* rail active bar / logo tile */
  --forest: #2f6b33;         /* primary green: buttons, accents, links */
  --forest-deep: #28582b;    /* hover/pressed */
  --leaf: #3e7a34;           /* secondary green accent (kept name for reuse) */
  --leaf-soft: #e8f0e4;      /* green tint for ok pills / selected rows */
  --ring: rgba(47, 107, 51, 0.28);
  --slate: #344054;          /* dark neutral (secondary text) */
  /* Ground — slate/neutral surfaces */
  --paper: #f5f6f8;          /* page background */
  --cream: #f9fafb;          /* subtle fill (inputs, header rows) */
  --panel: #ffffff;
  --ink: #101828;            /* primary text */
  --muted: #667085;          /* muted text */
  --line: #e4e7ec;           /* borders */
  --line-soft: #eaecf0;
  --line-strong: #d0d5dd;    /* input / button borders */
  /* Signals */
  --flag: #b42318;   --flag-bg: #fef3f2;  --flag-line: #fda29b;
  --warn: #b54708;   --warn-bg: #fffaeb;  --warn-line: #fedf89;
  --ok: #2f6b33;     --ok-bg: #e8f0e4;    --ok-line: #c6d2bd;
  --blue: #175cd3;   --blue-bg: #eff4ff;
  --radius: 8px;
  /* HUNTWEB-123 width policy. The work area itself is unbounded — these are the two exceptions.
   * `--measure` is in `ch` so it tracks the font rather than a guessed pixel count. */
  --measure: 78ch;
  --form-max: 940px;
  /* HUNTWEB-146: the dashboard's CARD region. Tables and toolbars stay unbounded (that is the
   * point of HUNTWEB-123); a row of summary cards stretched across an ultra-wide display just
   * re-creates the void. Never binds at 1280 or 1440. */
  --dash-max: 1600px;
  /* A RECORD's measure. Distinct from --form-max (data entry: 940px) and --dash-max (tile grid).
   * Applies to the record's header, rail, tab bar AND the grid beneath them, so the identity block
   * and the table share one right edge — today they are 940px and 2268px at a 2560px viewport.
   * Never binds at 1280 or 1440. */
  --record-max: 1200px;
  /* Both UI and display are the self-hosted grotesque (Libre Franklin) — the
   * pipeline's key call was dropping the Lora serif for headings; Public Sans
   * is a possible self-host follow-up (CSP is strict same-origin). */
  --font-ui: 'Libre Franklin', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-display: var(--font-ui);
}

* { box-sizing: border-box; }
html { height: 100%; }
body {
  margin: 0;
  min-height: 100dvh;
  font: 14px/1.5 var(--font-ui);
  color: var(--ink);
  background: var(--paper);
  -webkit-font-smoothing: antialiased;
}
::selection { background: var(--leaf-soft); color: var(--forest-deep); }
:focus-visible { outline: 2px solid var(--forest); outline-offset: 2px; }

/* ── App shell: fixed left rail + top bar + scrolling work area ─────────────── */
.app-shell { display: flex; min-height: 100dvh; }
.rail {
  width: 236px;
  flex: 0 0 236px;
  background: var(--sidebar);
  display: flex;
  flex-direction: column;
  padding: 0 0 1.2rem;
  position: sticky;
  top: 0;
  height: 100dvh;
  overflow-y: auto;
}
.brand {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 16px 14px 12px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
  text-decoration: none;
  color: inherit;
}
.brand-mark {
  width: 34px; height: 34px; flex: 0 0 auto;
  border-radius: 8px;
  background: var(--sidebar-accent);
  display: grid; place-items: center;
  color: #fff; font-weight: 700; font-size: 15px;
}
.brand-mark::after { content: 'H'; }
.brand-word {
  display: flex; flex-direction: column;
  font-weight: 600; font-size: 14px; line-height: 1.2;
  color: #fff;
}
.brand-sub {
  font-weight: 400;
  font-size: 11px;
  letter-spacing: 0;
  text-transform: none;
  color: rgba(255, 255, 255, 0.45);
}
.rail nav { display: flex; flex-direction: column; gap: 1px; padding: 8px 10px 0; }
/* Groups each get a box so a product tour can highlight one. The wrapper mirrors whatever
   direction the nav is using, so it adds a box without changing the layout — keep it in step with
   the nav in BOTH directions, including the narrow-screen rule below which switches to a row.
   Repeating the gap matters too, or the spacing inside a group collapses. */
.rail nav .rgroup { display: flex; flex-direction: column; gap: 1px; }
.rgl {
  font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;
  color: rgba(255, 255, 255, 0.5); font-weight: 600;
  padding: 0 10px; margin: 16px 0 4px;
}
.rail nav a {
  display: flex; align-items: center; gap: 10px;
  padding: 0.5rem 0.625rem;
  border-radius: 6px;
  font-size: 13.5px; font-weight: 500;
  color: rgba(255, 255, 255, 0.82);
  text-decoration: none;
  transition: background-color 150ms ease-out, color 150ms ease-out;
}
.rail nav a svg { width: 16px; height: 16px; flex: 0 0 auto; stroke: currentColor; opacity: 0.85; }
.rail nav a .rail-count { margin-left: auto; font-size: 12px; color: rgba(255, 255, 255, 0.6); font-variant-numeric: tabular-nums; }
.rail nav a:hover { background: rgba(255, 255, 255, 0.08); color: #fff; }
.rail nav a.active { background: rgba(255, 255, 255, 0.12); color: #fff; font-weight: 600; box-shadow: inset 3px 0 0 var(--sidebar-accent); }
.rail nav a.active .rail-count { color: rgba(255, 255, 255, 0.7); }

/* Collapsible sidebar / icon mode (HUNTWEB-95, client UAT) */
.rail-toggle {
  margin: auto 10px 0; display: flex; align-items: center; gap: 0.625rem;
  padding: 0.5rem 0.625rem; background: none; border: 0; cursor: pointer;
  color: rgba(255, 255, 255, 0.7); font: inherit; font-size: 0.9rem; border-radius: 8px;
}
.rail-toggle:hover { background: rgba(255, 255, 255, 0.08); color: #fff; }
.rail-toggle svg { width: 16px; height: 16px; flex: 0 0 auto; stroke: currentColor; transition: transform 0.15s ease; }
.rail-toggle:focus-visible { outline: 2px solid var(--sidebar-accent); outline-offset: 2px; }
body.anon .rail-toggle { display: none; }
.app-shell.rail-collapsed .rail { width: 64px; flex-basis: 64px; }
.app-shell.rail-collapsed .brand-word,
.app-shell.rail-collapsed .rgl,
.app-shell.rail-collapsed .rail-toggle > span { display: none; }
/* Nav labels: visually hidden (icon-only), but KEPT in the accessibility tree so screen readers
   still announce each link's name in collapsed mode (display:none would drop the name entirely). */
.app-shell.rail-collapsed .rail nav a > span {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}
.app-shell.rail-collapsed .rail nav a,
.app-shell.rail-collapsed .brand,
.app-shell.rail-collapsed .rail-toggle { justify-content: center; }
.app-shell.rail-collapsed .rail-toggle svg { transform: rotate(180deg); }

.main-col { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.topbar {
  height: 56px; flex: 0 0 56px;
  background: var(--panel);
  border-bottom: 1px solid var(--line);
  display: flex; align-items: center; gap: 10px;
  padding: 0 20px;
  position: sticky; top: 0; z-index: 10;
}
.topsearch {
  flex: 0 1 400px; min-width: 200px;
  display: flex; align-items: center; gap: 8px;
  height: 36px; padding: 0 12px;
  background: var(--cream); border: 1px solid var(--line); border-radius: 8px;
  color: var(--muted); font-size: 13.5px;
}
/* Persistent quick-action bar (HUNTWEB-86, client UAT) — Receive/Count/Scan/Transfer on every screen. */
.quick-actions { display: flex; align-items: center; gap: 6px; }
.qa-btn {
  display: inline-flex; align-items: center; gap: 6px;
  height: 36px; padding: 0 12px;
  border: 1px solid var(--line-strong); border-radius: 8px;
  background: var(--panel); color: var(--slate);
  font-size: 13px; font-weight: 600; text-decoration: none; white-space: nowrap;
}
.qa-btn svg { width: 15px; height: 15px; stroke: currentColor; }
.qa-btn:hover { background: var(--leaf-soft); color: var(--forest-deep); border-color: var(--ok-line); }
.qa-btn:focus-visible { outline: 2px solid var(--forest); outline-offset: 2px; }

/* HUNTWEB-112: the opt-in Tour dropdown. Anchored under the masthead Tour button; no tour auto-starts. */
.tour-wrap { position: relative; display: inline-flex; }
.tour-menu {
  position: absolute; top: calc(100% + 6px); right: 0; z-index: 60; min-width: 220px; max-width: calc(100vw - 24px);
  background: var(--panel); border: 1px solid var(--line-strong); border-radius: 10px;
  box-shadow: 0 8px 24px rgba(16, 24, 40, 0.14); padding: 6px; display: flex; flex-direction: column;
}
/* `display:flex` above out-specifies the UA `[hidden]{display:none}`, so restore it explicitly. */
.tour-menu[hidden] { display: none; }
.tour-menu-head { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--muted); padding: 6px 10px 4px; }
.tour-menu-item {
  display: block; width: 100%; text-align: left; padding: 8px 10px; border: 0; border-radius: 7px;
  background: transparent; color: var(--ink); font-size: 13px; font-weight: 600; cursor: pointer;
}
.tour-menu-item:hover { background: var(--leaf-soft); color: var(--forest-deep); }
.tour-menu-item:focus-visible { outline: 2px solid var(--forest); outline-offset: -2px; }

/* HUNTWEB-123 — the work area uses the WHOLE window.
 *
 * Raised at client UAT: the work area was fixed width, so it could not use the whole window and
 * columns could not run full size. Measured before the change, rail a constant 236px:
 * the table reached 988px at a 1280 viewport, 1148 at 1440, and then STOPPED at 1184 for both 1920
 * and 2560 — because `.container` carried `max-width: 1240px`. On a 2560px display that stranded
 * ~1,140px, close to half the screen, permanently empty.
 *
 * Removing the cap outright is the obvious move and it is wrong on its own: the same container also
 * holds explanatory prose, and a 2,300px line of text is unreadable — you lose your place between
 * the end of one line and the start of the next. So the width policy is PER REGION, set below:
 * ledger tables and toolbars take the window, prose keeps a measure, forms keep a sane field size.
 * Anything that wants the full width simply does nothing; only the two exceptions declare a bound. */
.container { width: 100%; margin: 0; padding: 24px 28px 56px; flex: 1; }

/* Prose keeps a reading measure. `ch` (not px) so it tracks the font: ~78 characters is inside the
 * 45-85 band typography settles on.
 *
 * This started as `.container > p` — direct children only — on the reasoning that `.muted` is also
 * used for small hints inside cards and drawers, and capping those would re-narrow places that are
 * already narrow. An independent review lens showed the rule was incomplete (the scan screen's
 * `.banner` and the `p` inside `.scan-form` are prose and matched neither), and the reasoning behind
 * restricting it was simply WRONG: `max-width` only bites when the parent is WIDER than the measure,
 * so on a 420px card a 78ch cap is a no-op. Descendant selectors are therefore both safer and
 * complete, and enumerating individual prose sites would have been whack-a-mole.
 *
 * A SECOND lens round then found two more carriers that are neither — `.req-hint` and
 * `.action-note` are `div`s. Rather than keep adding names, note what they share: THIS APP MARKS
 * PROSE WITH `.muted`. The screen ledes are `p.muted`, both hints are `div.muted`, the login copy is
 * `.muted`. So `.muted` is the convention, and keying on it covers the carriers that exist and the
 * ones nobody has written yet.
 *
 * `:not(td)` is load-bearing and NOT decoration. `.muted` is also on table cells — 29 `muted
 * empty-row` cells and 15 `num muted` cells — and `max-width` on a `td` feeds the column-width
 * algorithm, so capping those would distort the very tables this ticket widened. Checked before
 * writing it: every remaining `.muted` that is not a cell is short text (counts, statuses) where a
 * 78ch cap is a no-op, or genuine prose where it is exactly what we want. */
.container p,
.container .banner,
.container .muted:not(td) { max-width: var(--measure); }

/* Forms do NOT take the window. `.field-grid` is a 2-column grid; unbounded at 2560px it produces
 * ~1,150px-wide text inputs for a serial number, which looks broken and makes the eye travel
 * further than the table does. The bound is generous enough that nothing wraps at 1280. */
.field-grid { max-width: var(--form-max); }

h2 {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1.5rem;
  color: var(--ink);
  margin: 0 0 1.1rem;
  letter-spacing: -0.01em;
}
h3 {
  font-size: 0.8rem;
  font-weight: 600;
  margin: 1.4rem 0 0.6rem;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

.foot {
  width: 100%;
  margin: 2rem 0 0;
  padding: 1.1rem 0 0.5rem;
  border-top: 1px solid var(--line);
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem 1.1rem;
  align-items: baseline;
  font-size: 0.78rem;
  color: var(--muted);
}
.foot-brand { font-weight: 600; color: var(--forest); font-size: 0.85rem; }
.foot-line { text-transform: uppercase; letter-spacing: 0.1em; font-size: 0.68rem; color: var(--muted); }
.foot-note { flex-basis: 100%; }

/* ── Forms: a printed requisition sheet ────────────────────────────────────── */
.entry-form, .scan-form {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 1.4rem 1.5rem 1.5rem;
  box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04);
}
.field-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem 1.4rem; }
.field { display: flex; flex-direction: column; gap: 0.3rem; }
/* The `display: flex` above out-specifies the UA `[hidden] { display: none }` rule, so a field set
   `hidden` would go on rendering — the same trap `.ref-add[hidden]` documents below. The contacts
   form (HUNTWEB-128) hides two of its three org controls, and a form showing all three would ask
   for a supplier AND a property on one contact. Every scoped `.field` rule further down ties with a
   bare `.field[hidden]` on specificity and wins on order, so each is named here too: hiding a field
   must not work in some forms and silently not in others. `.part-form` used to be on this list and
   is gone with the Parts drawer; `.part-fields` takes its place — the record rail restates
   `display` on its fields, so it needs the same escape hatch even though nothing hides one yet. */
.field[hidden], .login-form .field[hidden], .part-fields .field[hidden] { display: none; }
/* A control stacked with its live hint (HUNTWEB-124). Layout only — it re-establishes the column
   the .field flex gives a bare control, so the input still fills the field's width. */
.field-stack { display: flex; flex-direction: column; gap: 0.25rem; }
.field-label {
  font-size: 0.7rem;
  color: var(--slate);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
input, select {
  font: inherit;
  padding: 0.5rem 0.6rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--cream);
  color: var(--ink);
  transition: border-color 150ms ease-out, background-color 150ms ease-out, box-shadow 150ms ease-out;
}
input:hover, select:hover { border-color: var(--slate); }
input:focus, select:focus {
  outline: none;
  border-color: var(--forest);
  background: #fff;
  box-shadow: 0 0 0 3px var(--ring);
}
input:disabled, select:disabled { background: var(--paper); color: #9aa091; border-color: var(--line-soft); }
.replacement-row { display: flex; align-items: center; gap: 0.9rem; margin-top: 1.1rem; flex-wrap: wrap; }
.check { display: inline-flex; align-items: center; gap: 0.45rem; font-size: 0.9rem; cursor: pointer; }
.check input { width: auto; accent-color: var(--forest); cursor: pointer; }

/* Line items — the ledger body of the requisition */
.lines { display: flex; flex-direction: column; gap: 0.45rem; }
.line-row { display: grid; grid-template-columns: 1fr 70px 110px 1fr 34px; gap: 0.5rem; align-items: center; }
/* min-width:0 turns each 1fr into minmax(0,1fr) so the two flexible columns resolve to the SAME
 * width in the header row (spans) and every input row (input/select) — grid tracks are per-grid,
 * and without this the select's/input's intrinsic min-content makes the header and rows misalign.
 * It also lets the tracks shrink on narrow screens instead of overflowing the card. */
.line-row > * { min-width: 0; }
.line-head { border-bottom: 1px solid var(--line-soft); padding-bottom: 0.25rem; }
.line-head span {
  font-size: 0.68rem;
  color: var(--slate);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.07em;
}
/* All line-item headers are left-aligned (a clean, consistent header row, like the requests grid);
 * values left-align too so each label sits directly over its column. `.num` (requests table) stays
 * right-aligned. min-width:0 above is what actually aligns the header row to the input rows. */
.num { text-align: right; font-variant-numeric: tabular-nums; }
.col-qty input, .col-cost input { font-variant-numeric: tabular-nums; }
.btn-remove {
  border: 1px solid var(--line);
  background: #fff;
  color: var(--flag);
  border-radius: var(--radius);
  cursor: pointer;
  height: 34px;
  font-size: 1.1rem;
  line-height: 1;
  transition: background-color 150ms ease-out, border-color 150ms ease-out;
}
.btn-remove:hover { background: var(--flag-bg); border-color: var(--flag-line); }
.btn-add {
  margin-top: 0.65rem;
  background: none;
  border: 1px dashed var(--line);
  color: var(--forest);
  font: inherit;
  font-weight: 600;
  padding: 0.45rem 0.8rem;
  border-radius: var(--radius);
  cursor: pointer;
  transition: border-color 150ms ease-out, background-color 150ms ease-out;
}
.btn-add:hover { border-color: var(--leaf); background: var(--leaf-soft); }
.actions { margin-top: 1.4rem; }
.btn-primary {
  background: var(--forest);
  color: #fff;
  border: 1px solid var(--forest);
  font: inherit;
  font-weight: 600;
  letter-spacing: 0;
  padding: 0.6rem 1.4rem;
  border-radius: var(--radius);
  cursor: pointer;
  transition: background-color 150ms ease-out;
}
.btn-primary:hover { background: var(--forest-deep); }
.btn-primary:disabled { opacity: 0.55; cursor: default; }
.result { margin-top: 1.25rem; }

/* ── Banners + flags ───────────────────────────────────────────────────────── */
.banner { padding: 0.75rem 0.95rem; border-radius: var(--radius); border: 1px solid transparent; }
.banner-info { background: var(--ok-bg); border-color: var(--ok-line); color: var(--ok); }
.banner-error { background: var(--flag-bg); border-color: var(--flag-line); color: var(--flag); font-weight: 600; }
/* HUNTWEB-123, third lens round: the flag REASON is sentence-length prose, but it is rendered as a
 * bare `span`, so it matches none of `p` / `.banner` / `.muted`. The cap goes on the CARD, not the
 * span: bounding the card holds the reason to the measure without touching markup, and the card is
 * a text row rather than a table, so nothing needs the window here. */
.flag-card {
  max-width: var(--measure);
  display: flex;
  align-items: center;
  gap: 0.65rem;
  background: var(--panel);
  border: 1px solid var(--line);
  border-left: 4px solid var(--flag);
  border-radius: var(--radius);
  padding: 0.65rem 0.85rem;
  margin-top: 0.5rem;
}
.flag-card.link { text-decoration: none; color: inherit; transition: background-color 150ms ease-out, border-color 150ms ease-out; }
.flag-card.link:hover { background: var(--cream); border-color: var(--slate); border-left-color: var(--flag); }
.sev {
  font-size: 0.66rem;
  font-weight: 700;
  letter-spacing: 0.05em;
  padding: 0.14rem 0.5rem;
  border-radius: 999px;
  text-transform: uppercase;
}
/* One rule per FlagSeverity (src/domain/intake-flags.ts), and the names must MATCH it: the pill
   renders `sev-${severity}` straight from the stored value, so a selector the engine never emits is
   a dead rule and a severity with no selector is an uncoloured pill. This said .sev-medium and
   .sev-low until 2026-08-12, which is neither of the two the engine actually raises — so `warn` and
   `info` painted bare for as long as they existed. Pinned by test/queue-severity-css.test.ts. */
.sev-high { background: var(--flag-bg); color: var(--flag); }
.sev-warn { background: var(--warn-bg); color: var(--warn); }
.sev-info { background: var(--line-soft); color: var(--muted); }

/* ── Tables: ruled ledger ──────────────────────────────────────────────────── */
.grid {
  width: 100%;
  border-collapse: collapse;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 10px;
  overflow: hidden;
  box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04);
}
.grid th {
  text-align: left;
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
  font-weight: 600;
  background: var(--cream);
  padding: 0.6rem 0.8rem;
  border-bottom: 1px solid var(--line);
}
.grid td { padding: 0.62rem 0.8rem; border-bottom: 1px solid var(--line); font-variant-numeric: tabular-nums; color: var(--slate); }
.grid tbody tr:last-child td { border-bottom: none; }
/* Columns group by MEANING, not by even distribution. On hand / Committed / Available are one
 * measurement and read as a block; Min / Max are reorder thresholds and are pushed away from it.
 * Before this, ON HAND sat at x=686 and AVAILABLE at x=1038 — 352px apart, with COMMITTED between
 * them, so comparing the two numbers people actually compare was the widest eye movement on the
 * screen. */
.grid th.col-group-start, .grid td.col-group-start { border-left: 1px solid var(--line); }
.grid th.col-gutter, .grid td.col-gutter { padding-left: 1.6rem; }
.row-link { cursor: pointer; transition: background-color 120ms ease-out; }
.row-link:hover { background: var(--cream); }
/* A disabled account on the Users screen (HUNTWEB-121). Dimmed so the state reads at a glance down
   a list, but not so far that the row is hard to read — it is still the row you click to re-enable
   someone. The `Disabled` pill carries the same state for anyone who cannot see the tint. */
.row-inactive td { background: var(--cream); opacity: 0.78; }
/* A cell whose content must not wrap — a date column narrow enough to break "2026-07-29" across
   two lines doubles the row height for no reason. */
.grid td.nowrap { white-space: nowrap; }
/* The total belongs at the foot of the column it totals, not as a field in the header. The heavier
 * top rule separates a derived row from the observed ones above it. */
.grid tr.grid-total td { border-top: 2px solid var(--line-strong); border-bottom: 0;
                         font-weight: 700; background: var(--cream); color: var(--ink); }
/* Per-location table on the part record's Inventory tab ONLY (HUNTWEB, Epic 1 follow-up).
 * `.grid` alone auto-sizes columns across the table's full width, which spreads On hand/
 * Committed/Available far enough apart that the col-group-start hairline barely reads as
 * grouping them. `.grid` is used at 30+ other call sites across app.ts and part-record.ts, so the
 * fix is scoped to this second class rather than to `.grid` itself — narrow, explicit widths on the
 * five quantity columns free the leftover width for Location/Kind instead of leaving every column
 * to stretch evenly. table-layout: fixed is required for the explicit widths to hold; Location is
 * deliberately left unset so it absorbs whatever space the fixed columns don't use, however long a
 * location name is. The tighter padding below (0.6rem vs the shared .grid's 0.8rem) is what keeps
 * a header word like "Committed" on one line at these widths — no wrap-related property is
 * declared or needed, since the values in the body are short numbers or an em dash. */
.grid-locations { table-layout: fixed; }
/* Tighter horizontal padding than the shared .grid th/td (0.8rem) buys back the few pixels the
 * single-word headers ("Committed", "Available") need to stay on one line at these widths — tested
 * against actual measured glyph widths, not guessed. */
.grid-locations th, .grid-locations td { padding-left: 0.6rem; padding-right: 0.6rem; }
/* `ON HAND` does not fit one line in a 52px column, so this header row is two lines tall where
 * other grids' are one — measured at 53px, the single largest block above the first data row.
 * Widening the column instead would push `Available` right and break the 160px grouping target,
 * which matters more than a one-line header. So the row keeps two lines and pays less for them. */
.grid-locations th { padding-top: 0.4rem; padding-bottom: 0.4rem; }
.grid-locations th:nth-child(2), .grid-locations td:nth-child(2) { width: 100px; } /* Kind */
.grid-locations th:nth-child(3), .grid-locations td:nth-child(3) { width: 52px; } /* On hand */
.grid-locations th:nth-child(4), .grid-locations td:nth-child(4) { width: 98px; } /* Committed */
.grid-locations th:nth-child(5), .grid-locations td:nth-child(5) { width: 88px; } /* Available */
.grid-locations th:nth-child(6), .grid-locations td:nth-child(6) { width: 64px; } /* Min — 48px overflowed under col-gutter padding + border-box (see finding 2); widened, taken from Location's slack. */
.grid-locations th:nth-child(7), .grid-locations td:nth-child(7) { width: 54px; } /* Max */
/* Stock levels' resizable columns (HUNTWEB-172). table-layout: fixed is what makes the <colgroup>
 * widths hold — the same reason .grid-locations above sets it. Scoped to its own class rather
 * than applied to the shared .grid, which has 30+ call sites elsewhere that expect auto sizing. */
.grid-inventory { table-layout: fixed; }
.grid-inventory th { position: relative; }
/* Keyboard path (HUNTWEB-172): the grip is a focusable role="separator", not aria-hidden, so
 * Left/Right can adjust a column's width without a pointer. The shared :focus-visible rule above
 * already outlines it; nothing further is needed here. */
.grid-inventory th .col-resize {
  position: absolute; top: 0; right: 0; width: 6px; height: 100%;
  cursor: col-resize; user-select: none;
}
.grid-inventory th .col-resize:hover { background: var(--line-strong); }
.pill {
  font-size: 0.7rem;
  font-weight: 600;
  padding: 0.12rem 0.55rem;
  border-radius: 999px;
  background: #f2f4f7;
  color: #475467; /* gray-600 on gray-100 → ~7:1, clears WCAG AA (the default/arch pill) */
  text-transform: capitalize;
}
.pill-flagged { background: var(--flag-bg); color: var(--flag); }
.pill-received, .pill-approved, .pill-fulfilled, .pill-completed { background: var(--ok-bg); color: var(--ok); }
/* Transfers (B1, HUNTWEB-74) — header status pills */
.pill-in_transit { background: var(--warn-bg); color: var(--warn); }
.pill-cancelled, .pill-draft { background: var(--line-soft); color: var(--muted); }
/* Cycle counts (B2, HUNTWEB-75) — session status pills + variance sign colors */
.pill-open { background: var(--warn-bg); color: var(--warn); }
.pill-posted { background: var(--ok-bg); color: var(--ok); }
.var-up { color: var(--ok); }
.var-down { color: var(--flag); }
/* Assets (B3, HUNTWEB-76) — asset status pills */
.pill-active { background: var(--ok-bg); color: var(--ok); }
.pill-retired { background: var(--line-soft); color: var(--muted); }
.pill-replaced { background: var(--warn-bg); color: var(--warn); }
/* Reports (C2, HUNTWEB-78) — total row accent */
.total-row td { border-top: 2px solid var(--line-strong); background: var(--paper); }
/* Dashboard (C1, HUNTWEB-77) — action-tile grid.
 *
 * HUNTWEB-146 rebuilt this to the approved Round-10 comp: label first, icon, big number,
 * then a caption saying what the number counts. The tone is carried by the ICON and — for the one
 * tile allowed to alarm — the number, not by a 4px left border on every card. Six of seven cards
 * wearing a coloured bar is what made the old dashboard read as a wall of alerts.
 *
 * A CARD REGION IS ITS OWN WIDTH REGION (the HUNTWEB-123 policy, extended rather than undone).
 * That ticket made the work area unbounded because ledger TABLES want the window; four cards
 * stretched across a 2,560px display do not — they become billboards with a 2rem number adrift in
 * the middle, which is the same void the panel below exists to remove. Tables and toolbars stay
 * unbounded; this cap applies only to the dashboard's cards. At 1280 and 1440 it never binds. */
.dash-region { max-width: var(--dash-max); }
.tile-grid { display: grid; gap: 1rem; margin-top: 0.5rem; }
/* Explicit column counts, not auto-fill: with the work area unbounded, `auto-fill` keeps minting
   empty 200px tracks and leaves the four lead tiles huddled on the left of a wide screen. */
/* BOTH rows share one 4-column rhythm. Giving the secondary strip 3 columns of its own made the
   "quieter" tiles WIDER than the lead ones (523px vs 388px at 1920) and knocked the two rows out of
   alignment — the hierarchy read backwards. Three tiles in a four-column grid leave the last cell
   empty, which is what keeps the columns lined up down the screen. */
.tile-grid-lead, .tile-grid-rest { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.tile-grid-rest { margin-top: 0.75rem; }
@media (max-width: 1080px) {
  .tile-grid-lead, .tile-grid-rest { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 620px) {
  .tile-grid-lead, .tile-grid-rest { grid-template-columns: minmax(0, 1fr); }
}
.tile { display: flex; flex-direction: column; gap: 0.3rem; padding: 1rem 1.15rem; border: 1px solid var(--line); border-radius: 10px; background: var(--panel); text-decoration: none; color: var(--ink); transition: box-shadow 0.12s, transform 0.12s, border-color 0.12s; }
.tile:hover { box-shadow: 0 2px 10px rgba(16,24,40,0.08); transform: translateY(-1px); border-color: var(--line-strong); }
.tile-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 0.6rem; }
.tile-top svg { width: 18px; height: 18px; flex: none; stroke: var(--line-strong); }
.tile-num { font-size: 2rem; font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1.05; color: var(--ink); }
.tile-label { font-size: 0.82rem; font-weight: 600; color: var(--slate); }
/* What the number counts, in the operator's words. Kept to two lines at the card's width so the
   cards in a row stay the same height. */
.tile-caption { font-size: 0.74rem; color: var(--muted); line-height: 1.35; }
/* The quieter strip: same card, less shout. */
.tile-grid-rest .tile { padding: 0.8rem 0.95rem; }
.tile-grid-rest .tile-num { font-size: 1.45rem; }
/* HUNTWEB-135: a tile whose count spans more than the screen it opens says so, quieter than the
   label so it reads as a qualifier rather than a second label. */
.tile-note { font-size: 0.72rem; color: var(--muted); opacity: 0.85; }
/* ── Tile tones (HUNTWEB-146) ──
 * clear   nothing in the queue — the number recedes.
 * volume  a large standing queue. Real information, in ink, with a neutral icon.
 *         It is NOT an alarm at any size and never was.
 * work    a worklist with items in it — the icon carries amber, the number stays ink.
 * alert   act today. The only red number on the screen. */
.tile-clear .tile-num { color: var(--muted); }
.tile-clear .tile-top svg { stroke: var(--line-strong); }
.tile-volume .tile-top svg { stroke: var(--muted); }
.tile-work .tile-top svg { stroke: var(--warn); }
.tile-alert .tile-top svg { stroke: var(--flag); }
.tile-alert .tile-num { color: var(--flag); }
.tile-alert { border-color: var(--flag-line); }

/* ── "Needs attention" worklist (HUNTWEB-146) ──
 * The panel that fills the ~60% of the dashboard that used to be empty below the tiles. Tiles say
 * how many; this says which, and every row is a link to the screen that resolves it. */
.attn-panel { border: 1px solid var(--line); border-radius: 12px; background: var(--panel); min-width: 0; }
.attn-head { display: flex; align-items: baseline; gap: 0.75rem; flex-wrap: wrap; padding: 1rem 1.15rem 0.75rem; border-bottom: 1px solid var(--line-soft); }
.attn-head h3 { margin: 0; font-size: 1rem; }
.attn-sub { font-size: 0.78rem; }
.attn-lanes { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (max-width: 900px) { .attn-lanes { grid-template-columns: minmax(0, 1fr); } }
/* Hairlines BETWEEN lanes only — no trailing rule under the last row of either column. */
.attn-lane { padding: 0.9rem 1.15rem; border-bottom: 1px solid var(--line-soft); }
.attn-lane:nth-child(odd) { border-right: 1px solid var(--line-soft); }
.attn-lanes > .attn-lane:last-child, .attn-lanes > .attn-lane:nth-last-child(2):nth-child(odd) { border-bottom: 0; }
@media (max-width: 900px) {
  .attn-lane:nth-child(odd) { border-right: 0; }
  .attn-lanes > .attn-lane:nth-last-child(2):nth-child(odd) { border-bottom: 1px solid var(--line-soft); }
}
.attn-lane-head { display: flex; align-items: baseline; gap: 0.5rem; margin-bottom: 0.5rem; }
.attn-lane-title { margin: 0; font-size: 0.82rem; font-weight: 600; color: var(--slate); }
.attn-lane-count { font-size: 0.74rem; color: var(--muted); font-variant-numeric: tabular-nums; }
.attn-see-all { margin-left: auto; font-size: 0.74rem; font-weight: 600; color: var(--forest); text-decoration: none; }
.attn-see-all:hover { text-decoration: underline; text-underline-offset: 2px; }
.attn-rows { display: flex; flex-direction: column; }
.attn-row { display: flex; align-items: center; gap: 0.6rem; padding: 0.42rem 0.5rem; margin: 0 -0.5rem; border-radius: 6px; text-decoration: none; color: var(--ink); }
.attn-row:hover { background: var(--paper); }
.attn-dot { width: 7px; height: 7px; border-radius: 50%; flex: none; background: var(--line-strong); }
.attn-dot-work { background: var(--warn); }
.attn-dot-alert { background: var(--flag); }
.attn-dot-volume { background: var(--muted); }
.attn-body { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.attn-title { font-size: 0.83rem; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.attn-detail { font-size: 0.74rem; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The verb appears on hover/focus so a resting panel is a list to read, not a wall of buttons —
   but it must never be keyboard-invisible, hence :focus-visible as well as :hover. */
.attn-verb { font-size: 0.72rem; font-weight: 600; color: var(--forest); flex: none; opacity: 0; transition: opacity 0.12s; }
.attn-row:hover .attn-verb, .attn-row:focus-visible .attn-verb { opacity: 1; }
.attn-empty { font-size: 0.78rem; margin: 0; padding: 0.3rem 0 0; }

/* ── The dashboard body: worklist left, insight panels right (HUNTWEB-147) ──
 * The Round-10 comp's two-column shape. `align-items: start` so a short right column does not
 * stretch its panels to the height of a long worklist.
 *
 * PLACED AFTER the worklist rules ON PURPOSE. The 1200px block below re-points `.attn-lanes`, and
 * at equal specificity the later rule wins — written above `.attn-lanes` it parsed fine, applied
 * nothing, and looked correct in the file. Order is the mechanism here, not tidiness. */
.dash-body { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 22rem); gap: 1.25rem; align-items: start; margin-top: 1.5rem; }
@media (max-width: 900px) { .dash-body { grid-template-columns: minmax(0, 1fr); } }
@media (max-width: 1200px) {
  /* With the insight column beside it the worklist is now ~60% of the page, and two lanes abreast
   * in that width are too cramped to read. Collapsing the LANES rather than the dashboard is what
   * lets the comp's two-column body survive down to 900px. */
  .attn-lanes { grid-template-columns: minmax(0, 1fr); }
  .attn-lane:nth-child(odd) { border-right: 0; }
  .attn-lanes > .attn-lane:nth-last-child(2):nth-child(odd) { border-bottom: 1px solid var(--line-soft); }
}
.insight-col { display: flex; flex-direction: column; gap: 1.25rem; min-width: 0; }
.insight-panel { border: 1px solid var(--line); border-radius: 12px; background: var(--panel); padding-bottom: 0.9rem; }
.insight-head { display: flex; align-items: baseline; gap: 0.75rem; flex-wrap: wrap; padding: 1rem 1.15rem 0.75rem; border-bottom: 1px solid var(--line-soft); }
.insight-head h3 { margin: 0; font-size: 1rem; }
.insight-sub { font-size: 0.78rem; margin-left: auto; }
.insight-empty { font-size: 0.78rem; margin: 0; padding: 0.9rem 1.15rem 0; }
/* The basis line. Small, but it is the sentence that stops a dollar figure from being read as a
 * claim it is not making — see src/db/insights.ts on why the basis is printed rather than implied. */
.insight-basis { font-size: 0.72rem; margin: 0; padding: 0.75rem 1.15rem 0; }

/* Spend bars. The width arrives inline, already clamped to [0,1] in web/insights.ts, so neither a
 * NaN (which renders as silence) nor a credit's negative share can reach it. */
.spend-rows { display: flex; flex-direction: column; gap: 0.55rem; padding: 0.9rem 1.15rem 0; }
.spend-row { display: grid; grid-template-columns: 6.5rem minmax(0, 1fr) auto; align-items: center; gap: 0.6rem; }
.spend-label { font-size: 0.78rem; color: var(--slate); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.spend-track { height: 8px; border-radius: 4px; background: var(--paper); overflow: hidden; }
.spend-fill { display: block; height: 100%; border-radius: 4px; background: var(--forest); }
.spend-value { font-size: 0.78rem; font-weight: 600; font-variant-numeric: tabular-nums; }

/* Consumption sparkline: one column per week, oldest left. Every column keeps its full-height
 * track, so a zero week still occupies its slot — a column that collapsed to nothing would leave
 * the axis reading as seven weeks instead of eight. */
.spark { display: flex; align-items: flex-end; gap: 0.35rem; height: 4.5rem; padding: 0.9rem 1.15rem 0; }
.spark-col { flex: 1; min-width: 0; height: 100%; display: flex; align-items: flex-end; background: var(--paper); border-radius: 3px; }
/* NO min-height here. A floor applied to every column paints a visible bar for a week in which
 * nothing happened — which is the one thing this chart must not do, since a generated zero week is
 * the whole reason the series is generated rather than discovered. The floor is on the modifier
 * below, which is only applied when the week actually has parts, so a small week stays visible
 * without a zero week pretending to be one. */
.spark-fill { width: 100%; background: var(--forest); border-radius: 3px; }
.spark-fill-some { min-height: 2px; }
/* The current week is always short because it has not finished. Hatching says "still running", so
 * a reader does not mistake the calendar for a downturn. */
.spark-col-partial .spark-fill { background: repeating-linear-gradient(45deg, var(--forest) 0 3px, var(--leaf-soft) 3px 6px); }
.spark-axis { display: flex; justify-content: space-between; font-size: 0.7rem; padding: 0.4rem 1.15rem 0; }
/* Inventory (A1) — status pills + By-part/By-location segmented toggle */
.pill-instock { background: var(--ok-bg); color: var(--ok); }
.pill-low { background: var(--warn-bg); color: var(--warn); }
.pill-out { background: var(--flag-bg); color: var(--flag); }
/* Fulfilment (A3, HUNTWEB-70) — request status + per-line availability pills */
.pill-partially_fulfilled { background: var(--warn-bg); color: var(--warn); }
.pill-backordered { background: var(--flag-bg); color: var(--flag); }
.pill-avail-on_hand { background: var(--ok-bg); color: var(--ok); }
.pill-avail-partial { background: var(--warn-bg); color: var(--warn); }
.pill-avail-backorder { background: var(--flag-bg); color: var(--flag); }
/* Part record / detail (HUNTWEB-84) */
.pill-arch { background: var(--line-soft); color: var(--slate); }
.delta-pos { color: var(--ok); font-weight: 600; }
.delta-neg { color: var(--flag); font-weight: 600; }
.row-link-a { color: var(--forest); text-decoration: none; font-weight: 600; }
.row-link-a:hover { text-decoration: underline; text-underline-offset: 2px; }
/* ── Record shell (Epic 1) ─────────────────────────────────────────────────── */
.record { max-width: var(--record-max); }
/* Only the part record (#/parts/:id) wraps its tab body in `.grid-wrap` (see `bodyWrap` in
 * part-record.ts, shared by all five of its tabs) — there, and only there, a wide table scrolls
 * horizontally inside the --record-max cap rather than forcing the record wider. The other five
 * screens built on this same `.record` shell — locations, suppliers, contacts, appliances,
 * transfers, all in app.ts — put `<table class="grid">` directly under `.record` with no
 * `.grid-wrap`, so a wide table there spills past the record's right edge instead of scrolling.
 * That gap is out of scope for this fix; the global `.grid-wrap { overflow-x: auto }` rule below
 * is what the part record actually relies on — no `.record`-scoped override is needed. */
/* Two-pane record: a fixed identity rail beside the working area.
 * 168px is chosen so the longest label ("Mfr part #") and its value sit on one line without
 * wrapping at the base font size. It is NOT a fraction of the container: a fraction reintroduces
 * the problem the rail exists to solve, which is columns sized by available space rather than by
 * their content. */
.record-body { display: flex; gap: 1.25rem; align-items: flex-start; margin: 0.5rem 0 1rem; }
/* The head sits INSIDE the working pane, so its spacing is the pane's problem, not the screen's.
 * `.screen-head`'s 0.75rem was tuned for a full-width heading with nothing directly beneath it;
 * here the category line follows immediately and reads as the title's subtitle, so the gap between
 * them should be smaller than the gap to the tab bar below. Measured: leaving it at 0.75rem pushed
 * the first data row to 211px, past the 200px the spec fixes for it. */
.record-main .screen-head { margin-bottom: 0.1rem; }
.record-main .record-cat { margin-bottom: 0.35rem; }
.record-rail { width: 168px; flex: none; border-right: 1px solid var(--line); padding-right: 0.85rem; }
.record-main { flex: 1; min-width: 0; }

/* The image slot is ALWAYS reserved, placeholder or not. Most of the catalogue has no image, so a
 * collapsing slot would change the rail's height on most navigations between parts.
 * Selector is `.part-thumb.part-thumb-rail`, not `.part-thumb-rail` alone: the element this class
 * is added to (see railThumb() in part-record.ts) always also carries `.part-thumb`, and a single-
 * class rule here would tie on specificity with `.part-thumb`'s own 40x40 size rule and lose
 * whenever source order changes — the two-class selector wins on specificity instead, so it
 * cannot be defeated by moving blocks around later. */
.part-thumb.part-thumb-rail { width: 100%; height: 96px; margin-bottom: 0.6rem; }

.spec-list { margin: 0; }
.spec-row { display: flex; justify-content: space-between; gap: 0.6rem;
            padding: 0.28rem 0; border-bottom: 1px solid var(--line-soft); }
.spec-row:last-child { border-bottom: 0; }
.spec-k { color: var(--muted); font-size: 0.8rem; }
.spec-v { color: var(--ink); font-size: 0.8rem; text-align: right;
          font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
/* ── Editing the identity in place ─────────────────────────────────────────── */
/* The rail is 168px because that fits the longest READ-STATE label and its value on one line. A
 * GL select reading "5100 — Maintenance supplies" does not fit it, so the rail widens WHILE
 * EDITING and returns on save or cancel. The step is deliberate — it reads as a mode change.
 *
 * Scoped to min-width:1025px on purpose. Below the breakpoint the rail is already full-width and
 * stacked (see the max-width:1024px block near the end of this file), and
 * `.record-body.record-editing .record-rail` OUT-SPECIFIES that rule — so an unscoped version
 * would silently re-impose a 240px rail on a phone whichever order the blocks appear in. */
@media (min-width: 1025px) {
  .record-body.record-editing .record-rail { width: 240px; }
}
/* FIELD SPACING IS 0.7rem, and that is a decision rather than an accident.
   These rules were written for the record rail; a second `.part-fields` block down in the drawer's
   section was written for its <fieldset> and made the container a flex column with `gap: 0.7rem`.
   The new rail markup is a <div class="part-fields">, so BOTH still matched: flex gaps do not
   collapse with margins, and the rendered gap was 0.7 + 0.45 = 1.15rem — the sum of two rules
   nobody chose together. The drawer's block is gone and the margin here now carries the whole
   spacing at the 0.7rem both authors had independently reached for. */
.part-fields .field { display: block; margin-bottom: 0.7rem; }
.part-fields .field-label { display: block; color: var(--muted); font-size: 0.75rem;
                            margin-bottom: 0.15rem; }
.part-fields input, .part-fields select { width: 100%; box-sizing: border-box; }
.part-fields .check { display: flex; align-items: flex-start; gap: 0.35rem;
                      font-size: 0.78rem; margin-top: 0.4rem; }
.part-fields .check input { width: auto; flex: none; margin-top: 0.2rem; }
.record-name-input { width: 100%; max-width: 32rem; font-size: 1.05rem; font-weight: 600; }
/* Pushed to the right edge of the head, so Edit and Save occupy the SAME spot and the mode is
   read off the label rather than off the position. */
.record-actions { display: flex; gap: 0.5rem; margin-left: auto; }
/* `.screen-head` is `align-items: center`, so its row height is whichever child is tallest — and
 * `.btn-primary`'s default padding (0.6rem block) makes a button 42.1875px tall against the h2's
 * 36px line box, six-odd px the row didn't have when `.screen-head`'s 200px tuning above was
 * chosen (it held only the heading and status pills then). The fix is here, on the buttons that
 * grew the row, not on the margins tuned around the row's OLD height: zero the buttons' own
 * block padding and give them the h2's line-box height directly, so they sit inside the head
 * without setting its height. */
.record-actions .btn-primary, .record-actions .btn-ghost {
  height: 2.25rem;
  padding-top: 0;
  padding-bottom: 0;
  display: inline-flex;
  align-items: center;
}
.rail-result:empty { display: none; }
.candidate-pane { margin-top: 0.25rem; }
.candidate-head { display: flex; align-items: baseline; gap: 0.75rem; margin-bottom: 0.5rem; }
.candidate-head h3 { margin: 0; font-size: 0.95rem; }
.candidate-head .candidate-done { margin-left: auto; }
/* Inline action bar used by receiving, transfers, count sessions, contacts and the part
   editor (10 call sites). Named for its first user; NOT the request lifecycle bar below. */
.fulfil-bar { display: flex; align-items: center; gap: 0.75rem; margin: 0.75rem 0; flex-wrap: wrap; }
.fulfil-note { font-size: 0.8rem; }
/* Request lifecycle bar (HUNTWEB-119) — approve / reject / fulfil / cancel on the request itself.
   A panel rather than a loose button row: it is the one place a reviewer acts on a request, so it
   reads as part of the record, and it always occupies the same spot whether it holds buttons or the
   sentence explaining why there are none. */
.action-bar { margin: 0.75rem 0 1rem; padding: 0.7rem 0.85rem; border: 1px solid var(--line-strong); border-radius: 8px; background: var(--panel); }
.action-row { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.action-row:empty { display: none; }
.action-note { font-size: 0.8rem; margin-top: 0.45rem; }
/* Two classes so it beats the `.muted` the note also carries — a single-class rule ties on
   specificity and loses on source order, which silently left a refusal rendering muted grey. */
.action-note.action-note-error { color: var(--flag); font-weight: 600; }
.action-row:empty + .action-note { margin-top: 0; }
.action-btn { font: inherit; font-size: 0.85rem; font-weight: 600; padding: 0.42rem 0.9rem; border: 1px solid var(--line-strong); border-radius: 6px; background: var(--panel); color: var(--ink); cursor: pointer; }
.action-btn:hover { border-color: var(--forest); background: var(--leaf-soft); }
.action-btn:disabled { opacity: 0.55; cursor: default; }
.action-btn:disabled:hover { border-color: var(--line-strong); background: var(--panel); }
.action-primary { background: var(--forest); border-color: var(--forest); color: #fff; }
.action-primary:hover { background: var(--forest-deep); border-color: var(--forest-deep); }
.action-primary:disabled:hover { background: var(--forest); border-color: var(--forest); }
.action-danger { color: var(--flag); }
.action-danger:hover { border-color: var(--flag); background: var(--flag-bg); color: var(--flag); }
.action-ghost { color: var(--muted); }
/* Requests list: the standing line that says where the actions are (the bulk bar is hidden until
   a row is ticked, which is how the lifecycle went unfound in UAT). */
.req-hint { font-size: 0.8rem; margin: 0 0 0.6rem; }
/* A refused sign-out must be visible in the masthead, not silently swallowed (HUNTWEB-139). */
.logout-error { font-size: 0.78rem; font-weight: 600; color: var(--flag); max-width: 22rem; }
.logout-error[hidden] { display: none; }
/* Receiving (A4, HUNTWEB-72) */
.screen-head { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin-bottom: 0.75rem; }
.screen-head h2 { margin: 0; }
.pack-input { width: 5rem; font: inherit; padding: 0.3rem 0.45rem; border: 1px solid var(--line-strong); border-radius: 6px; background: var(--panel); color: var(--ink); }
.pack-input:disabled { background: var(--line-soft); }
/* Reconciliation (A5, HUNTWEB-73) */
.match-cell { position: relative; }
.match-input { font: inherit; min-width: 16rem; padding: 0.3rem 0.5rem; border: 1px solid var(--line-strong); border-radius: 6px; background: var(--panel); color: var(--ink); }
.match-results { display: flex; flex-direction: column; gap: 2px; margin-top: 4px; }
.match-opt { text-align: left; font: inherit; font-size: 0.85rem; padding: 0.3rem 0.5rem; border: 1px solid var(--line-soft); border-radius: 6px; background: var(--panel); color: var(--ink); cursor: pointer; }
.match-opt:hover { background: var(--line-soft); }
.match-status { display: block; font-size: 0.78rem; margin-top: 2px; }
/* Seeded reference-list picker: category + brand (HUNTWEB-129/130).
   The select must take the free space so a long category (they run to ~40 characters) is
   readable, while the + button stays its own width — hence 1fr auto rather than a flex row, which
   would let the button grow. `.ref-add[hidden]` needs the explicit `display: none` because the
   `display: flex` below would otherwise override the hidden attribute's UA rule (same trap as
   .newpart-form above). */
.ref-cell { display: flex; flex-direction: column; gap: 0.3rem; }
.ref-row { display: grid; grid-template-columns: 1fr auto; gap: 0.35rem; align-items: center; }
.ref-add { display: flex; gap: 0.35rem; align-items: center; flex-wrap: wrap; }
.ref-add[hidden] { display: none; }
.ref-add .match-input { min-width: 8rem; flex: 1; }
.ref-add-btn { padding: 0.3rem 0.6rem; line-height: 1; }
/* Reconciliation "+ New part" inline create (HUNTWEB-93) */
.newpart-form { display: flex; flex-direction: column; gap: 0.4rem; margin-top: 0.5rem; }
.newpart-form[hidden] { display: none; }
.reconcile-actions { display: flex; gap: 0.4rem; }
.btn-ghost { font: inherit; font-size: 0.85rem; padding: 0.35rem 0.7rem; border: 1px solid var(--line-strong); border-radius: 6px; background: var(--panel); color: var(--muted); cursor: pointer; }
.btn-ghost:hover { color: var(--ink); }
.seg { display: inline-flex; border: 1px solid var(--line-strong); border-radius: 8px; overflow: hidden; background: var(--panel); }
.seg-btn { font: inherit; font-size: 0.82rem; font-weight: 600; padding: 0.42rem 0.9rem; border: none; background: var(--panel); color: var(--muted); cursor: pointer; }
.seg-btn.seg-on { background: var(--forest); color: #fff; }
.grid-wrap { overflow-x: auto; }
.sub-sku { display: block; font-size: 0.8em; }
.flag-dot { color: var(--flag); font-weight: 600; }
.muted { color: var(--muted); }
.detail-meta { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1rem; flex-wrap: wrap; }
.back { color: var(--forest); text-decoration: none; font-size: 0.85rem; font-weight: 600; }
.back:hover { text-decoration: underline; text-decoration-color: var(--leaf); text-underline-offset: 3px; }
a { color: var(--forest); }
a:hover { text-decoration-color: var(--leaf); }

/* ── View entrance: one staggered rise per route change ────────────────────── */
@media (prefers-reduced-motion: no-preference) {
  /* HUNTWEB-122 — `backwards`, NEVER `both`/`forwards`. Load-bearing, not a tidy-up.
   *
   * `both` = backwards + forwards, and the `forwards` half retains the final keyframe FOREVER.
   * `to { transform: none }` looks like it ends clean, but a transform under an active animation
   * fill computes to `matrix(1, 0, 0, 1, 0, 0)` — not `none` — and any transform other than `none`
   * creates a STACKING CONTEXT. So every `#app > *` kept one permanently, trapping the z-index of
   * every popover inside it: `.cols-menu` (z-index 20) could only order itself WITHIN
   * `.req-toolbar`, and `.grid-wrap` is a LATER SIBLING, so the table painted straight over the
   * open Columns picker. Reported twice at client UAT: the picker would not appear underneath.
   *
   * `backwards` still applies the `from` keyframe during the stagger delay — the whole reason a
   * fill was wanted — so the entrance is visually identical, but applies NO fill once the animation
   * ends, so the element returns to its own `transform: none` and the stacking context disappears.
   * Verified on the deployed preview: with `both`, elementFromPoint over the open menu returned the
   * table header; with `backwards` it returns the menu's own checkbox.
   *
   * A transformed ancestor is ALSO the containing block for `position: fixed` descendants, so this
   * could not have been worked around by making the menu fixed. Only the top layer escapes it —
   * see `anchoredMenu()` in web/app.ts, which uses it as defence in depth.
   *
   * The bug was invisible with reduced motion ON: no animation, no fill, no transform.
   */
  #app > * { animation: rise 320ms cubic-bezier(0.2, 0.7, 0.3, 1) backwards; }
  #app > *:nth-child(2) { animation-delay: 45ms; }
  #app > *:nth-child(3) { animation-delay: 90ms; }
  #app > *:nth-child(4) { animation-delay: 135ms; }
  #app > *:nth-child(n + 5) { animation-delay: 180ms; }
}
@keyframes rise {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: none; }
}

/* ── Auth: masthead userbox + letterhead login card ────────────────────────── */
.userbox { display: flex; align-items: center; gap: 0.6rem; margin-left: auto; }
.user-email { font-size: 0.8rem; color: var(--muted); max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.btn-logout {
  background: none;
  color: var(--slate);
  border: 1px solid var(--line);
  font: inherit;
  padding: 0.35rem 0.75rem;
  border-radius: var(--radius);
  font-size: 0.8rem;
  font-weight: 600;
  cursor: pointer;
  transition: border-color 150ms ease-out, color 150ms ease-out;
}
.btn-logout:hover { border-color: var(--forest); color: var(--forest); }

/* On the login screen the app nav is meaningless — hide the rail + search + quick actions. */
/* Pre-auth paint guard (HUNTWEB-108). Same element list as .anon on purpose: the booting state
   must hide precisely what the anonymous state hides, or the flash comes back for whichever
   element the two lists disagree on.

   visibility, not display: hidden elements keep their box, so when the chrome is revealed nothing
   below it moves. That matters on the <=680px layout, where .rail becomes a horizontal auto-height
   strip and a display swap would push the whole page down as the nav appeared. */
:root[data-booting] .rail nav,
:root[data-booting] .topsearch,
:root[data-booting] .quick-actions,
:root[data-booting] .foot,
:root[data-booting] .rail-toggle {
  visibility: hidden;
  /* Failsafe reveal. app.ts clears data-booting once auth resolves, but it is the ONLY thing that
     does -- and it cannot run if main.js 404s, fails to parse, or is blocked, nor if the
     /auth/session fetch never settles (it carries no timeout). An inline <head> timeout is not an
     option here: the CSP is script-src 'self', so inline script never executes. So the reveal is
     expressed in CSS, which needs nothing to run. After 3s the app degrades to its OLD behaviour --
     visible chrome -- rather than a permanently blank page. Zero duration: a state flip, not motion,
     so it is unaffected by prefers-reduced-motion. */
  animation: hunt-boot-reveal 0s linear 3s forwards;
}
@keyframes hunt-boot-reveal { to { visibility: visible; } }

/* Anonymous is a settled state, not a transient one, so it collapses the chrome outright. It also
   wins over the guard above regardless of order: display:none beats any visibility.

   Collapse the SHELL, not just its contents (HUNTWEB-118). Hiding only `.rail nav` / `.topsearch` /
   `.quick-actions` emptied the chrome but left the structure painted around the login card: the
   236px dark rail with the brand still in it, the topbar strip, and the footer with its
   "Supply-request entry — intake flagging routes…" line sitting under the card. A signed-out
   visitor has no use for any of it, and the footer text in particular read as the app leaking
   through the login screen. `.app-shell` is a flex row, so removing `.rail` lets `.main-col` take
   the full width and the card centres on the viewport instead of on the offset column. */
body.anon .rail,
body.anon .topbar,
body.anon .foot { display: none; }
/* ...and re-centre what is left.
   HISTORY, because the reason this rule exists has changed: `.container` used to be `margin: 0`
   with `max-width: 1240px`, so it left-anchored — with the rail gone it no longer filled the row and
   the login card drifted LEFT as the viewport grew (340px off centre at 1920px; the first check
   missed it because at 1280px the error is only 20px). HUNTWEB-123 removed that cap, so the
   container now fills the row on its own and `margin: 0 auto` is a no-op rather than the fix. It is
   KEPT deliberately: it costs nothing, and it means re-introducing any width bound on `.container`
   cannot silently re-open the drift. `.login-card` centres itself via its own `max-width: 420px;
   margin: … auto`. Re-measured after HUNTWEB-123: 0px off centre at 1280 / 1440 / 1920 / 2560. */
body.anon .container { margin: 0 auto; }

.login-card {
  max-width: 420px;
  margin: 3.25rem auto 0;
  background: var(--panel);
  border: 1px solid var(--line);
  border-top: 3px solid var(--forest);
  border-radius: var(--radius);
  padding: 2rem 1.85rem 2.1rem;
  text-align: center;
  box-shadow: 0 1px 3px rgba(38, 67, 29, 0.06), 0 12px 32px rgba(38, 67, 29, 0.08);
}
.login-logo { display: block; margin: 0 auto 0.9rem; width: 166px; height: 90px; }
.login-card h2 { margin: 0 0 0.3rem; }
.login-card .muted { color: var(--muted); margin: 0 0 1.35rem; }
.login-card .banner { text-align: left; margin-bottom: 1rem; }
.login-providers { display: flex; flex-direction: column; gap: 0.6rem; }
.provider-btn {
  display: block;
  padding: 0.7rem 1rem;
  border-radius: var(--radius);
  text-decoration: none;
  font: inherit;
  font-weight: 600;
  border: 1px solid var(--line);
  color: var(--ink);
  background: #fff;
  transition: border-color 150ms ease-out;
}
.provider-btn:hover { border-color: var(--forest); }
.login-form { display: flex; flex-direction: column; gap: 0.7rem; text-align: left; }
.login-form .field { display: flex; flex-direction: column; gap: 0.25rem; }
.login-form .actions { margin-top: 0.3rem; }
.login-form .btn-primary { width: 100%; }
.login-result:empty { display: none; }
.login-result { margin-top: 0.6rem; text-align: left; }
.login-divider { display: flex; align-items: center; gap: 0.6rem; color: var(--muted); font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.08em; margin: 1.2rem 0 0.9rem; }
.login-divider::before, .login-divider::after { content: ''; flex: 1; height: 1px; background: var(--line); }
.provider-btn.disabled { color: var(--muted); background: var(--paper); border-color: var(--line-soft); cursor: not-allowed; opacity: 0.75; }
.provider-btn.disabled:hover { border-color: var(--line-soft); }

/* ── Request detail: per-line part image (#5), original-scan panel (#1) ──────── */
.col-thumb { width: 52px; padding-right: 0 !important; }
.line-thumb {
  width: 40px;
  height: 40px;
  border-radius: var(--radius);
  object-fit: cover;
  border: 1px solid var(--line);
  background: var(--cream);
  display: block;
}
.line-thumb[src] { cursor: zoom-in; transition: transform 120ms ease-out, box-shadow 120ms ease-out; }
.line-thumb[src]:hover { transform: scale(1.04); box-shadow: 0 2px 8px rgba(38, 67, 29, 0.18); }
.line-thumb-empty {
  display: grid;
  place-items: center;
  color: var(--muted);
  font-size: 1rem;
  opacity: 0.55;
}

/* ── Parts manager (HUNTWEB-66) ─────────────────────────────────────────────── */
.parts-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
.parts-head h2 { margin: 0; }
.part-thumb {
  width: 40px;
  height: 40px;
  border-radius: var(--radius);
  object-fit: cover;
  border: 1px solid var(--line);
  background: var(--cream);
  display: block;
}
.part-thumb-empty { display: grid; place-items: center; color: var(--muted); font-size: 1rem; opacity: 0.55; }
.pill-inactive { background: var(--line-soft); color: var(--muted); margin-left: 0.5rem; }
.pill-nonstock { background: var(--warn-bg); color: var(--warn); margin-left: 0.5rem; }
/* `.part-form`, and the `.part-fields` <fieldset> reset that sat here, went with the Parts drawer
   (HUNTWEB-109). The record does not lock a fieldset — in read state the inputs are not in the
   document at all — and `.part-fields` is now a plain div styled up beside the rest of the rail;
   see the field-spacing note there for what the fieldset rule was silently adding until now. */
/* HUNTWEB-109: the asset detail's Status/Warranty/Retire/Replacement-part edit block is the same
   read-only-until-Edit fieldset gate as Parts — strip the default fieldset chrome and keep the
   two-column grid layout it had as a flat div.field-grid. */
.appliance-edit-fields { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem 1.4rem; border: 0; margin: 0; padding: 0; min-inline-size: 0; }
/* Match the sibling .field-grid breakpoint (680px) so this grid collapses in step with the asset-meta
   and replace grids around it on the same page — not one column-count ahead in the 641–680px band. */
@media (max-width: 680px) { .appliance-edit-fields { grid-template-columns: 1fr; } }
.part-image-field { display: flex; flex-direction: column; gap: 0.4rem; }
.part-image-preview .part-thumb { width: 96px; height: 96px; }
/* HUNTWEB-117 — "Find image" candidate cards. */
.part-image-field .btn-ghost { align-self: flex-start; }
.img-candidates:not(:empty) { display: flex; flex-direction: column; gap: 0.5rem; margin-top: 0.2rem; }
.img-candidate {
  display: flex; gap: 0.7rem; align-items: center;
  padding: 0.5rem; border: 1px solid var(--line-strong); border-radius: var(--radius); background: var(--panel);
}
.img-candidate-thumb {
  width: 64px; height: 64px; flex: 0 0 64px; object-fit: contain;
  border: 1px solid var(--line-soft); border-radius: 6px; background: #fff;
}
.img-candidate-meta { display: flex; flex-direction: column; gap: 0.25rem; min-width: 0; }
.img-candidate-title { font-size: 0.85rem; font-weight: 600; overflow-wrap: anywhere; }
.img-candidate-sub { font-size: 0.75rem; color: var(--muted); }
.img-candidate-meta .btn-primary { align-self: flex-start; }
/* HUNTWEB-117 MR3 — bulk find-missing-photos drawer. */
.parts-head-actions { display: flex; gap: 0.6rem; align-items: center; }
.drawer.drawer-wide { width: min(860px, 96vw); }
.bulk-status { margin-bottom: 0.8rem; }
.bulk-list { display: flex; flex-direction: column; gap: 0.9rem; margin-top: 0.9rem; }
.bulk-part { border: 1px solid var(--line-strong); border-radius: var(--radius); padding: 0.7rem 0.85rem; }
.bulk-part-head { margin-bottom: 0.5rem; }
.bulk-part-cards { display: flex; flex-direction: column; gap: 0.5rem; }
.bulk-part-done { color: var(--forest, #2f6b2f); font-size: 0.85rem; font-weight: 600; }
.scan-panel {
  margin: 0 0 1rem;
  display: inline-flex;
  flex-direction: column;
  gap: 0.35rem;
}
.scan-thumb {
  max-width: 180px;
  max-height: 240px;
  border: 1px solid var(--line);
  border-top: 3px solid var(--forest);
  border-radius: var(--radius);
  background: var(--panel);
  box-shadow: 0 1px 3px rgba(38, 67, 29, 0.08);
  cursor: zoom-in;
  transition: box-shadow 150ms ease-out;
}
.scan-thumb:hover { box-shadow: 0 4px 14px rgba(38, 67, 29, 0.16); }
.scan-panel figcaption { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.07em; }

/* Full-screen image overlay for #1/#5 */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: grid;
  place-items: center;
  padding: 3vmin;
  background: rgba(27, 49, 21, 0.82);
  cursor: zoom-out;
  animation: fade-in 140ms ease-out both;
}
.lightbox-img {
  max-width: 92vw;
  max-height: 92vh;
  object-fit: contain;
  border: 3px solid var(--panel);
  border-radius: var(--radius);
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.45);
  background: var(--panel);
}
.lightbox-close {
  position: fixed;
  top: 1.1rem;
  right: 1.4rem;
  width: 2.4rem;
  height: 2.4rem;
  font-size: 1.6rem;
  line-height: 1;
  color: #fff;
  background: rgba(255, 255, 255, 0.12);
  border: 1px solid rgba(255, 255, 255, 0.4);
  border-radius: 999px;
  cursor: pointer;
}
.lightbox-close:hover { background: rgba(255, 255, 255, 0.24); }
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }

/* ── Requests list: search toolbar + denser ledger (#4) ──────────────────────── */
.req-toolbar { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.85rem; flex-wrap: wrap; }
.req-search { flex: 1 1 240px; min-width: 180px; }
.req-search, .req-status, .req-pagesize, .req-location {
  padding: 0.4rem 0.6rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
  font: inherit;
}
.req-pagesize { flex: 0 0 auto; }
.flag-toggle { display: inline-flex; align-items: center; gap: 0.3rem; font-size: 0.85rem; color: var(--slate); cursor: pointer; }
.result-count { margin-left: auto; font-size: 0.8rem; font-variant-numeric: tabular-nums; }
.grid.dense td, .grid.dense th { padding: 0.35rem 0.7rem; }
.grid.dense { font-size: 0.9rem; }
.empty-row { text-align: center; padding: 1.4rem !important; }

/* The reason is free text from the rule engine and the only unbounded column here — the rest are
   codes, ids and dates. Hold it to the reading measure so one long reason cannot push Request,
   Routed to and Raised out past the table's edge.
   The bound sits on a block box INSIDE the cell, not on the <td> itself. `.grid-queue` uses auto
   table layout (it sets no table-layout, unlike `.grid-locations`, whose comment above its own
   `table-layout: fixed` says that property is required for explicit column widths to hold), and
   `max-width` on a table cell in auto layout is undefined by CSS 2.1 and left to the engine.
   Measured in Chromium 1234 at a 1440px viewport, same content, same sheet otherwise: no bound at
   all gave a reason column of 609.5px; `max-width` on the `<td>` gave 569.3px; the `div.reason-text`
   wrapper below gives 571.3px (2026-08-12). Chromium does honour it on the cell — the `<td>` form
   was not inert — but a block box is well-defined across engines where a table cell's `max-width`
   is not, so the bound rests on the `div` instead of behaviour the spec does not guarantee. Forcing
   `.grid-queue` to `table-layout: fixed` was the alternative and was rejected here: it would force
   explicit widths onto all six columns and need browser measurement to tune, where the `div` inside
   the `<td>` constrains the column's max-content contribution in auto layout and touches nothing
   else. */
.grid-queue .reason-text { max-width: var(--measure); }
.grid-queue td.col-request, .grid-queue td.col-reason { overflow-wrap: anywhere; }

/* Column show/hide control + editable bin (HUNTWEB-87, client UAT; widened HUNTWEB-173 —
   On hand, Status and Location are now hideable too: "everything except Part" per UI QA). */
.grid.hide-onhand .c-onhand,
.grid.hide-reorder .c-reorder,
.grid.hide-suggested .c-suggested,
.grid.hide-locations .c-locations,
.grid.hide-value .c-value,
.grid.hide-bin .c-bin,
.grid.hide-status .c-status,
.grid.hide-location .c-location { display: none; }
.bin-input {
  font: inherit; font-size: 0.82rem; width: 5.5rem; padding: 0.2rem 0.4rem;
  border: 1px solid var(--line-strong); border-radius: 6px; background: var(--panel); color: var(--ink);
}
.bin-input:focus-visible { outline: 2px solid var(--forest); outline-offset: 1px; }
.cols-wrap { position: relative; display: inline-flex; }
.cols-menu {
  position: absolute; top: calc(100% + 4px); right: 0; z-index: 20;
  display: flex; flex-direction: column; gap: 2px; min-width: 9rem; padding: 6px;
  background: var(--panel); border: 1px solid var(--line-strong); border-radius: 8px;
  box-shadow: 0 8px 24px rgba(16, 24, 40, 0.12);
}
.cols-menu[hidden] { display: none; }

/* HUNTWEB-122 — anchoredMenu() promotes these to the browser's TOP LAYER via `popover`, where the
 * UA stylesheet supplies its own `position: fixed; inset: 0; margin: auto; border; padding` and, more
 * importantly, `display: none` until open. Reset the box back to ours; anchoredMenu() writes the
 * top/left. The `:popover-open` rule restores the flex layout the UA `display:none` would otherwise
 * beat, exactly as the `[hidden]` rules above have to. Both menus keep their in-flow CSS anchoring
 * (`top: calc(100% + Npx); right: 0`) so a browser without `popover` still renders them correctly. */
.cols-menu:popover-open,
.tour-menu:popover-open { display: flex; }
[popover].cols-menu,
[popover].tour-menu { inset: auto; margin: 0; border-width: 1px; padding: 6px; overflow: visible; }
.cols-opt {
  display: flex; align-items: center; gap: 8px; font-size: 0.85rem;
  padding: 0.25rem 0.4rem; border-radius: 6px; cursor: pointer; color: var(--slate);
}
.cols-opt:hover { background: var(--cream); }

/* ── Row selection + bulk actions (HUNTWEB-59) ──────────────────────────────── */
.col-check { width: 1%; white-space: nowrap; text-align: center; }
.col-check .row-check { width: auto; margin: 0; accent-color: var(--forest); cursor: pointer; vertical-align: middle; }
.grid.selectable tbody tr.row-selected { background: var(--leaf-soft); }
.grid.selectable tbody tr.row-selected:hover { background: #e3edd2; }

/* The UA `[hidden]{display:none}` rule ties on specificity with `.bulk-bar` and loses to it as the
   later (author) rule — so `display:flex` would keep the bar visible even when .hidden is set. This
   attribute+class selector outranks the class rule and restores the hide (HUNTWEB-59 review). */
.bulk-bar[hidden] { display: none; }
.bulk-bar {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  margin-bottom: 0.7rem;
  padding: 0.5rem 0.75rem;
  background: var(--panel);
  border: 1px solid var(--line);
  border-left: 3px solid var(--forest);
  border-radius: var(--radius);
}
.bulk-count { font-weight: 600; color: var(--forest); font-size: 0.85rem; margin-right: 0.25rem; }
.bulk-gl-group { display: inline-flex; align-items: center; gap: 0.35rem; }
.bulk-gl { padding: 0.3rem 0.5rem; border: 1px solid var(--line); border-radius: var(--radius); background: var(--panel); font: inherit; font-size: 0.85rem; max-width: 260px; }
/* Button base (HUNTWEB-141).
 *
 * The colour classes used to be modifiers that ONLY worked combined with `.bulk-btn`
 * (`.bulk-btn.btn-approve`). Used on their own they matched nothing, so the button fell back to raw
 * browser chrome — and 8 call sites did exactly that, every one of them the PRIMARY action of its
 * screen (Receive delivery, Create transfer order, Start count, Post adjustments, Add asset, Record
 * replacement, Create & match, Add contact). The class WAS on the element, so a classList assertion,
 * an accessibility snapshot and a glance at a screenshot all looked correct; only the computed value
 * showed it (background rgb(239,239,239), border-radius 0px).
 *
 * Fixed by making the colour classes carry the base themselves rather than by adding `bulk-btn` at
 * each call site: that removes the trap instead of paying it off once, so a future bare
 * `class: 'btn-approve'` simply works. Combined use (`bulk-btn btn-approve`) is unchanged — the
 * declarations are identical, so the bulk action bar renders exactly as before. */
.bulk-btn,
.btn-approve,
.btn-reject,
.btn-clearflags {
  font: inherit;
  font-size: 0.82rem;
  font-weight: 600;
  padding: 0.32rem 0.7rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
  color: var(--ink);
  cursor: pointer;
  transition: background-color 120ms ease-out, border-color 120ms ease-out;
}
.bulk-btn:hover,
.btn-approve:hover,
.btn-reject:hover,
.btn-clearflags:hover { border-color: var(--forest); background: var(--leaf-soft); }
.bulk-btn:disabled,
.btn-approve:disabled,
.btn-reject:disabled,
.btn-clearflags:disabled { opacity: 0.55; cursor: default; }
/* Colour, layered on the base above. Same specificity as the base rules, so these win on source
 * order — which is why they must stay BELOW them. */
.btn-approve { background: var(--forest); border-color: var(--forest); color: #fff; }
.btn-approve:hover { background: var(--forest-deep); }
.btn-reject:hover, .btn-clearflags:hover { border-color: var(--flag); background: var(--flag-bg); color: var(--flag); }
/* A disabled button must not react to hover — without this the greyed-out control still lights up
 * green or red under the cursor, which reads as clickable. */
.btn-approve:disabled:hover { background: var(--forest); border-color: var(--forest); }
.btn-reject:disabled:hover, .btn-clearflags:disabled:hover { border-color: var(--line); background: var(--panel); color: var(--ink); }
.bulk-btn:disabled:hover { border-color: var(--line); background: var(--panel); }
/* Layout, specific to the bulk action bar — not a colour modifier, so it stays scoped to it. */
.bulk-btn.btn-clearsel { margin-left: auto; color: var(--muted); }
.bulk-feedback:not(:empty) { margin-bottom: 0.7rem; }
.pill-rejected, .pill-cancelled { background: var(--line-soft); color: var(--muted); }

/* ── Request drawer (#6): slide-out panel over a scrim ───────────────────────── */
.drawer-scrim {
  position: fixed;
  inset: 0;
  z-index: 90;
  background: rgba(27, 49, 21, 0.5);
  display: flex;
  justify-content: flex-end;
  animation: fade-in 140ms ease-out both;
}
.drawer {
  width: min(680px, 94vw);
  height: 100%;
  background: var(--paper);
  border-left: 3px solid var(--forest);
  box-shadow: -12px 0 40px rgba(27, 49, 21, 0.22);
  overflow-y: auto;
  padding: 1.4rem 1.6rem 2rem;
  animation: slide-in 220ms cubic-bezier(0.2, 0.7, 0.3, 1) both;
}
@keyframes slide-in { from { transform: translateX(24px); opacity: 0.4; } to { transform: none; opacity: 1; } }
.drawer-head { display: flex; align-items: baseline; gap: 0.9rem; margin-bottom: 1rem; }
.drawer-head h2 { margin: 0; }
.drawer-openfull { font-size: 0.82rem; font-weight: 600; text-decoration: none; }
.drawer-openfull:hover { text-decoration: underline; text-underline-offset: 3px; }
.drawer-close {
  margin-left: auto;
  width: 2rem;
  height: 2rem;
  font-size: 1.4rem;
  line-height: 1;
  color: var(--slate);
  background: none;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  cursor: pointer;
}
.drawer-close:hover { border-color: var(--forest); color: var(--forest); }

/* ── Small screens ─────────────────────────────────────────────────────────── */
/* Masthead reflow (HUNTWEB-86): the rail is a fixed 236px, so once the viewport dips below
 * ~1024px the topbar can't hold the search pill + 4 labelled action buttons + userbox. Drop the
 * (decorative, non-functional) search and collapse the actions to icon-only well before they'd
 * overflow onto tablet / split-screen widths; the aria-label + title keep the icons named.
 *
 * Below 1024px the rail stops being a rail. Keeping a 168px column beside the table at tablet
 * width would squeeze the numeric columns the rest of this work exists to bring together, so the
 * identity strip goes above instead and the specs flow in as many columns as fit.
 *
 * This breakpoint was DECIDED, not measured — no narrow-viewport review has happened yet. It is
 * the first thing to revisit once the layout is looked at on a tablet. */
@media (max-width: 1024px) {
  .topsearch { display: none; }
  .qa-btn { padding: 0 9px; }
  .qa-btn span { display: none; }
  .record-body { flex-direction: column; }
  .record-rail { width: 100%; border-right: 0; border-bottom: 1px solid var(--line);
                 padding-right: 0; padding-bottom: 0.75rem; }
  .record-rail .part-thumb-rail { width: 96px; }
  .spec-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
               column-gap: 1.25rem; }
}

@media (max-width: 680px) {
  .field-grid { grid-template-columns: 1fr; }
  /* Each line becomes a self-contained stacked card: part (full) · qty + cost + remove (one row) ·
   * GL (full). A single column header can't align to stacked fields, so it's hidden — the inputs'
   * placeholders/labels carry the meaning. min-width:0 (base rule) keeps nothing from overflowing. */
  .line-row.line-head { display: none; } /* .line-head also has .line-row → need the 2-class selector to beat the flex rule below */
  .lines { gap: 0.7rem; }
  .line-row {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    align-items: center;
    padding: 0.7rem;
    border: 1px solid var(--line-soft);
    border-radius: var(--radius);
    background: var(--cream);
  }
  .line-row .col-part { flex: 1 1 100%; order: 1; }
  .line-row .col-qty  { flex: 1 1 4rem; order: 2; }
  .line-row .col-cost { flex: 2 1 6rem; order: 3; }
  .line-row .btn-remove { flex: 0 0 auto; order: 4; width: 2.4rem; }
  .line-row .col-gl   { flex: 1 1 100%; order: 5; }
  /* Rail collapses to a horizontal top strip on narrow screens */
  .app-shell { flex-direction: column; }
  .rail { width: 100%; flex-basis: auto; height: auto; position: static; flex-direction: row; align-items: center; gap: 0.5rem; overflow-x: auto; padding: 0.5rem 0.75rem; }
  .rail .brand { border-bottom: none; padding: 0; }
  .rail nav { flex-direction: row; padding: 0; gap: 2px; }
  /* The group wrapper follows the nav into a row here, or each group becomes its own vertical
     stack and the rail turns into columns of links. */
  .rail nav .rgroup { flex-direction: row; gap: 2px; }
  .rgl { display: none; }
  .rail nav a.active { box-shadow: inset 0 -3px 0 var(--sidebar-accent); }
  .container { padding: 16px; }
  input, select { font-size: 16px; } /* keep iOS from auto-zooming on focus */
}

/* ── Print: a request detail prints like the paper form it replaces ────────── */
@media print {
  .rail, .topbar, .userbox, .foot-note { display: none; }
  body { background: #fff; }
  .grid, .entry-form { box-shadow: none; }
}

/* Onboarding tour (HUNTWEB-90) — DPH-brand the self-hosted driver.js popover (popoverClass:hunt-tour). */
.driver-popover.hunt-tour { border-radius: 10px; box-shadow: 0 10px 30px rgba(16, 24, 40, 0.18); }
.driver-popover.hunt-tour .driver-popover-title { color: var(--ink); font-weight: 700; }
.driver-popover.hunt-tour .driver-popover-description { color: var(--muted); }
.driver-popover.hunt-tour .driver-popover-progress-text { color: var(--muted); }
.driver-popover.hunt-tour button.driver-popover-next-btn {
  background: var(--forest); color: #fff; border: 1px solid var(--forest); text-shadow: none; border-radius: 6px;
}
.driver-popover.hunt-tour button.driver-popover-next-btn:hover { background: var(--forest-deep); border-color: var(--forest-deep); }
.driver-popover.hunt-tour button.driver-popover-prev-btn {
  border-radius: 6px; text-shadow: none;
}

/* Unavailable nav destination (HUNTWEB-111). Matches how the login card already presents the
   not-yet-enabled SSO buttons: still visible, so the capability is discoverable and people can ask
   for it, but clearly not usable yet. Kept clickable on purpose — the route explains WHY it is
   unavailable, which a dead link cannot do. */
.rail nav a.nav-unavailable,
.qa-btn.nav-unavailable {
  opacity: 0.45;
  /* not `not-allowed`: the link IS clickable, and following it is how you learn why the feature
     is unavailable. `help` signals "there is something to read here" without claiming it is inert. */
  cursor: help;
}
/* HUNTWEB-162 (review r1, code lens LOW): `nav-unavailable` now carries TWO different reasons, and
   the badge must not state the wrong one. Unconfigured OCR is "soon" — the feature is coming. A
   role without the permission is not waiting for anything, so it reads "locked", with the full
   sentence in the accessible name. Without this a viewer was told the feature was not ready when
   the real reason was their account. Longer selector, so it wins on specificity. */
.rail nav a.nav-unavailable.nav-restricted span::after {
  content: "locked";
}
.rail nav a.nav-unavailable span::after {
  content: "soon";
  margin-left: auto;
  font-size: 10px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.55);
  border: 1px solid rgba(255, 255, 255, 0.25);
  border-radius: 999px;
  padding: 0 5px;
}
/* Lays the label and its badge out on one row in the EXPANDED rail.
   HUNTWEB-162 (review r2) corrected the comment that used to sit here. It claimed this rule beats
   .app-shell.rail-collapsed .rail nav a > span and so keeps the badge out of the collapsed-mode
   clip. MEASURED, that is false: equal specificity (0-3-3) means this later rule wins only for the
   properties it sets, so `display` and `width` are overridden while `position:absolute`,
   `height:1px`, `overflow:hidden` and `clip:rect(0,0,0,0)` all still apply. In collapsed mode the
   span measures 64x1 and the badge is invisible.
   Left as-is deliberately: the collapsed rail is icon-only by design and has nowhere to put a text
   badge. Unavailability is still carried there by the 0.45 opacity and by the link's accessible
   name, which is the part assistive tech reads. The behaviour was right; only the comment was wrong. */
.app-shell .rail nav a.nav-unavailable span { display: flex; align-items: center; gap: 6px; width: 100%; }
/* HUNTWEB-146 (review r4) — the panel says when a payload could not be fully rendered. Warn, not
   alarm: the rest of the dashboard is still correct and still usable. */
.attn-anomaly { margin: 0; padding: 0.6rem 1.15rem; font-size: 0.76rem; color: var(--warn); background: var(--warn-bg); border-top: 1px solid var(--warn-line); }

/* ── Reconciliation, grouped by supply item (HUNTWEB-136) ─────────────────────
   The queue is 1,625 supply items wearing 48,483 lines. These are the parts that
   make that readable: the sentence saying what the queue IS, the three bucket
   chips, and the per-item row whose action depends on what kind of decision the
   item needs. Everything is a class because `style-src 'self'` silently refuses
   a style ATTRIBUTE — the HUNTWEB-147 failure where 14 elements rendered at zero
   size with the whole suite green. */
.recon-why-box {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 0.85rem 1rem;
  margin-bottom: 1rem;
  max-width: 82ch;
}
.recon-why { margin: 0; font-size: 0.86rem; color: var(--slate); line-height: 1.55; }

.recon-buckets { display: flex; gap: 0.6rem; margin-bottom: 1rem; flex-wrap: wrap; }
.recon-chip {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  font: inherit;
  text-align: left;
  border: 1px solid var(--line-strong);
  border-radius: 8px;
  background: var(--panel);
  padding: 0.6rem 0.9rem;
  min-width: 196px;
  cursor: pointer;
  transition: border-color 150ms ease-out, box-shadow 150ms ease-out;
}
.recon-chip:hover { border-color: var(--muted); }
.recon-chip-on { border-color: var(--forest); box-shadow: 0 0 0 3px var(--ring); }
.recon-chip:focus-visible { outline: 2px solid var(--forest); outline-offset: 2px; }
.recon-chip-h {
  display: flex; align-items: center; gap: 0.4rem;
  font-weight: 650; font-size: 0.86rem; color: var(--ink);
}
.recon-chip-n { font-size: 1.05rem; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--ink); }
.recon-chip-s { font-size: 0.72rem; color: var(--muted); }
.recon-dot { width: 8px; height: 8px; border-radius: 999px; flex: 0 0 auto; }
/* The three states draw from the signal palette only — never a colour outside it. */
.recon-dot-ready { background: var(--ok); }
.recon-dot-choice { background: var(--warn); }
.recon-dot-none { background: var(--muted); }

.recon-item { font-weight: 550; color: var(--ink); }
.recon-match-ok { color: var(--ok); font-weight: 600; }
.recon-match-amb { color: var(--warn); font-weight: 600; }
.recon-match-none { color: var(--muted); }
.recon-sku { color: var(--muted); font-size: 0.76rem; margin-left: 0.45rem; font-variant-numeric: tabular-nums; }
.recon-status { display: block; font-size: 0.76rem; margin-top: 0.2rem; }

td.recon-act { text-align: right; }
.recon-actions { display: inline-flex; gap: 0.4rem; align-items: center; justify-content: flex-end; flex-wrap: wrap; }
.btn-recon { font-size: 0.78rem; padding: 0.34rem 0.7rem; white-space: nowrap; }
/* The confirm step for an irreversible bulk write. It replaces the trigger rather than sitting
   beside it, so the button under the cursor is never the one that fires by accident. */
.recon-confirm { display: inline-flex; gap: 0.4rem; }
.recon-confirm[hidden] { display: none; }

.recon-foot { font-size: 0.78rem; margin-top: 0.8rem; }
.recon-pager { display: flex; gap: 0.5rem; margin-top: 0.4rem; }
.recon-crumb { display: flex; align-items: center; gap: 0.7rem; margin-bottom: 0.9rem; }
.recon-crumb-label { font-weight: 600; color: var(--ink); }
