/*
 * MESPL Tracker — shared design system.
 * Every custom screen (dashboard, import wizard, and everything built after
 * it) should build on these tokens and components rather than inventing new
 * colors/spacing/shadows inline. Django admin is out of scope — untouched.
 */

:root {
  --color-bg: #f5f5f7;
  --color-surface: #ffffff;
  --color-surface-muted: #f2f2f7;
  --color-text: #1d1d1f;
  --color-text-muted: #6e6e73;
  --color-border: #e5e5ea;
  --color-primary: #0a84ff;
  --color-primary-tint: #eaf4ff;

  /* Traffic-light families (revamp 01). Every hex here already appeared as a
     literal in this file — 8 to 12 times each, across the approval pills, the
     issue-flag pills, the urgency bands, the match verdicts and the money
     cells. Naming them changed no rendered colour; it means the next screen
     picks the same green as the last one by name instead of by grep. */
  --color-good: #2e7d32;
  --color-good-tint: #e8f5e9;
  --color-warn: #b28704;
  --color-warn-tint: #fff8e1;
  --color-bad: #c62828;
  --color-bad-tint: #ffebee;
  --color-info: #1565c0;
  --color-info-tint: #e3f2fd;

  /* A line that has to read across 1,600px of table needs more than the
     hairline that separates two cards. New in revamp 01 — not a promotion. */
  --color-border-strong: #d1d1d6;

  --radius-sm: 0.5rem;
  --radius-md: 0.875rem;
  --radius-lg: 1.25rem;
  --radius-full: 999px;

  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.25rem;
  --space-6: 1.5rem;
  --space-8: 2rem;

  --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.04), 0 8px 24px rgba(0, 0, 0, 0.06);
  --shadow-nav: 0 1px 0 rgba(0, 0, 0, 0.06);

  --touch-min: 44px;
  --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  /* Named, not introduced: this exact stack is already written out as a literal
     in four rules below. The GM landing (ADR-0128) sets its figures in it, and
     the approved design's IBM Plex Mono is deliberately NOT loaded — this app
     runs on an office LAN that may have no internet route, and a web font is a
     dependency this screen does not need to keep its numerals aligned. */
  --font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;

  /* Synced top scrollbar for wide tables (below) — deliberately thicker
     than a default OS scrollbar sliver so it's comfortably grabbable with
     a mouse and fine to drag on touch. */
  --table-scrollbar-height: 16px;

  /* The width of the FROZEN checkbox column that leads the line table, and so
     also the left offset of the frozen Item no. column beside it (ADR-0131).
     One token, because the two have to be the SAME number: a `left` a few
     pixels under the column's real width paints the item number over the tick,
     and a few pixels over it leaves a sliver of the scrolling rows showing
     between them — and a sticky cell takes its `left` even unscrolled, so an
     offset that is too big opens that gap on a table nobody has touched yet.
     Neither of those is a number a stylesheet may guess at, so the frozen cell
     is sized so that it CANNOT come out any other width: 12px of padding
     either side (table.app-table's own) around a checkbox given an explicit
     20px — the size .checkbox-row already uses — which is 44px of content
     nothing can push past, declared here as 2.75rem. Measured, not assumed:
     tests/test_line_table_sticky_header.py fails on a one-pixel drift. */
  --frozen-lead: 2.75rem;
}

* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  padding: 0;
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-sans);
  -webkit-font-smoothing: antialiased;
}

a {
  color: var(--color-primary);
  text-decoration: none;
}

a:hover {
  text-decoration: underline;
}

/* ---------- Nav ---------- */

.app-nav {
  position: sticky;
  top: 0;
  z-index: 30;
  background: rgba(255, 255, 255, 0.86);
  backdrop-filter: saturate(180%) blur(12px);
  -webkit-backdrop-filter: saturate(180%) blur(12px);
  box-shadow: var(--shadow-nav);
}

/* Width comes from --app-width (the "Width system" section at the foot of this
   file), never from a literal here: the bar and the content it sits over used
   to carry two independent 56rem values, and two literals eventually drift. */
.app-nav__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2) var(--space-3);
  padding: var(--space-2) var(--space-4);
  flex-wrap: wrap;
}

.app-nav__brand {
  font-weight: 600;
  font-size: 1.0625rem;
  color: var(--color-text);
  min-height: var(--touch-min);
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  /* Long as this brand string is, it must be able to wrap onto a second line
     on a narrow phone rather than forcing the whole bar wider than the
     viewport — see .app-nav__links below for the matching fix on the link
     row (both were the cause of the "Log out clipped off-screen" bug). */
  flex: 1 1 auto;
  min-width: 0;
}

.app-nav__logo {
  height: 30px;
  width: auto;
  max-width: 120px;
  object-fit: contain;
  flex-shrink: 0;
}

.app-nav__links {
  display: flex;
  align-items: center;
  /* This row used to be nowrap while its parent (.app-nav__bar) was allowed
     to wrap — so at phone widths the bar had room to wrap the brand+links
     onto separate lines, but the links themselves stayed on one unbroken
     line and simply ran off the right edge of the viewport (e.g. "Log out"
     fully clipped at 320-414px). Letting this row wrap too, plus a row-gap,
     is what actually fixes it — every link now flows onto as many lines as
     it needs instead of overflowing.  */
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-4);
  font-size: 0.9375rem;
}

.app-nav__links a,
.app-nav__links button {
  color: var(--color-text-muted);
  min-height: var(--touch-min);
  display: inline-flex;
  align-items: center;
  background: none;
  border: none;
  font-family: var(--font-sans);
  font-size: inherit;
  cursor: pointer;
  padding: 0;
}

.app-nav__links a:hover,
.app-nav__links button:hover {
  color: var(--color-text);
  text-decoration: none;
}

.app-nav__user {
  color: var(--color-text-muted);
  font-size: 0.875rem;
  display: inline-flex;
  align-items: center;
  min-height: var(--touch-min);
}

/* ---------- Notification bell + centre ---------- */

.app-nav__bell {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--touch-min);
  min-width: var(--touch-min);
  color: var(--color-text-muted);
}

.app-nav__bell:hover {
  color: var(--color-text);
  text-decoration: none;
}

.app-nav__bell-icon {
  font-size: 1.25rem;
  line-height: 1;
}

/* The unread count, sitting on the bell's upper-right corner. */
.notif-badge {
  position: absolute;
  top: 4px;
  right: 2px;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: var(--radius-full);
  background: #ff3b30;
  color: #fff;
  font-size: 0.6875rem;
  font-weight: 600;
  line-height: 18px;
  text-align: center;
}

.notif-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.notif-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) 0;
  border-top: 1px solid var(--color-border);
  min-height: var(--touch-min);
}

.notif-row:first-child {
  border-top: none;
}

/* An unread notification gets a subtle tinted rail down its left edge. */
.notif-row--unread {
  background: var(--color-primary-tint);
  border-radius: var(--radius-md);
  padding-left: var(--space-3);
  padding-right: var(--space-3);
  border-top-color: transparent;
  box-shadow: inset 3px 0 0 var(--color-primary);
}

.notif-row--unread + .notif-row {
  border-top-color: transparent;
}

.notif-row__link {
  display: flex;
  flex: 1 1 auto;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-1) var(--space-3);
  min-width: 0;
  color: var(--color-text);
}

.notif-row__link:hover {
  text-decoration: none;
}

.notif-row__link:hover .notif-row__msg {
  text-decoration: underline;
}

.notif-cat {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  padding: 2px var(--space-2);
  border-radius: var(--radius-full);
  background: var(--color-surface-muted);
  color: var(--color-text-muted);
  font-size: 0.6875rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.02em;
}

/* "Needs you" — the interrupt urgency (ADR-0120). Warm, and the only warm
   thing in the notification list, so the two or three rows a day that want
   something from the reader are findable without re-sorting the list. */
.notif-cat--urgent {
  background: var(--color-warn-tint);
  color: var(--color-warn);
}

.notif-row__msg {
  flex: 1 1 12rem;
  min-width: 0;
}

.notif-row__time {
  flex-shrink: 0;
  color: var(--color-text-muted);
  font-size: 0.8125rem;
}

.notif-row__read {
  flex-shrink: 0;
  padding: var(--space-1) var(--space-3);
  min-height: 36px;
  font-size: 0.8125rem;
}

.notif-empty {
  color: var(--color-text-muted);
  margin: var(--space-3) 0 0;
}

/* ---------- Layout ---------- */

/* Width and gutter both come from --app-width / --app-gutter — see the "Width
   system" section at the foot of this file. */
.app-main {
  margin: 0 auto;
}

.mt-4 {
  margin-top: var(--space-4);
}

/* ---------- Cards ---------- */

.card {
  background: var(--color-surface);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  padding: var(--space-6);
}

.card + .card {
  margin-top: var(--space-4);
}

/* A destructive-action zone (the superuser hard delete, ADR-0040): the same
   card, edged in the .btn-danger red and pushed further from its neighbours,
   so it reads as a different kind of block and can't be hit by accident while
   reaching for an ordinary action above it. */
.card--danger {
  border: 1px solid var(--color-bad);
}

.card + .card--danger,
.card--danger + .card,
* + .card--danger {
  margin-top: var(--space-8);
}

.card-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
}

.card-header .card-title,
.card-header .card-subtitle {
  margin: 0;
}

.card-header .card-subtitle {
  margin-top: var(--space-2);
}

/* A right-aligned row of buttons living inside a .card-header (e.g. the
   money summary card's Export buttons) — plain flex+gap, no margin-top
   (unlike .filter-actions, which assumes it sits below a form). */
.card-header-actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
}

.card-title {
  font-size: 1.375rem;
  font-weight: 600;
  margin: 0 0 var(--space-2);
}

.card-subtitle {
  color: var(--color-text-muted);
  margin: 0;
  font-size: 0.9375rem;
}

.card-subtitle + * {
  margin-top: var(--space-4);
}

.section-title {
  font-size: 1rem;
  font-weight: 600;
  margin: 0 0 var(--space-3);
}

.text-muted {
  color: var(--color-text-muted);
}

/* ---------- Stat tiles ---------- */

.stat-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-4);
}

@media (min-width: 640px) {
  .stat-grid {
    grid-template-columns: repeat(4, 1fr);
  }
}

.stat-tile {
  background: var(--color-surface);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  padding: var(--space-4) var(--space-5);
}

.stat-tile__label {
  color: var(--color-text-muted);
  font-size: 0.8125rem;
}

.stat-tile__value {
  font-size: 1.75rem;
  font-weight: 600;
  margin-top: var(--space-1);
}

.stat-tile__value--negative {
  color: var(--color-bad);
}

/* ---------- Money strip (compact money summary — see SPEC.md's "Margin
   tracking" / "Compact display") ---------- */
/* A row of stats inside a single .card, NOT one .stat-tile per number —
   that's what produced five oversized cards with wrapping totals before
   this replaced it. Label small and muted above, value below, left-aligned. */

.money-strip {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-4);
}

@media (min-width: 640px) {
  /* Not equal columns: "Ordered" and "Margin" each combine two figures
     onto one line ("₹X of ₹Y · Z%", "₹X · Y%") and need real room for
     that — an even 4-way split was too narrow at this font size and
     wrapped mid-phrase. Relies on stat order matching this ratio:
     Client total, Ordered, Vendor committed, Margin. */
  .money-strip {
    grid-template-columns: 1fr 1.7fr 1fr 1.7fr;
  }
}

.money-strip__label {
  color: var(--color-text-muted);
  font-size: 0.75rem;
}

.money-strip__value {
  font-size: 1.125rem;
  font-weight: 600;
  margin-top: var(--space-1);
}

.money-strip__value[title] {
  cursor: help;
  text-decoration: underline dotted var(--color-border);
  text-underline-offset: 3px;
}

.money-strip__value--negative {
  color: var(--color-bad);
}

/* Sub-line under a strip value — the old→new comparison shown while a
   "Reprice balance" amendment is pending approval (SPEC.md "Repricing the
   balance"). Muted, like the strip labels, so the live NEW figure stays the
   headline. */
.money-strip__note {
  color: var(--color-text-muted);
  font-size: 0.75rem;
  margin-top: var(--space-1);
}

/* The same strip with a variable number of figures — the site total
   (ADR-0073), which has no picker and shows every figure its reader may see.
   The fixed four-column ratio above is tuned to a known order of four; this
   wraps instead, so eleven figures lay out on a desktop and stack down a
   390px phone without either being a special case. */
.money-strip--wrap {
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: var(--space-5) var(--space-4);
}

@media (min-width: 640px) {
  .money-strip--wrap {
    grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  }
}

/* The band that names a site above its projects on the project list
   (ADR-0073). Structure, not an alert: a muted full-width row a reader's eye
   passes over on the way down the table, in the same surface the table's own
   heading bands use. */
table.app-table tbody tr.list-band td {
  background: var(--color-surface-muted);
  padding-top: var(--space-2);
  padding-bottom: var(--space-2);
  font-weight: 600;
}

.list-band__note {
  margin-left: var(--space-2);
  font-weight: 400;
  font-size: 0.75rem;
  color: var(--color-text-muted);
}

/* ---------- Tappable option rows (sheet picker etc.) ---------- */

.option-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.option-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: var(--touch-min);
  padding: var(--space-3) var(--space-4);
  border: 1.5px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  cursor: pointer;
  transition: border-color 0.15s ease, background-color 0.15s ease;
}

.option-row:hover {
  border-color: #c7c7cc;
}

.option-row:has(input:checked) {
  border-color: var(--color-primary);
  background: var(--color-primary-tint);
}

.option-row input[type="radio"],
.option-row input[type="checkbox"] {
  width: 20px;
  height: 20px;
  accent-color: var(--color-primary);
  flex-shrink: 0;
}

.option-row__label {
  font-size: 0.9375rem;
}

/* Card-style option row (import-type picker etc.): a title plus multi-line
   helper text stacked underneath, instead of .option-row's default single
   line — needs top-aligned content since the body is now taller than the
   radio input. */
.option-row--card {
  align-items: flex-start;
}

.option-row--card .option-row__label {
  font-weight: 600;
}

.option-row__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.option-row__helper {
  font-size: 0.8125rem;
  line-height: 1.4;
}

/* Where a proposed fact came from (ADR-0074): the document, the page, and the
   sentence that states it. Quieter than the value it sits under, because it is
   what you check the value against rather than the value itself. */
.option-row__source {
  font-size: 0.8125rem;
  line-height: 1.4;
  color: var(--color-text-muted);
}

.project-row {
  justify-content: space-between;
  color: inherit;
  text-decoration: none;
}

.project-row:hover {
  text-decoration: none;
}

.project-row__name {
  font-weight: 600;
  font-size: 1rem;
}

.project-row__meta {
  color: var(--color-text-muted);
  font-size: 0.8125rem;
  margin-top: 2px;
}

.project-row__stats {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  font-size: 0.875rem;
  flex-wrap: wrap;
  justify-content: flex-end;
}

/* ---------- Buttons ---------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--touch-min);
  padding: 0 var(--space-5);
  border-radius: var(--radius-full);
  font-size: 0.9375rem;
  font-weight: 600;
  border: none;
  cursor: pointer;
  transition: opacity 0.15s ease;
  font-family: var(--font-sans);
}

.btn:hover {
  opacity: 0.88;
  text-decoration: none;
}

.btn-primary {
  background: var(--color-primary);
  color: #fff;
}

.btn-secondary {
  background: var(--color-surface-muted);
  color: var(--color-text);
  /* A visible edge so secondary actions (Columns, exports) read as buttons,
     not stray text — see SPEC.md Design language "Interactive affordance". */
  border: 1.5px solid var(--color-border);
}

.btn-secondary:hover {
  border-color: var(--color-primary);
  color: var(--color-primary);
  opacity: 1;
}

.btn:active {
  transform: translateY(1px);
  opacity: 0.8;
}

.btn-danger {
  background: var(--color-bad);
  color: #fff;
}

/* ---------- Forms ---------- */

.field {
  margin-bottom: var(--space-4);
}

.field label {
  display: block;
  font-size: 0.875rem;
  font-weight: 500;
  margin-bottom: var(--space-2);
}

.input,
select,
textarea,
input[type="text"],
input[type="password"],
input[type="date"],
input[type="file"] {
  width: 100%;
  min-height: var(--touch-min);
  padding: 0 var(--space-3);
  border: 1.5px solid var(--color-border);
  border-radius: var(--radius-md);
  font-size: 0.9375rem;
  font-family: var(--font-sans);
  background: var(--color-surface);
  color: var(--color-text);
}

input[type="file"] {
  padding: var(--space-2) var(--space-3);
}

textarea {
  padding: var(--space-3);
  min-height: 5.5rem;
  resize: vertical;
}

.input:focus,
select:focus,
textarea:focus {
  outline: none;
  border-color: var(--color-primary);
  box-shadow: 0 0 0 3px rgba(10, 132, 255, 0.18);
}

/* A button that is waiting on an answer (the import wizard's re-upload /
   new-section question, ADR-0082). .btn-primary paints its own background, so
   without this a disabled button looks exactly like a live one. */
.btn:disabled,
.btn[disabled] {
  opacity: 0.45;
  cursor: not-allowed;
}

.options-card {
  border: 1.5px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-4);
  background: var(--color-surface);
}

.filter-grid,
.action-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: var(--space-3);
  align-items: end;
}

.filter-grid .field,
.action-grid .field {
  margin-bottom: 0;
}

/* Terms & Conditions clause cards on the PO details form. A clause cell holds
   a title, a "Mandatory" / "Optional — prints only if ticked" hint and a
   textarea, so it needs wider columns than .filter-grid's 9rem — the hint used
   to be wider than its own column and painted over the neighbouring card's
   hint. Cells stretch to the row height (not .filter-grid's align-items: end)
   and stack as a column, so a title wrapping to two lines leaves the hint and
   the textarea below it aligned with the rest of the row. */
.terms-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  gap: var(--space-4);
  align-items: stretch;
}

.terms-grid .term-field {
  display: flex;
  flex-direction: column;
  margin-bottom: 0;
  min-width: 0; /* let a cell shrink rather than push into its neighbour */
}

/* The title takes the row's spare height, so what follows it stays aligned. */
.term-field__title {
  flex: 1 0 auto;
  overflow-wrap: anywhere;
}

.term-field__hint {
  margin: 0 0 var(--space-2);
}

.term-field__hint .pill {
  max-width: 100%;
  white-space: normal; /* wrap inside the cell instead of overflowing it */
}

.filter-actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-top: var(--space-4);
}

.checkbox-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: var(--touch-min);
  font-size: 0.9375rem;
}

.checkbox-row input[type="checkbox"] {
  width: 20px;
  height: 20px;
  accent-color: var(--color-primary);
  flex-shrink: 0;
}

/* ---------- Tables ---------- */

.table-wrap {
  overflow-x: auto;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  background: var(--color-surface);
  -webkit-overflow-scrolling: touch;
}

/* A .table-wrap that is ALSO bounded in height, so it scrolls on both axes
   inside itself — the project's line table, and so far only that (ADR-0131).
   That bound is the whole point: `table.app-table thead th` has carried
   position:sticky since the beginning and never once pinned, because a plain
   .table-wrap scrolls sideways while the PAGE scrolls down, so the header's
   containing block is never the thing moving (SPEC.md's known gap). Here both
   axes move inside this box and the header pins against its top.

   The geometry is .cmp-scroll's, unchanged — the comparison grid proved it on
   the widest data in the app (ADR-0052), and two boxes that do the same job
   should not be two different heights. What is new is that this one is still
   a .table-wrap: it keeps the wrap's radius, shadow and surface, and every
   other .table-wrap on every other screen is untouched.

   The synced top scrollbar comes OFF this table (data-no-top-scrollbar in the
   markup, honoured by app.js). Same reasoning ADR-0052 gave the grid: that bar
   exists because the only reachable horizontal scrollbar on a long table was
   the native one past every row, and a box bounded in height has its own on
   screen at all times — a bar that drives one axis of a box that scrolls two
   is a control that half works. */
.table-wrap--bounded {
  max-height: min(72vh, 46rem);
  overflow: auto;
}

@media (max-width: 640px) {
  /* Unbounded on a phone, deliberately, for the reason .proj-head and the
     comparison grid already give: two sticky bars on a phone leave nothing for
     the work. The phone preset is four columns, so the header is not worth a
     scroll region here.
     overflow-y, not `overflow`, on purpose — `overflow: visible` would take
     the SIDEWAYS scroll away too, and horizontal scroll inside the wrap is
     exactly what keeps a 25-column table off the page's own scrollbar at
     390px (tests/test_project_lines_tab.py::test_the_lines_tab_fits_a_phone).
     With no max-height and auto height it never overflows vertically, so this
     is the plain .table-wrap the phone has always had. */
  .table-wrap--bounded {
    max-height: none;
    overflow-y: visible;
  }
}

/* Synced top scrollbar (static/js/app.js inserts one of these immediately
   before every .table-wrap) — the only reachable horizontal scrollbar on a
   long table used to be the native one at the very bottom of it, which on
   a 500+ row table means scrolling past the whole thing first. This one
   sits right above the header instead and stays there (position: sticky)
   as the page scrolls, mirroring .table-wrap's own native scrollbar exactly
   (app.js keeps them in sync both ways) — dragging either one moves the
   table the same way.
   A fully custom thumb/track, not a styled native scrollbar: real
   `::-webkit-scrollbar` customization turned out not to reliably paint
   across browsers/platforms when we checked — a plain absolutely-positioned
   div app.js drives directly has no such dependency and looks identical
   everywhere. Always reserves this height, even when the table doesn't
   currently overflow (app.js just hides the thumb then) — so its
   appearance/disappearance never shifts the layout. */
.table-scroll-top {
  position: sticky;
  top: 0;
  z-index: 2;
  height: var(--table-scrollbar-height);
  margin-bottom: var(--space-2);
  background: var(--color-surface-muted);
  border-radius: var(--radius-full);
}

.table-scroll-top__thumb {
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  background: #b5b5bd;
  border-radius: var(--radius-full);
  cursor: grab;
  /* Comfortably grabbable per the design language, not a thin sliver —
     matches --table-scrollbar-height rather than some thinner inline bar. */
  touch-action: none;
}

.table-scroll-top__thumb:hover {
  background: #9a9aa2;
}

.table-scroll-top__thumb:active {
  background: #83838b;
  cursor: grabbing;
}

table.app-table {
  border-collapse: collapse;
  width: 100%;
  font-size: 0.875rem;
}

table.app-table thead th {
  position: sticky;
  /* top: 0, not --table-scrollbar-height (ADR-0131). The offset was for the
     synced top scrollbar, and the only table where this rule actually PINS —
     the project's line table, in its own bounded box below — is the one table
     that has no top bar any more, so the offset would just leave a 16px gap
     the rows scroll through. Everywhere else this rule is still inert: a
     .table-wrap is only ever a horizontal scroll container, so its thead has
     nothing to stick against and the offset never showed. */
  top: 0;
  z-index: 1;
  background: var(--color-surface-muted);
  text-align: left;
  padding: var(--space-3);
  border-bottom: 1px solid var(--color-border);
  white-space: nowrap;
  font-weight: 600;
}

table.app-table thead select {
  min-width: 8.5rem;
  min-height: 36px;
  font-size: 0.8125rem;
}

/* A column heading a person may sort by (ADR-0109). It reads as the heading it
   already was — same weight, same colour — because most of the twenty-odd
   headings are NOT sortable, and a row of blue links with gaps in it would say
   "some of these are links" louder than it says what the column holds. The
   pointer and the underline on hover are what mark it as a control; the arrow
   marks the one column the table is actually ordered by. */
.th-sort {
  color: inherit;
  text-decoration: none;
  /* The whole heading is the target, not the word — 44px of it, the same
     minimum every other control on these screens keeps. The negative margin
     puts the padding back where the <th>'s own was, so a sortable heading sits
     exactly where an unsortable one does. */
  display: flex;
  align-items: center;
  gap: var(--space-1);
  min-height: 44px;
  margin: calc(var(--space-3) * -1);
  padding: var(--space-3);
}

.th-sort:hover,
.th-sort:focus-visible {
  text-decoration: underline;
}

.th-sort--on {
  color: var(--color-primary);
}

.th-sort__arrow {
  font-size: 0.75rem;
}

/* The sentence naming the order the table is in. Quiet: it is a fact to check
   when something reads oddly, not an instruction. */
.line-order-note {
  max-width: 60rem;
  white-space: normal;
}

.th-label {
  font-weight: 400;
  color: var(--color-text-muted);
  font-size: 0.75rem;
  margin-bottom: var(--space-1);
  max-width: 10rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The AI mapping proposal's mark on one column (ADR-0068). The proposal used
   to name its column in a paragraph above a 43-column table — "Column 13,
   'Contact Details'" — and the reader then counted columns across a horizontal
   scroll to find it. The finding now renders into that column's own header
   slot, so it is found by looking. Amber, the same warn tokens the panel's pill
   and the shifted-row question use: something to check, not something broken.
   Deliberately allowed to wrap and to widen its column past the nowrap header
   rule — a marked column standing wider than its neighbours is part of how it
   is spotted. */
.th-flag {
  display: block;
  max-width: 13rem;
  margin-top: var(--space-2);
  padding-top: var(--space-2);
  border-top: 1px solid var(--color-warn);
  white-space: normal;
  font-weight: 400;
}

.th-flag__reads {
  display: block;
  margin-top: var(--space-1);
  font-size: 0.75rem;
  line-height: 1.35;
  color: var(--color-text);
}

/* The reason in short form. The full sentence — what the column is mapped to
   now, what it looks like instead, then why — is on the mark's title. */
.th-flag__reason {
  display: block;
  font-size: 0.6875rem;
  line-height: 1.35;
  color: var(--color-text-muted);
}

/* How a prefilled dropdown was arrived at (ADR-0092), under the select it is
   about. Quieter than .th-flag on purpose: this is on EVERY suggested column,
   most of which are exact matches and want to be read past, and it must not
   compete with a finding that needs acting on. A contested column — one another
   column nearly took — is the one that speaks up. */
.th-match {
  display: block;
  max-width: 13rem;
  margin-top: var(--space-2);
  white-space: normal;
  font-weight: 400;
  font-size: 0.6875rem;
  line-height: 1.35;
  color: var(--color-text-muted);
}

.th-match--contested {
  padding-left: var(--space-2);
  border-left: 2px solid var(--color-warn);
  color: var(--color-text);
}

.th-match__how {
  display: block;
}

.th-match--contested .th-match__how {
  color: var(--color-warn);
  font-weight: 600;
}

.th-match__detail {
  display: block;
  color: var(--color-text-muted);
}

/* The header and its dropdown, marked together: the finding is about the
   choice in that select, so the select carries it too — the same way
   .field--invalid marks a control that needs attention. Keyed on the mark
   itself, which is present only on a flagged column (the slot every column
   carries is empty), so nothing is marked when nothing is flagged. */
table.app-table thead th:has(.th-flag) {
  background: var(--color-warn-tint);
}

table.app-table thead th:has(.th-flag) .th-label {
  color: var(--color-warn);
  font-weight: 600;
}

table.app-table thead th:has(.th-flag) select {
  border-color: var(--color-warn);
}

table.app-table tbody td {
  padding: var(--space-3);
  border-bottom: 1px solid var(--color-border);
  white-space: nowrap;
  max-width: 16rem;
  overflow: hidden;
  text-overflow: ellipsis;
}

table.app-table tbody tr {
  min-height: var(--touch-min);
  transition: background-color 0.15s ease;
}

table.app-table tbody tr:hover {
  background: var(--color-surface-muted);
}

table.app-table tbody tr:has(input:checked) {
  background: var(--color-primary-tint);
}

table.app-table tbody tr.is-selectable {
  cursor: pointer;
}

table.app-table tbody tr label {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  cursor: pointer;
  font-weight: 600;
}

table.app-table td.checkbox-cell {
  width: 2.5rem;
  white-space: nowrap;
}

/* ---------- Section headings in the line list ----------
   A BOQ specification heading (SPEC.md) stands above the items it heads as a
   full-width band that reads as a heading, not a row: a prominent item code
   with the specification text clearly subordinate beneath it. The band is
   indented by its dotted depth (--indent-depth), and tiered by that depth so a
   deeper heading reads as subordinate to a shallower one — the accent bar
   softens and the code shrinks, level by level, beyond the indent alone. The
   priced items below indent one level deeper via the same variable on their
   Item-no cell (depth 0 leaves the base padding untouched, so every
   non-hierarchical project renders exactly as before). */
table.app-table tbody tr.line-row--heading:hover {
  background: transparent;
}

/* The band's own select-all is a control, not a selection of the heading, so
   the "a checked row is tinted" affordance would mislead — keep it neutral. */
table.app-table tbody tr.line-row--heading:has(input:checked) {
  background: transparent;
}

table.app-table td.line-heading {
  white-space: normal;
  max-width: none;
  background: var(--color-surface-muted);
  color: var(--color-text);
  padding-top: var(--space-2);
  padding-bottom: var(--space-2);
  padding-left: calc(var(--space-3) + var(--indent-depth, 0) * 1.25rem);
}

/* The is-expanded rule (shared with the description column) caps the cell at
   30rem; a full-width heading band must stay full width when expanded. */
table.app-table td.line-heading.is-expanded {
  max-width: none;
}

.line-heading__band {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
}

.line-heading__check {
  flex: none;
  width: 1.15rem;
  height: 1.15rem;
  margin-top: 0.15rem;
  cursor: pointer;
}

.line-heading__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
  /* The accent bar that marks a heading and carries its depth tier. */
  border-left: 3px solid var(--color-border);
  padding-left: var(--space-3);
}

.line-heading__code {
  font-weight: 700;
  color: var(--color-text);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.01em;
  line-height: 1.3;
}

.line-heading__text {
  font-weight: 400;
  font-size: 0.85rem;
  color: var(--color-text-muted);
  line-height: 1.4;
}

/* Reuse the description column's expand affordance, but let the preview WRAP
   inside the full-width band instead of clipping to one line — truncatewords
   already caps the length, so the whole preview should show before "Show more". */
table.app-table td.line-heading .description-toggle,
table.app-table td.line-heading .description-preview {
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
}

/* Depth tiers. The base rules above are the deepest, most subordinate look;
   the shallowest sections get a solid accent and a larger code, stepping down
   with depth so the hierarchy reads beyond the indent. */
table.app-table tr.line-row--heading[data-heading-depth="0"] .line-heading__body {
  border-left-color: var(--color-primary);
}
table.app-table tr.line-row--heading[data-heading-depth="0"] .line-heading__code {
  font-size: 1.05rem;
}
table.app-table tr.line-row--heading[data-heading-depth="1"] .line-heading__body {
  border-left-color: var(--color-text-muted);
}
table.app-table tr.line-row--heading[data-heading-depth="1"] .line-heading__code {
  font-size: 0.98rem;
}

table.app-table tbody td[data-col="item_no"] {
  padding-left: calc(var(--space-3) + var(--indent-depth, 0) * 1.25rem);
}

/* ---------- Description column: word-truncated preview, inline expand ---------- */

/* Wider than the default 16rem column cap: truncatewords already limits the
   collapsed preview to ~10 words, so the column needs enough room to show that
   whole phrase on its own line — otherwise the generic ellipsis rule clips it
   again, mid-word, before the clean word-boundary "…" ever shows. */
table.app-table tbody td[data-col="description"] {
  max-width: 30rem;
}

table.app-table tbody td.is-expanded {
  white-space: normal;
  max-width: 30rem;
  overflow: visible;
  text-overflow: clip;
}

.description-toggle {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  font: inherit;
  color: inherit;
  cursor: pointer;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* `display: block` above has the same specificity as the UA stylesheet's
   `[hidden] { display: none }` and would otherwise win by source order,
   silently keeping the button visible after JS sets the hidden attribute. */
.description-toggle[hidden] {
  display: none;
}

.description-toggle:hover .description-preview {
  text-decoration: underline;
  text-decoration-style: dotted;
}

.description-full {
  white-space: normal;
  margin-top: var(--space-2);
}

.description-full p {
  margin: 0 0 var(--space-2);
}

.btn-link {
  background: none;
  border: none;
  padding: 0;
  color: var(--color-primary);
  font-family: var(--font-sans);
  font-size: inherit;
  cursor: pointer;
  min-height: auto;
}

.btn-link:hover {
  text-decoration: underline;
}

.btn-link--danger {
  color: var(--color-bad);
}

table.app-table td form {
  margin: 0;
}

/* ---------- Vendors screen row actions ---------- */

.vendor-row-actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  white-space: nowrap;
}

table.app-table select.status-select {
  min-width: 11rem;
  min-height: 36px;
  font-size: 0.8125rem;
}

/* The history drawer holds a table of its own now (revamp 01), so this cell has
   to stop behaving like a dense body cell entirely — nowrap, a 16rem cap and
   hidden overflow would clip the trail it contains to one ellipsised line. */
tr.history-row td {
  background: var(--color-surface-muted);
  white-space: normal;
  max-width: none;
  overflow: visible;
}

.history-log-list {
  list-style: none;
  margin: var(--space-2) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  font-size: 0.8125rem;
}

.history-log-list li {
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--color-border);
}

.history-log-list li:last-child {
  border-bottom: none;
  padding-bottom: 0;
}

/* ---------- Messages ---------- */

.app-messages {
  list-style: none;
  margin: 0 0 var(--space-4);
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.app-message {
  background: var(--color-warn-tint);
  color: #8a5a00;
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
  font-size: 0.875rem;
}

.app-message--error {
  background: #ffefed;
  color: #b3261e;
}

/* ---------- Pills: one consistent color per status / issue flag ---------- */

.pill {
  display: inline-flex;
  align-items: center;
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-full);
  font-size: 0.8125rem;
  font-weight: 600;
  white-space: nowrap;
}

.pill-not-started {
  background: #eceff1;
  color: #455a64;
}

.pill-enquiry-sent {
  background: var(--color-info-tint);
  color: var(--color-info);
}

.pill-quote-received {
  background: #ede7f6;
  color: #5e35b1;
}

.pill-po-issued {
  background: #ede7f6;
  color: #4527a0;
}

.pill-drawing-under-approval {
  background: var(--color-warn-tint);
  color: var(--color-warn);
}

.pill-drawing-approved-code-1 {
  background: #e0f2f1;
  color: #00695c;
}

.pill-drawing-approved-with-comments-code-2 {
  background: #f1f8e9;
  color: #558b2f;
}

.pill-drawing-returned-for-revision-code-3 {
  background: #fbe9e7;
  color: #d84315;
}

.pill-under-manufacturing {
  background: #fff3e0;
  color: #e65100;
}

.pill-ready-for-dispatch {
  background: #e1f5fe;
  color: #0277bd;
}

.pill-dispatched {
  background: var(--color-good-tint);
  color: var(--color-good);
}

.pill-delivered-at-site {
  background: var(--color-good-tint);
  color: #1b5e20;
}

.pill-closed {
  background: #eceff1;
  color: #37474f;
}

.pill-rate-revision-pending {
  background: var(--color-warn-tint);
  color: var(--color-warn);
}

.pill-delivery-delayed {
  background: var(--color-bad-tint);
  color: var(--color-bad);
}

.pill-vendor-not-responding {
  background: #fce4ec;
  color: #ad1457;
}

.pill-client-clearance-pending {
  background: #fff3e0;
  color: #e65100;
}

.pill-quality-issue {
  background: var(--color-bad-tint);
  color: #b71c1c;
}

.pill-payment-hold {
  background: var(--color-bad-tint);
  color: #b71c1c;
}

.pill-other {
  background: #eceff1;
  color: #455a64;
}

/* PO approval_status — same one-consistent-color-per-value convention as
   the status/issue-flag pills above (class name = slugify of the label). */
.pill-draft {
  background: #eceff1;
  color: #455a64;
}

.pill-pending-approval {
  background: var(--color-warn-tint);
  color: var(--color-warn);
}

.pill-approved {
  background: var(--color-good-tint);
  color: var(--color-good);
}

.pill-rejected {
  background: var(--color-bad-tint);
  color: var(--color-bad);
}

/* A quote's rank in the comparison (procurement/comparison_screen.py). Cool,
   never red, for the same reason every other lifecycle pill is: where a quote
   STANDS is a position, and being expensive is not an emergency — the red on
   that screen belongs to the quote that priced only part of the PO. L1 carries
   the good green because it is the same fact its column and its award option
   are painted with, and "Not ranked" is grey because a quote with no
   comparable total is not last, it is absent. */
.pill-l1 {
  background: var(--color-good-tint);
  color: var(--color-good);
}

.pill-ranked {
  background: #eceff1;
  color: #455a64;
}

.pill-unranked {
  background: #f2f2f7;
  color: var(--color-text-muted);
}

/* Final sign-off stage (SPEC.md's "PO document generation") — distinct from
   the approval_status pills above since a PO can be Approved and still be
   anywhere in this separate document-finalization stage. */
.pill-not-requested {
  background: #eceff1;
  color: #455a64;
}

.pill-pending-final-signoff {
  background: #ede7f6;
  color: #5e35b1;
}

.pill-final-approved {
  background: #e0f2f1;
  color: #00695c;
}

.pill-sent {
  background: var(--color-info-tint);
  color: var(--color-info);
}

/* Negative margin (SPEC.md's "Margin tracking") — the vendor costs more
   than what was budgeted. Deliberately the same red as pill-rejected: this
   needs to read as unambiguously bad at a glance, not just "different". */
.pill-negative-margin {
  background: var(--color-bad-tint);
  color: var(--color-bad);
}

/* Band pills (ADR-0048): the project frame's exception and a tab's single flag
   are not statuses with names of their own — they are "how bad is this", said
   once. Built straight on the traffic-light tokens rather than borrowing a
   status pill whose class name would then lie about what it marks. */
.pill-bad {
  background: var(--color-bad-tint);
  color: var(--color-bad);
}
.pill-warn {
  background: var(--color-warn-tint);
  color: var(--color-warn);
}
.pill-info {
  background: var(--color-info-tint);
  color: var(--color-info);
}
.pill-good {
  background: var(--color-good-tint);
  color: var(--color-good);
}

/* A project's own status, in the frame beside its name. "Closed" already has
   its line-status pill above; an active project is a state, not good news, so
   it reads the same neutral blue every other "this is simply where it is"
   pill does. */
.pill-active {
  background: var(--color-info-tint);
  color: var(--color-info);
}

/* Reprice pair marker (SPEC.md's "Repricing the balance") — one half of the
   paired rows that restate an undelivered balance at a new rate, and the
   line that heads such a group. Neutral amber: this is structure worth
   noticing (the row doesn't stand alone) but it is not a problem, so it
   deliberately avoids the red of .pill-negative-margin. */
.pill-repriced {
  background: #fff4e0;
  color: #9a6412;
  font-size: 0.6875rem;
  font-weight: 600;
  padding: 1px var(--space-2);
}

/* Three-way match statuses (SPEC.md's "Three-way match") — one consistent
   colour per status, same convention as the line status/issue pills above
   (class name = slugify of the label). `matched` reads green like an approved
   PO; the payment-blocking problems (short received / over billed) read the
   same red as a negative margin; rate variance is amber (worth a look, not a
   block); the two "awaiting" states are neutral blues (a state, not an error);
   no-activity is grey. The clear-to-pay / held verdict pills reuse the same
   green / red so a summary and its lines agree at a glance. */
.pill-matched,
.pill-clear-to-pay {
  background: var(--color-good-tint);
  color: var(--color-good);
}
.pill-short-received,
.pill-held {
  background: var(--color-bad-tint);
  color: var(--color-bad);
}
.pill-over-billed {
  background: #fbe9e7;
  color: #d84315;
}
.pill-rate-variance {
  background: var(--color-warn-tint);
  color: var(--color-warn);
}
.pill-awaiting-invoice {
  background: var(--color-info-tint);
  color: var(--color-info);
}
.pill-awaiting-receipt {
  background: #ede7f6;
  color: #5e35b1;
}
.pill-no-activity-yet,
.pill-no-po {
  background: #eceff1;
  color: #455a64;
}

/* Measurement-sheet import preview (ADR-0028) — the four things a scanned item
   code can be, reusing the three-way-match palette above so "will land / choose
   / nothing there / can't land" reads the same across screens. Green: matched,
   stage resolved, it will land on the bill. Amber: the code names more than one
   BOQ line, so a human chooses (worth a look, not an error). Grey: no line on
   this project carries the code — a fact, not a failure. Red-orange: matched,
   but there's nothing to land it on (no installation stage, or no rate on it) —
   the same red-orange over-billing uses, because it blocks a figure. */
.pill-code-ready {
  background: var(--color-good-tint);
  color: var(--color-good);
}
.pill-code-ambiguous {
  background: var(--color-warn-tint);
  color: var(--color-warn);
}
.pill-code-unmatched {
  background: #eceff1;
  color: #455a64;
}
.pill-code-unresolved {
  background: #fbe9e7;
  color: #d84315;
}

/* "Unverified" vendor chip (SPEC.md's "Vendor verification & cleanup") —
   sits next to a vendor name in tables/dropdowns. Deliberately smaller and
   lower-contrast than the status/issue pills above: it's a subtle "someone
   should look at this" note, not an alert. */
.pill-unverified {
  background: #f5f5f5;
  color: #757575;
  font-size: 0.6875rem;
  font-weight: 600;
  padding: 1px var(--space-2);
}

/* Mandatory vs selectable Standard Terms clauses (SPEC.md "PO document
   generation" — ADR-0015), on the PO details form. Same low-key chip
   treatment as .pill-unverified: a label, not an alert. */
.pill-mandatory {
  background: #eef4fb;
  color: #4a6b8a;
  font-size: 0.6875rem;
  font-weight: 600;
  padding: 1px var(--space-2);
}

.pill-selectable {
  background: #f5f5f5;
  color: #757575;
  font-size: 0.6875rem;
  font-weight: 600;
  padding: 1px var(--space-2);
}

/* ---------- Parent / sub-item grouping (SPEC.md "Parent & sub-items") ----------
   A sub-item row reads as nested under its main item: indented description
   with a "↳" marker and the same low-key chip treatment as .pill-unverified —
   grouping is structure, not an alert. */
.line-row--sub td[data-col="description"] {
  padding-left: calc(var(--space-4) * 2);
}
.sub-item-indent {
  color: #9e9e9e;
  margin-right: var(--space-1);
}
.pill-sub-item,
.pill-main-item {
  background: #eef4fb;
  color: #4a6b8a;
  font-size: 0.6875rem;
  font-weight: 600;
  padding: 1px var(--space-2);
}
.sub-item-follows {
  display: block;
  font-size: 0.6875rem;
}
.sub-item-add {
  display: inline-block;
  margin-left: var(--space-2);
  font-size: 0.8125rem;
  white-space: nowrap;
}
.field-hint {
  font-size: 0.75rem;
  color: #757575;
  margin-top: var(--space-1);
}

/* ---------- Table toolbar: Columns button ---------- */

.table-toolbar {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  flex-wrap: wrap;
}

/* ---------- Pagination ---------- */

.pagination-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
  /* Its own top rhythm (2026-08-06). Every screen used to rely on whatever sat
     around it, and the bar between a toolbar card and a table had nothing on
     either side — "Showing 1–3 of 3 projects" printed flush against the card
     above it, reading as part of that card rather than as the caption of the
     table below. */
  margin-top: var(--space-4);
}

.pagination-bar p {
  margin: 0;
}

.pagination-controls {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

/* ---------- Dashboard: status pill wrap, grouped exception lists ---------- */

.pill-wrap {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.dash-group {
  margin-top: var(--space-5);
}

.dash-group:first-child {
  margin-top: var(--space-4);
}

.dash-group__title {
  display: flex;
  align-items: center;
  min-height: var(--touch-min);
  font-weight: 600;
  font-size: 0.9375rem;
}

.dash-line-row {
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--color-border);
}

.dash-group .dash-line-row:last-child {
  border-bottom: none;
}

.dash-line-row__main {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
}

.dash-line-row__desc {
  font-size: 0.9375rem;
}

.dash-line-row__meta {
  font-size: 0.8125rem;
  margin-top: var(--space-1);
}

.dash-resolve-form {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

.dash-resolve-form input[type="text"] {
  flex: 1;
  min-width: 0;
}

/* ---------- Column visibility (Columns button — see app.js) ---------- */

#line-table-wrapper.hide-col-item_no [data-col="item_no"],
#line-table-wrapper.hide-col-sr_no [data-col="sr_no"],
#line-table-wrapper.hide-col-sor_no [data-col="sor_no"],
#line-table-wrapper.hide-col-part_no [data-col="part_no"],
#line-table-wrapper.hide-col-sor_title [data-col="sor_title"],
#line-table-wrapper.hide-col-description [data-col="description"],
#line-table-wrapper.hide-col-qty [data-col="qty"],
#line-table-wrapper.hide-col-uom [data-col="uom"],
#line-table-wrapper.hide-col-clearance_qty [data-col="clearance_qty"],
#line-table-wrapper.hide-col-delivered_qty [data-col="delivered_qty"],
#line-table-wrapper.hide-col-balance [data-col="balance"],
#line-table-wrapper.hide-col-scope [data-col="scope"],
#line-table-wrapper.hide-col-category [data-col="category"],
#line-table-wrapper.hide-col-discipline [data-col="discipline"],
#line-table-wrapper.hide-col-vendor [data-col="vendor"],
#line-table-wrapper.hide-col-po [data-col="po"],
#line-table-wrapper.hide-col-rate [data-col="rate"],
#line-table-wrapper.hide-col-vendor_rate [data-col="vendor_rate"],
#line-table-wrapper.hide-col-amount [data-col="amount"],
#line-table-wrapper.hide-col-vendor_amount [data-col="vendor_amount"],
#line-table-wrapper.hide-col-margin [data-col="margin"],
#line-table-wrapper.hide-col-margin_pct [data-col="margin_pct"],
#line-table-wrapper.hide-col-status [data-col="status"],
#line-table-wrapper.hide-col-issue [data-col="issue"],
#line-table-wrapper.hide-col-remark [data-col="remark"] {
  display: none;
}

/* ---------- PO quote comparison: the compact rate input ---------- */
/* Compact input for a dense per-item x per-vendor rate grid — the standard
   44px touch-min .input/select/textarea rule (see Forms above) is right for
   primary controls but would make a multi-row x multi-column grid absurdly
   tall; table.app-table thead select already carries the same kind of
   compact override for the same reason. Everything else about this screen
   lives in "REVAMP 01 — THE COMPARISON GRID" below. */
.rate-cell-input {
  width: 6.5rem;
  min-height: 36px;
  padding: 0 var(--space-2);
  border: 1.5px solid var(--color-border);
  border-radius: var(--radius-sm);
  font-size: 0.8125rem;
  font-family: var(--font-sans);
  background: var(--color-surface);
  color: var(--color-text);
}

.rate-cell-input--text {
  width: 9rem;
}

.rate-cell-input:focus {
  outline: none;
  border-color: var(--color-primary);
  box-shadow: 0 0 0 3px rgba(10, 132, 255, 0.18);
}

/* ---------- Table footers (totals rows) ---------- */

table.app-table tfoot td {
  padding: var(--space-3) var(--space-4);
  border-top: 2px solid var(--color-border);
  font-weight: 600;
  white-space: nowrap;
}

/* ---------- Login page ---------- */

.login-page {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-6);
  padding: var(--space-6) var(--space-4);
}

.login-page__brand {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
  text-align: center;
}

.login-page__logo {
  height: 72px;
  width: auto;
  max-width: 100%;
  object-fit: contain;
}

.login-page__company {
  font-size: 1.25rem;
  font-weight: 600;
  color: var(--color-text);
}

.login-page__card {
  width: 100%;
  max-width: 22rem;
}

.login-page__submit {
  width: 100%;
  margin-top: var(--space-2);
}

/* ---------- Interactive affordance: chips & sortable headers ---------- */

/* A pill that's a link (negative-margin chips, filter chips) must read as
   pressable: edge + hover, not just colored text. */
a.pill {
  border: 1.5px solid transparent;
  transition: border-color 0.15s ease, opacity 0.15s ease;
}

a.pill:hover {
  border-color: currentColor;
  text-decoration: none;
}

/* ---------- Discipline switch (project page view lens) ---------- */

.discipline-switch {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
}

.discipline-switch__label {
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--color-text-muted);
  margin-right: var(--space-2);
}

.discipline-switch__option {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--touch-min);
  padding: 0 var(--space-4);
  border-radius: var(--radius-full);
  background: var(--color-surface);
  border: 1.5px solid var(--color-border);
  color: var(--color-text);
  font-size: 0.9375rem;
  font-weight: 600;
  text-decoration: none;
  transition: border-color 0.15s ease, background 0.15s ease;
}

.discipline-switch__option:hover {
  text-decoration: none;
  border-color: var(--color-primary);
  color: var(--color-primary);
  background: var(--color-primary-tint);
}

.discipline-switch__option:active {
  transform: translateY(1px);
}

.discipline-switch__option--active,
.discipline-switch__option--active:hover {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: #fff;
}

/* ---------- Import wizard: sheet picker with per-sheet discipline ---------- */

.option-row--sheet {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  /* A sheet row carries two pickers now (section and scope, ADR-0083), so it
     wraps at any width rather than only under the mobile breakpoint below. */
  flex-wrap: wrap;
}

.option-row--sheet .option-row__label {
  flex: 1;
}

.option-row--sheet .option-row__discipline {
  width: auto;
  min-width: 11rem;
  flex: 0 0 auto;
}

@media (max-width: 480px) {
  .option-row--sheet {
    flex-wrap: wrap;
  }
  .option-row--sheet .option-row__discipline {
    width: 100%;
    flex-basis: 100%;
  }
}

/* ---------- Two-factor auth (enrolment + code challenge) ---------- */

.twofa-card {
  max-width: 26rem;
  margin-inline: auto;
}

.twofa-qr {
  display: flex;
  justify-content: center;
  margin: var(--space-5) 0;
}

.twofa-qr img {
  width: 200px;
  height: 200px;
  max-width: 100%;
  border: 1.5px solid var(--color-border);
  border-radius: var(--radius-md);
}

.twofa-key {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  word-break: break-all;
  background: var(--color-surface-muted);
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-sm);
}

.twofa-code-input {
  letter-spacing: 0.35em;
  font-size: 1.25rem;
  text-align: center;
}

.twofa-secondary {
  margin-top: var(--space-4);
}

/* ---------- RA bills (ADR-0022) ---------- */

/* Bill status pills — same one-consistent-color-per-value convention as the
   PO approval pills (.pill-draft is shared with PO drafts). "ready" reads
   green like an approved PO (validation passed), "submitted" the same blue as
   a sent PO, "certified" the teal of final approval — the record is closed. */
.pill-ready {
  background: var(--color-good-tint);
  color: var(--color-good);
}

.pill-submitted {
  background: var(--color-info-tint);
  color: var(--color-info);
}

/* Back from the client for revision — quantities open again, gate re-runs on
   re-submit. Amber: attention, not an error (the loop is the normal case). */
.pill-revising {
  background: #fff3e0;
  color: #b26a00;
}

.pill-certified {
  background: #e0f2f1;
  color: #00695c;
}

/* The certified-quantity input on the bill entry screen — the one typed
   figure per row. Narrow so the computed columns stay readable beside it. */
.input--qty {
  width: 7rem;
}

/* A row billed beyond its contracted quantity (rule 5 — a WARNING, never a
   block). Subtle tint, matching .pill-over-billed's orange family: worth
   noticing, deliberately not the red of an error. */
.row-over-billed td {
  background: #fff8f2;
}

/* Settlement pills (ADR-0029) — how much of an invoice or RA bill has
   actually been settled by payment allocations. One consistent color per
   value across payables, receivables and the payment/receipt screens:
   paid reuses the clear-to-pay green (money done), part-paid the amber of
   worth-a-look, unpaid the neutral grey of a state that isn't an error. */
.pill-paid {
  background: var(--color-good-tint);
  color: var(--color-good);
}
.pill-part-paid {
  background: var(--color-warn-tint);
  color: var(--color-warn);
}
.pill-unpaid {
  background: #eceff1;
  color: #455a64;
}

/* Low-key chips on a payment/receipt row (ADR-0029), same subtle treatment
   as .pill-unverified: labels, not alerts. Refundable = a security deposit,
   money out but not cost; unreconciled = the receipt's cash + TDS +
   retention + other doesn't equal what it settled — a warning to chase,
   never a block. */
.pill-refundable {
  background: #eef4fb;
  color: #4a6b8a;
  font-size: 0.6875rem;
  font-weight: 600;
  padding: 1px var(--space-2);
}
.pill-unreconciled {
  background: var(--color-warn-tint);
  color: var(--color-warn);
  font-size: 0.6875rem;
  font-weight: 600;
  padding: 1px var(--space-2);
}

/* ---------- Subcontractor work orders (ADR-0032) ---------- */

/* Work order status pills, one consistent color per value like every other
   status here. .pill-draft and .pill-closed are shared with PO drafts and
   closed lines. "issued" reads the same blue as a sent PO — the document is
   out with the other party; "extended" is the teal of a record that carries
   on into a successor. */
.pill-issued {
  background: var(--color-info-tint);
  color: var(--color-info);
}

.pill-extended {
  background: #e0f2f1;
  color: #00695c;
}

/* A certification that has been billed on a subcontractor invoice — the same
   green as .pill-paid: that period is done and moves to the money side. */
.pill-invoiced {
  background: var(--color-good-tint);
  color: var(--color-good);
}

/* A work order line certified past the quantity ordered — flagged, never
   blocked, exactly as an over-billed RA bill row is, and deliberately the
   same tint as .row-over-billed rather than the red of an error. */
.row-over-certified td {
  background: #fff8f2;
}

/* ---------- Line stage status (ADR-0035) ---------- */

/* One consistent color per stage status, the lifecycle read left to right:
   pending is the neutral grey of not-an-error, due the amber of complete-and-
   billable (bill it), billed the same blue as a submitted bill. "certified"
   and "paid" reuse .pill-certified / .pill-paid above — the same words mean
   the same thing on a bill and on the stage that rode it. */
.pill-pending {
  background: #eceff1;
  color: #455a64;
}

.pill-due {
  background: var(--color-warn-tint);
  color: var(--color-warn);
}

.pill-billed {
  background: var(--color-info-tint);
  color: var(--color-info);
}

/* ---------- Goods receipt inspection (ADR-0044) ---------- */

/* One consistent colour per inspection outcome, borrowing the three-way-match
   palette so "fine / worth a look / blocks a figure" reads the same on the
   receipts screen as it does on the match. Green: the whole delivery was taken
   in. Red-orange: damaged — some of it can't be used. Red: short supplied, the
   same red short-received uses on the match, because it is the same shortfall
   seen one step earlier. "Pending" reuses .pill-pending's neutral grey (a
   state, not an error — material is inspected after it is booked in) and
   "rejected" reuses .pill-rejected's red, the same word meaning the same thing
   here as on a PO. */
.pill-passed {
  background: var(--color-good-tint);
  color: var(--color-good);
}

.pill-damaged {
  background: #fbe9e7;
  color: #d84315;
}

.pill-short-supplied {
  background: var(--color-bad-tint);
  color: var(--color-bad);
}

/* ---------- PO cover summary edit fields ---------- */

/* The cover editor's inputs inherit the global `width: 100%` rule, so their
   real width is whatever the table column collapses to — and "Qty" and "Unit"
   are short headers, so those two columns shrank to about four characters.
   Procurement could not read "12500.75" or "Running Mtr" while typing it.
   Explicit min-widths per column let each cell hold its own content; the
   surrounding .table-wrap already scrolls horizontally when the row no longer
   fits, so nothing is clipped. */
.cover-input {
  min-width: 6rem;
  /* The Amount cell is type="number", which the global input rule above only
     covers for text/password/date/file — without this it would print as an
     unstyled browser default beside six styled siblings. */
  width: 100%;
  min-height: var(--touch-min);
  padding: 0 var(--space-3);
  border: 1.5px solid var(--color-border);
  border-radius: var(--radius-md);
  font-size: 0.9375rem;
  font-family: var(--font-sans);
  background: var(--color-surface);
  color: var(--color-text);
}

.cover-input:focus {
  outline: none;
  border-color: var(--color-primary);
  box-shadow: 0 0 0 3px rgba(10, 132, 255, 0.18);
}

.cover-input--part {
  min-width: 8rem;
}

.cover-input--material {
  min-width: 28rem;
}

.cover-input--sor {
  min-width: 14rem;
}

.cover-input--qty {
  min-width: 9rem;
}

.cover-input--unit {
  min-width: 9rem;
}

.cover-input--amount {
  min-width: 10rem;
}

/* ---------- Landing-screen shared components (project rows, urgency bands,
   three-state progress — dashboard/components.py and the
   templates/dashboard/_*.html partials) ---------- */

/* Urgency bands — one consistent colour per value, same convention as every
   pill family above (fine/watch/overdue reuse the approved/pending/rejected
   traffic-light hexes so urgency reads the same as approval state does). */
.pill-fine {
  background: var(--color-good-tint);
  color: var(--color-good);
}

.pill-watch {
  background: var(--color-warn-tint);
  color: var(--color-warn);
}

.pill-overdue {
  background: var(--color-bad-tint);
  color: var(--color-bad);
}

/* The needs-action count on a project row — amber, matching the existing
   landing screen's outstanding-count pills. */
.pill-needs-action {
  background: var(--color-warn-tint);
  color: var(--color-warn);
}

/* One money figure on a landing row. Tabular digits so amounts line up when
   rows stack; the negative modifier matches .stat-tile__value--negative. */
.money-cell {
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.money-cell--negative {
  color: var(--color-bad);
}

/* Three-state progress bar: done / in-flight / not-started segments grow by
   raw line COUNT (flex-grow set inline from data), so the bar needs no
   percentage maths. */
.progress-track {
  display: flex;
  height: 8px;
  margin-top: var(--space-3);
  border-radius: var(--radius-full);
  overflow: hidden;
  background: #eceff1;
}

.progress-track__seg--done {
  background: var(--color-good);
}

/* Revamp 01, two deliberate colour changes to this component. In-flight was
   amber, which is "watch" everywhere else in this file, so every busy project
   read as a warning; it is primary blue now — activity, not alarm.
   Not-started was the track colour, so the bar looked like it simply ended and
   the remaining scope was invisible. */
.progress-track__seg--in-flight {
  background: var(--color-primary);
}

.progress-track__seg--not-started {
  background: var(--color-border-strong);
}

/* A landing-screen project row: one head line always (name · client · lines
   · money · urgency), progress + action items below only when something
   needs action. Rows stack inside one .card, separated by hairlines. */
.project-row {
  padding: var(--space-3) 0;
}

.project-row:first-child {
  padding-top: 0;
}

.project-row:last-child {
  padding-bottom: 0;
}

.project-row + .project-row {
  border-top: 1px solid var(--color-border);
}

.project-row__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-3);
  min-height: var(--touch-min);
}

.project-row__name {
  font-weight: 600;
}

.project-row__client,
.project-row__lines {
  color: var(--color-text-muted);
  font-size: 0.8125rem;
}

.project-row__items {
  margin-top: var(--space-3);
}

/* Money + count on the right edge of an action item's row. */
.project-row__item-figures {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin-left: auto;
}

/* At phone width the figures stop competing with the label for one line —
   the label reads full-width and the money/wait/count row sits under it
   (the "readable on a phone" rule; side by side, the nowrap figures squeeze
   the label to a word per line). */
@media (max-width: 640px) {
  .option-row--landing {
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-2);
  }

  .option-row--landing .project-row__item-figures {
    margin-left: 0;
  }
}

/* The head line's margin figure (full money scope only — the view context
   decides, this class just lays it out beside the client total). */
.project-row__margin {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-1);
  white-space: nowrap;
}

/* ============================================================
   REVAMP 01 — WIDTH SYSTEM
   ============================================================
   A page declares its width ONCE, on <body>. The nav bar and .app-main both
   read the variable, so the brand never drifts out of alignment with the
   content the way two independent 56rem values eventually would.

   base.html:              <body class="{% block body_class %}{% endblock %}">
   a screen that opts in:  {% block body_class %}w-wide{% endblock %}
   everything else:        nothing at all — w-read (56rem) is the default.
*/

:root {
  --app-width: 56rem;          /* w-read — today's width, the default */
  --app-width-max: 160rem;     /* even w-full stops here: a 34" monitor should
                                  not produce a 3,000px table row */
  --app-gutter: var(--space-4);

  --row-h: 40px;               /* dense table row; --touch-min still governs
                                  every control and every phone card */

  /* The landing-row track list (.plist below). It lives here, not on .plist,
     so a list that has not been given a modifier still renders as a grid
     instead of collapsing to one column — and so the full-money track list
     exists exactly once. A screen with fewer money tracks says so with
     .plist--client-money / .plist--no-money. */
  --plist-cols: minmax(17rem, 2.4fr) 4.5rem 8.5rem 8rem minmax(9rem, 1.3fr) 5.5rem 9rem;
}

body.w-form { --app-width: 30rem; }
body.w-wide { --app-width: 84rem; }
body.w-full {
  --app-width: min(100%, var(--app-width-max));
  --app-gutter: var(--space-6);
}

.app-nav__bar,
.app-main {
  max-width: var(--app-width);
  margin-inline: auto;
}

.app-main {
  padding: var(--space-6) var(--app-gutter) var(--space-8);
}

/* Width is for data, never for sentences: running text stops at a readable
   measure however wide the page is. Deliberately NOT .card-subtitle — a
   subtitle is a caption on the card it heads, and capping it narrowed every
   subtitle on every screen, most of which are nowhere near a wide page. A
   screen with real running text opts in with .prose. */
.prose,
.empty-state__body {
  max-width: 72ch;
}

@media (max-width: 640px) {
  :root { --app-width: 100%; --app-gutter: var(--space-4); }
  body.w-form, body.w-wide, body.w-full { --app-width: 100%; --app-gutter: var(--space-4); }
}

/* ============================================================
   REVAMP 01 — THE WAIT CHIP
   ============================================================
   Urgency is a pair — days waiting and rupees at stake — and only the days
   are coloured. Band comes from the existing .pill-fine / .pill-watch /
   .pill-overdue classes, so a chip is <span class="wait-chip pill-overdue">.

   Two scales, both server-side (see dashboard/urgency.py):
     internal   fine <= 3d   watch 4-7d    overdue > 7d
     client     fine <= 30d  watch 31-60d  overdue > 60d
   The SCALE BELONGS TO THE COLUMN, not the chip: a column headed "Waiting on
   the client" bands on the client scale, so 45d there is green while 45d in a
   "Waiting on us" column is red. Only a mixed list (the GM's) puts the party
   in the chip itself. */

.wait-chip {
  display: inline-flex;
  align-items: center;
  padding: 2px var(--space-2);
  border-radius: var(--radius-full);
  font-size: 0.75rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Nothing is at stake yet (an unreviewed vendor) — a duration, not a wait. */
.wait-chip--unscaled {
  background: #f5f5f5;
  color: #757575;
}

/* ============================================================
   REVAMP 01 — PROGRESS ROW
   ============================================================
   Bar + percentage on one line. Replaces the per-row caption: on a 12-row
   list that caption was 12 restatements of one legend, so the legend moves to
   the list header (.progress-legend) and the row keeps a number. The two
   segment colour changes that came with it are on the component itself, up
   in the landing-screen section. */

.progress-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.progress-row .progress-track { flex: 1 1 auto; height: 6px; margin-top: 0; }
.progress-row__pct {
  flex: none;
  font-size: 0.75rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--color-text-muted);
}

/* The page-header size. */
.progress-track--lg { height: 10px; }

.progress-legend {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  font-size: 0.75rem;
  color: var(--color-text-muted);
}

.progress-legend__key {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
}

.progress-legend__swatch {
  width: 10px;
  height: 6px;
  border-radius: 2px;
  display: inline-block;
}

/* ============================================================
   REVAMP 01 — LANDING ROWS: one grid, three uses
   ============================================================
   Eight fields cannot fit in 896px of wrapping flexbox; that is why the
   dashboard rows read as run-on text. Header strip, project row and action row
   share one track list, so figures line up down the page and a row is scanned
   by column instead of read as a sentence.

   Track order: project · lines · client value · [margin] · progress · worst
   wait · needs action. A role that cannot see margin gets a DIFFERENT track
   list — never a blanked cell:

     <div class="card plist plist--client-money">
*/

.plist--client-money {
  /* coordinator: no margin track at all */
  --plist-cols: minmax(17rem, 2.4fr) 4.5rem 9rem minmax(9rem, 1.3fr) 5.5rem 9rem;
}

.plist--no-money {
  /* site: no value track either */
  --plist-cols: minmax(17rem, 2.6fr) 4.5rem minmax(9rem, 1.3fr) 5.5rem 9rem;
}

.plist__head,
.plist__row {
  display: grid;
  grid-template-columns: var(--plist-cols);
  gap: var(--space-4);
  align-items: center;
  padding: var(--space-2) var(--space-3);
}

.plist__head {
  /* ALIGNMENT — paired with .plist__row below, and the pairing is the whole
     reason a caption lands over its figure: this strip's content starts at its
     padding-inline (--space-3), and the row's rail offset is built to land on
     exactly that number. Change this padding without changing the row's and
     the header quietly stops lining up — nothing errors, it just drifts. */
  padding-block: 0 var(--space-2);
  border-bottom: 1px solid var(--color-border);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}

.plist__row {
  min-height: 56px;
  /* The rail is the ranking, restated: it repeats what the wait chip already
     says, so the eye can find the top of the trouble without reading a
     number. Transparent by default — a quiet project gets no ornament. */
  border-left: 3px solid transparent;
  /* ALIGNMENT — paired with .plist__head above. The rail hangs the row out into
     the card's own padding and the row pays it straight back:
       -(--space-6) + 3px border + (--space-6 + --space-3 - 3px) = --space-3,
     which is exactly .plist__head's padding-inline, so every caption sits over
     its figure. The two cancel by construction — change one number here and the
     header silently stops lining up with the rows under it. */
  margin-left: calc(-1 * var(--space-6));
  padding-left: calc(var(--space-6) + var(--space-3) - 3px);
}

.plist__row--overdue { border-left-color: var(--color-bad); }
.plist__row--watch { border-left-color: var(--color-warn); }

.plist__group { border-bottom: 1px solid var(--color-border); }
.plist__group:last-child { border-bottom: none; }

.plist__name {
  font-weight: 600;
  font-size: 0.9375rem;
  color: var(--color-text);
  display: block;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.plist__sub {
  font-size: 0.8125rem;
  color: var(--color-text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.plist__num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.plist__num--muted { color: var(--color-text-muted); font-size: 0.875rem; }

/* The per-cell caption a row carries for the phone. OFF here, deliberately:
   above the breakpoint .plist__head is naming that column, and a label would
   name the same figure twice. It switches on in the one media query that hides
   the strip (bottom of this section) — the two are a single decision, so they
   must stay in the same block. The words come from one table
   (dashboard/templatetags/plist_extras.py) which the strip and the row both
   read, so a caption and its label cannot drift apart. */
.plist__label { display: none; }

/* Action rows — the queues nested under a project row. Its own track list:
   label · count · value · worst wait · chevron. A project with nine of these
   shows its three worst and links to the rest; nine rows times twelve
   projects is the current dashboard's problem restated, not solved. */
.plist__actions {
  padding: 0 var(--space-3) var(--space-2) 2.25rem;
}

.action-row {
  display: grid;
  grid-template-columns: 1fr 3.5rem 8.5rem 5.5rem 1.25rem;
  gap: var(--space-4);
  align-items: center;
  min-height: var(--row-h);
  font-size: 0.875rem;
  border-top: 1px solid var(--color-surface-muted);
  color: inherit;
}

.action-row:hover { text-decoration: none; background: var(--color-surface-muted); }

/* Queue-first landing (coordinator, procurement): the same row, one level up —
   the queue is the subject and the project is a detail inside it. */
.qlist__head,
.qlist__row {
  display: grid;
  grid-template-columns: 1fr 4rem 9rem 6.5rem 1.5rem;
  gap: var(--space-4);
  align-items: center;
  padding: var(--space-2) var(--space-3);
}

.qlist__row {
  min-height: 48px;
  border-left: 3px solid transparent;
  margin-left: calc(-1 * var(--space-6));
  padding-left: calc(var(--space-6) + var(--space-3) - 3px);
}

.qlist__row--overdue { border-left-color: var(--color-bad); }
.qlist__row--watch { border-left-color: var(--color-warn); }
.qlist__label { font-weight: 600; font-size: 0.9375rem; }
.qlist__hint { font-size: 0.8125rem; color: var(--color-text-muted); }

/* The disclosure is the only control on a queue header, and a bare glyph is a
   10px tap target — on a phone, where the grid stacks and it lands on a line of
   its own, that is the whole row's affordance. Give it the house minimum. */
.qlist__toggle {
  display: inline-flex;
  align-items: center;
  justify-content: flex-end;
  min-height: var(--touch-min);
  min-width: var(--touch-min);
}

/* The one control that switches a landing between its two shapes. Two buttons
   side by side, not a stacked pair: it is a choice between two views of the
   same list, and two full-width blocks read as two separate destinations. */
.shape-toggle {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-4);
}

.shape-toggle .btn { flex: 0 1 12rem; }

@media (max-width: 480px) {
  .shape-toggle .btn { flex: 1 1 0; }
}

/* The disclosure swaps a queue's header and its open rows as ONE thing, so the
   two live inside a group — the same wrapper .plist__group is for a project
   and its action rows. */
.qlist__group { border-bottom: 1px solid var(--color-border); }
.qlist__group:last-child { border-bottom: none; }

/* An informational queue is news, not work: material that arrived is already
   handled by the time the row exists. It ranks below everything actionable and
   reads back — greyed, no rail, no urgency colour — so a week of ordinary
   deliveries cannot make a quiet screen look busy (ledger H-12). */
.qlist__row--quiet,
.action-row--quiet {
  color: var(--color-text-muted);
  border-left-color: transparent;
}

.qlist__row--quiet .qlist__label { font-weight: 400; }
.qlist__row--quiet .money-cell,
.action-row--quiet .money-cell { color: inherit; }

/* A quiet project collapses to one line, and below the ranked list the quiet
   ones collapse further into a single disclosure. Ranked lists earn their
   length. */
.plist__quiet {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  min-height: var(--touch-min);
  padding: var(--space-3) var(--space-3) 0;
  font-size: 0.875rem;
  color: var(--color-text-muted);
}

.plist__quiet-figures {
  display: flex;
  align-items: center;
  gap: var(--space-6);
  font-variant-numeric: tabular-nums;
}

/* ============================================================
   ADR-0128 — THE GM LANDING LEADS WITH WHAT NEEDS YOU
   ============================================================
   Three count cards over one open queue, and the portfolio demoted to a
   disclosure at the foot. The cards ARE the queue headers this screen used to
   stack in a grid: a count belongs at the size of the answer, and the sub-line
   under it (money at stake, plus either the oldest wait or how far the problem
   is spread) is what makes the number decidable without opening anything.

   Figures here are set in --font-mono. On a screen whose whole job is
   comparing four numbers down a column, a proportional face makes 11 look
   wider than 4; tabular-nums fixes the widths but not the rhythm.

   THIS BLOCK SITS ABOVE THE 900px QUERY ON PURPOSE. That query collapses both
   grids below at the same specificity, so it only wins on source order — put
   these rules after it and the cards stay three-across on a phone with their
   figures clipped to nothing, which is exactly what happened first time. */

.gm-cards__title {
  /* The cards are the heading a sighted reader sees. This one is for the rest. */
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.gm-cards {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-3);
  margin-top: var(--space-4);
}

.gm-card {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-1);
  text-align: left;
  padding: var(--space-4) var(--space-5) var(--space-4);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-card);
  font: inherit;
  color: inherit;
  cursor: pointer;
  min-height: var(--touch-min);
}

.gm-card:hover { border-color: var(--color-border-strong); }

/* The ring says WHICH card the panel below belongs to, and says it without
   colour: colour on this screen means urgency (the wait chip's band), and a
   second meaning for it would make both unreadable. */
.gm-card--active {
  border-color: var(--color-text);
  box-shadow: inset 0 0 0 1px var(--color-text), var(--shadow-card);
}

.gm-card__count {
  font-family: var(--font-mono);
  font-size: 2.75rem;
  line-height: 1;
  font-weight: 500;
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
}

/* A queue that needs nothing states its nought quietly — a clear queue is good
   news, and printing it in the same weight as four waiting POs is how a
   dashboard teaches people to stop reading it. */
.gm-card--clear .gm-card__count { color: var(--color-text-muted); font-weight: 400; }

.gm-card__label {
  font-size: 0.9375rem;
  font-weight: 600;
  margin-top: var(--space-3);
}

.gm-card__sub {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  color: var(--color-text-muted);
  font-variant-numeric: tabular-nums;
}

.gm-card__sub .money-cell { font-size: inherit; color: inherit; font-weight: 500; }

/* One item of the open queue: what it is and whose it is · how long · what it
   is worth · the one control. Its own track list rather than .qlist__row's,
   because the last cell here is a button and not a disclosure triangle. */
.gmq__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 5.5rem 9rem 11rem;
  gap: var(--space-4);
  align-items: center;
  min-height: 52px;
  padding: var(--space-2) var(--space-3);
  border-top: 1px solid var(--color-surface-muted);
  border-left: 3px solid transparent;
  font-size: 0.875rem;
}

.gmq__row--overdue { border-left-color: var(--color-bad); }
.gmq__row--watch { border-left-color: var(--color-warn); }

.gmq__subject {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  min-width: 0;
}

.gmq__name {
  font-family: var(--font-mono);
  font-weight: 600;
  font-size: 0.875rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.gmq__project {
  font-weight: 600;
  color: var(--color-text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.gmq__detail {
  font-size: 0.8125rem;
  color: var(--color-text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.gmq__row .money-cell,
.gmq__row .wait-chip { font-family: var(--font-mono); }

.gmq__act { display: flex; justify-content: flex-end; }

.gmq__none { margin-top: 0; }

/* The portfolio, demoted (ADR-0128). Its rows are unchanged — this is the head
   line that opens them, and it carries the totals so a shut portfolio still
   answers "what is the house worth". */
.gm-portfolio__head {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  width: 100%;
  padding: 0;
  background: none;
  border: 0;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
  min-height: var(--touch-min);
}

.gm-portfolio__title { display: block; font-size: 0.9375rem; font-weight: 600; }

.gm-portfolio__sub {
  display: block;
  margin-top: var(--space-1);
  font-family: var(--font-mono);
  font-size: 0.75rem;
  color: var(--color-text-muted);
  font-variant-numeric: tabular-nums;
}

.gm-portfolio__toggle {
  margin-left: auto;
  flex: none;
  font-size: 0.8125rem;
  color: var(--color-primary);
}

.gm-portfolio__rows { margin-top: var(--space-4); }

/* Stated rather than left to the user agent: .plist__row and its family set
   `display`, so a hidden wrapper inside this card must be told to stay hidden
   by a rule with at least their specificity. */
.gm-portfolio__rows[hidden] { display: none; }

@media (max-width: 900px) {
  /* Below the wide breakpoint the grid stops being a grid: label first, then
     the figures on their own line — the same decision .option-row--landing
     already makes at 640px, applied to the whole family. */
  .plist__head, .qlist__head { display: none; }
  .plist__row, .qlist__row, .action-row {
    grid-template-columns: 1fr;
    gap: var(--space-1);
    margin-left: 0;
    padding-left: var(--space-3);
  }
  .plist__num, .action-row > *:not(:first-child) { text-align: left; }

  /* The two affordances in these grids, once the grid has stacked. The row's
     chevron is decoration — the whole row is the link, and on its own line it
     is a stray arrow — so it goes. The disclosure is the opposite: it is the
     only thing on a queue header you can press, so it fills the line it landed
     on and keeps its 44px. */
  .action-row > [aria-hidden="true"] { display: none; }
  .qlist__toggle { justify-content: flex-start; width: 100%; }

  /* The GM landing's item rows (ADR-0128) stack the same way and for the same
     reason — their cells carry the same .plist__label captions, switched on
     below. The action is the exception: it is a button, so it keeps its 44px
     and fills the line it landed on rather than shrinking to its text. */
  .gm-cards { grid-template-columns: 1fr; }
  .gmq__row {
    grid-template-columns: 1fr;
    gap: var(--space-1);
  }
  .gmq__act { justify-content: stretch; }
  .gmq__act .btn { width: 100%; min-height: var(--touch-min); }

  /* With the strip gone, every cell names itself — otherwise a row here reads
     "8 / ₹1,00,00,000 / ₹21,60,000" and nothing says which is which. Inline,
     not stacked: a caption on its own line would turn a seven-field row into
     fourteen lines and put the one-handed reader back to scrolling past the
     figure they came for. The project cell is the exception and stacks by
     itself, because the name under it is a block. */
  .plist__label {
    display: inline;
    margin-right: var(--space-2);
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--color-text-muted);
  }
}

/* One line of text that must not push its grid track wider than the track is.
   The triple was already written out inside .plist__name and .plist__sub; this
   is the same three declarations for text that has no component of its own. */
.text-truncate {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}

/* The GM landing keeps ONE mutable control: the inline issue-resolve form
   (ledger H-04 — deliberate in SPEC, which overturned its removal). It sits in
   the row's own grid rather than on a third line under it, so thirty open
   issues stay thirty rows; the remark field opens as a sibling row beneath,
   and the remark is required — a flag cannot be cleared silently. */
.issue-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 8.5rem 5.5rem 8rem;
  gap: var(--space-4);
  align-items: center;
  min-height: var(--touch-min);
  font-size: 0.875rem;
  border-top: 1px solid var(--color-surface-muted);
}

.issue-row__label {
  min-width: 0;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
}

.issue-resolve {
  border-top: 1px solid var(--color-surface-muted);
  background: var(--color-surface-muted);
  padding: var(--space-2) 0 var(--space-2) var(--space-6);
}

.issue-resolve__form { display: flex; align-items: center; gap: var(--space-2); }

.issue-resolve__form input[type="text"] {
  flex: 1 1 auto;
  min-height: 40px;
  padding-inline: var(--space-3);
  background: var(--color-surface);
  border: 1.5px solid var(--color-primary);
  border-radius: var(--radius-md);
  font: inherit;
  font-size: 0.875rem;
  min-width: 0;
}

@media (max-width: 640px) {
  .issue-row { grid-template-columns: minmax(0, 1fr); gap: var(--space-1); }
  .issue-resolve { padding-left: var(--space-3); }
  .issue-resolve__form { flex-wrap: wrap; }
  .issue-resolve__form .btn { flex: 1 1 auto; min-height: var(--touch-min); }
}


/* ============================================================
   REVAMP 01 — PROJECT-PAGE CONTROLS (not wired yet)
   ============================================================
   Search is the first thing on the Lines tab and the loudest control on it:
   tabs cost Cmd-F across the project (ledger H-22, accepted), so the thing
   that replaces it cannot be a 40px box sharing a row with six filters.
   Focus ring visible at rest; "/" focuses it from anywhere on the page. */
.line-search {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex: 1 1 auto;
  min-height: 52px;
  padding-inline: var(--space-4);
  background: var(--color-surface);
  border: 1.5px solid var(--color-primary);
  border-radius: var(--radius-md);
  box-shadow: 0 0 0 3px rgba(10, 132, 255, 0.12);
}

.line-search input {
  flex: 1 1 auto;
  border: 0;
  outline: 0;
  background: none;
  font-size: 1.0625rem;
  font-family: inherit;
  color: var(--color-text);
  min-width: 0;
}

.line-search__key {
  flex: none;
  font: 600 0.6875rem ui-monospace, SFMono-Regular, Menlo, monospace;
  color: var(--color-text-muted);
  background: var(--color-surface-muted);
  border: 1px solid var(--color-border);
  border-radius: 6px;
  padding: 3px 6px;
}

.line-toolbar { display: flex; flex-direction: column; gap: var(--space-3); }
.line-toolbar__filters { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }

/* Which four figures sit in the project header is the USER's choice (ledger
   H-09): four because at eleven nothing is a headline, not because the header
   is short of room — so the picker enforces exactly four and picking a fifth
   asks which one it replaces. Per-role default, then per-device preference,
   stored the same way as the column preset. */
.figure-picker {
  max-width: 44rem;
  background: var(--color-surface);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  padding: var(--space-4) var(--space-5);
}

.figure-picker__grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-1) var(--space-4);
  font-size: 0.875rem;
}

.figure-picker__grid label {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 32px;
}

.figure-picker__grid label:has(input:not(:checked)) { color: var(--color-text-muted); }

.figure-picker__foot {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-top: var(--space-3);
}

@media (max-width: 640px) {
  .figure-picker__grid { grid-template-columns: minmax(0, 1fr); }
  .figure-picker__grid label { min-height: var(--touch-min); }
}

/* ============================================================
   REVAMP 01 — DENSE TABLE
   ============================================================
   Mostly not wired to any screen yet — .app-table--dense, the grouped header
   row and the phone card list are all still waiting for the dense line table.
   The frozen lead group below IS wired, to the project's line table (ADR-0131).

   Column PRESETS are a server concern, not a CSS one: the view emits only the
   columns in the chosen preset (?cols=site|commercial|billing|all), which
   sends fewer bytes and makes the role gate structural — a coordinator's HTML
   has no vendor-rate cell to un-hide. The existing .hide-col-* rules stay for
   the per-device "Customise" panel behind the presets. */

table.app-table--dense tbody td,
table.app-table--dense tbody th { padding: var(--space-2) var(--space-3); }
table.app-table--dense tbody tr { height: var(--row-h); }

/* Grouped header row above the column names: Quantity · Sourcing · Client
   money · Vendor money · State. It is what makes 17 columns legible, and it is
   also the money boundary — the whole "Vendor money" group is absent for a
   coordinator. */
table.app-table thead tr.col-groups th {
  top: 0;
  padding-block: var(--space-1);
  font-size: 0.625rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-text-muted);
  border-bottom: none;
}

table.app-table th.col-group-start,
table.app-table td.col-group-start {
  box-shadow: inset 2px 0 0 var(--color-border-strong);
}

/* Checkbox + item no. freeze as one group: scrolling fifteen columns to the
   right must never cost you which row you are on. Written in revamp 01 and
   unused until ADR-0131 wired it to the line table, which is the table it was
   always describing. (Its note about a box-shadow standing in for a
   border-right has gone: under border-collapse: collapse neither of them
   paints — see the edge rule below.)

   The DESCRIPTION is not in the group, though the note above once said it
   would be: it is only adjacent to Item no. under the commercial preset. The
   site and billing presets put Sr no (and SOR no) between them, and three
   columns that are not next to each other cannot be one frozen block at the
   left edge. Item no. is first in all four presets, which is what makes the
   group a group.

   `left` is the pair of offsets, and the second one is stated as a sibling
   rule rather than a constant on the cell: a role that cannot edit issues gets
   no checkbox column at all, and then Item no. is itself the left edge. The
   adjacent-sibling selector answers "is there a checkbox to my left" without
   anyone having to know the answer when the template is written. --frozen-lead
   is the checkbox column's own declared width, so the two cannot drift. */
table.app-table .is-frozen {
  position: sticky;
  left: 0;
  z-index: 1;
  background: var(--color-surface);
}

table.app-table th.checkbox-cell.is-frozen,
table.app-table td.checkbox-cell.is-frozen {
  width: var(--frozen-lead);
  padding: var(--space-3);
}
table.app-table .checkbox-cell.is-frozen input[type="checkbox"] {
  display: block;
  width: 20px;
  height: 20px;
  margin: 0;
  accent-color: var(--color-primary);
}
table.app-table .checkbox-cell.is-frozen + .is-frozen { left: var(--frozen-lead); }

table.app-table thead .is-frozen { z-index: 3; background: var(--color-surface-muted); }

/* The edge that says "this column is frozen and the next one is sliding under
   it", drawn INSIDE the cell. Neither of the two obvious ways works on a table
   cell under border-collapse: collapse — a border-right is collapsed away, and
   a box-shadow is computed and then never painted (checked in Chrome on this
   very table: getComputedStyle reported the shadow while the pixels at the
   cell's right edge stayed white). An absolutely positioned strip inside the
   cell is painted like any other content, and the cell is already `sticky`, so
   it is the containing block the strip needs. */
table.app-table .is-frozen--last::after {
  content: "";
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: 2px;
  background: var(--color-border-strong);
}
/* A frozen cell paints its own background, so every row state that is a
   background has to be repeated here or the frozen pair stays white while the
   rest of the row changes colour. */
table.app-table tbody tr:hover .is-frozen { background: var(--color-surface-muted); }
table.app-table tbody tr:has(input:checked) .is-frozen { background: var(--color-primary-tint); }
table.app-table tbody tr.row-over-billed .is-frozen { background: #fff8f2; }

/* Every figure right-aligned and tabular, so a column of money can be read as
   a column instead of as fourteen separate strings. */
table.app-table td.num,
table.app-table th.num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

table.app-table td.num--total { font-weight: 600; }

/* Phone: a dense table becomes a card list — the same second-rendering the
   comparison grid already does, because a 17-column table does not squeeze. */
.line-cards { display: none; }

@media (max-width: 640px) {
  .line-table-desktop { display: none; }
  .line-cards { display: flex; flex-direction: column; gap: var(--space-2); }
  .line-card {
    background: var(--color-surface);
    border-radius: var(--radius-md);
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
    padding: var(--space-3);
    min-height: var(--touch-min);
  }
  .line-card--flagged { border-left: 3px solid var(--color-bad); }
  .line-card__head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-2);
  }
  .line-card__figures {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1) var(--space-3);
    margin-top: var(--space-2);
    font-size: 0.8125rem;
    color: var(--color-text-muted);
    font-variant-numeric: tabular-nums;
  }
}

/* ============================================================
   REVAMP 01 — PROJECT PAGE CHROME: header, tabs, selection tray
   ============================================================
   Mounted 2026-08-04 (ADR-0048). Ten stacked cards became one persistent frame
   and seven tabs; the panels are the same templates, so the lines are the first
   thing on the page instead of the tenth. Tabs are real URLs (?tab=lines)
   swapped with htmx: linkable, back-button safe, and each panel still renders
   standalone for a direct hit or a non-JS client.

   The frame is sticky and carries only what is true of the project on EVERY
   tab: its name and client, the single worst exception on it, four money
   figures, progress, the tab strip and the role's actions. The selection tray
   below is still unwired — that is session two. */

/* Deliberately NOT sticky here. The frame has to stick UNDER the app nav,
   which is itself sticky at 0 with a higher z-index and which WRAPS onto two
   or three lines as the window narrows — it measured 112px at 1280px wide, not
   the 60px a literal would have guessed. app.js gives this element its
   position and its offset from the nav's own rendered height, re-measured on
   resize, the same way the synced table scrollbars already do. Without
   JavaScript the frame simply scrolls with the page, which is a working page;
   a wrong constant would put it half under the nav, which is not.

   .rec-head is the same frame with a different subject — the detail screens'
   header, session four. It shares this component's band, its inner width and
   its bar rather than restating them, and app.js positions whichever of the
   two a page has. The two are one component; only the contents differ. */
.proj-head,
.rec-head {
  background: var(--color-surface);
  border-bottom: 1px solid var(--color-border);
  z-index: 20;
  /* Flush with the content edges rather than inset from them, so the frame
     reads as one band instead of a floating card. */
  margin-inline: calc(var(--app-gutter) * -1);
  margin-top: calc(var(--space-6) * -1);
}

.proj-head__bar,
.proj-head__figures,
.proj-head__progress,
.rec-head__bar,
.rec-head__figures,
.rec-head__track,
.tabs {
  max-width: var(--app-width);
  margin-inline: auto;
  padding-inline: var(--app-gutter);
}

.proj-head__bar,
.rec-head__bar {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-8);
  padding-block: var(--space-4) var(--space-3);
}

.proj-head__name {
  font-size: 1.375rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  margin: 0;
}

/* Client, code, status and the exception on one line under the name: the
   exception is promoted OUT of a pill under the money card to sit beside what
   it is an exception to. */
.proj-head__meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: var(--space-1) 0 0;
  font-size: 0.875rem;
  color: var(--color-text-muted);
}

.proj-head__meta a.pill:hover { text-decoration: none; filter: brightness(0.96); }

.proj-head__actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex: none;
}

.proj-head__progress { padding-block: 0 var(--space-3); }

.proj-head__figures {
  display: flex;
  gap: var(--space-8);
  border-top: 1px solid var(--color-surface-muted);
  padding-block: var(--space-3);
  align-items: center;
}

.proj-figure { min-width: 0; }
.proj-figure__label { font-size: 0.75rem; color: var(--color-text-muted); }
.proj-figure__value {
  font-size: 1.125rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  margin-top: 1px;
}
.proj-figure__value--negative { color: var(--color-bad); }
.proj-figure__note { font-size: 0.75rem; color: var(--color-text-muted); }

/* Four figures, then a disclosure for the rest. Eleven figures in one strip is
   a reference table, not a summary. The disclosure is pushed to the end of the
   strip so the figures stay left-aligned as their number changes. */
.proj-figures__disclosure { position: relative; margin-left: auto; }

.proj-figures__more {
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--color-primary);
  min-height: var(--touch-min);
  display: inline-flex;
  align-items: center;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  font-family: inherit;
}

/* The figure picker is a POPOVER off that disclosure, not a card in the page
   (owner's ruling): which four figures head the page is a setting touched once
   a month, and a setting does not get three paragraphs of body text. Each row
   carries its figure's value as well as its checkbox, so this is also where the
   unchosen figures stay readable — one disclosure, not two. */
.figure-pop {
  position: absolute;
  right: 0;
  top: calc(100% + var(--space-2));
  z-index: 30;
  width: 22rem;
  max-width: calc(100vw - var(--space-4) * 2);
  background: var(--color-surface);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  padding: var(--space-3);
}

.figure-pop[hidden] { display: none; }

.figure-pop__list { display: flex; flex-direction: column; }

.figure-pop__row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--touch-min);
  font-size: 0.8125rem;
}

.figure-pop__row:has(input:not(:checked)) .figure-pop__label { color: var(--color-text-muted); }
.figure-pop__value { font-variant-numeric: tabular-nums; color: var(--color-text-muted); }

.figure-pop__foot {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-surface-muted);
}

/* Three equal Export buttons become one menu with three items. <details> so it
   opens with no JavaScript at all. */
.menu { position: relative; }
.menu > summary { list-style: none; cursor: pointer; }
.menu > summary::-webkit-details-marker { display: none; }

.menu__items {
  position: absolute;
  right: 0;
  top: calc(100% + var(--space-1));
  z-index: 30;
  min-width: 16rem;
  background: var(--color-surface);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-card);
  padding: var(--space-1);
  display: flex;
  flex-direction: column;
}

.menu__items a {
  display: flex;
  align-items: center;
  min-height: var(--touch-min);
  padding-inline: var(--space-3);
  border-radius: var(--radius-sm);
  font-size: 0.875rem;
  color: var(--color-text);
}

.menu__items a:hover { background: var(--color-surface-muted); text-decoration: none; }

.tabs {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  border-top: 1px solid var(--color-surface-muted);
  /* Seven tabs do not fit 56rem on a narrow window, and a wrapped tab strip
     stops reading as one row of peers — it scrolls instead. */
  overflow-x: auto;
  scrollbar-width: none;
}

.tabs::-webkit-scrollbar { display: none; }

.tab {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 48px;
  padding-inline: var(--space-3);
  font-size: 0.9375rem;
  font-weight: 600;
  color: var(--color-text-muted);
  white-space: nowrap;
  flex: none;
}

.tab:hover { color: var(--color-text); text-decoration: none; }

.tab--active,
.tab--active:hover {
  color: var(--color-primary);
  box-shadow: inset 0 -2px 0 var(--color-primary);
}

.tab__count { font-weight: 400; font-variant-numeric: tabular-nums; }

/* What the search box crosses, said under it. Losing Cmd-F across the whole
   project is only acceptable if the replacement states its own reach. */
.line-search__scope {
  margin: 0;
  font-size: 0.8125rem;
  color: var(--color-text-muted);
}

/* Access is a CONTROL, not a list (owner's ruling): a role that cannot change
   it is sent no control at all, and there is nothing here to style read-only.
   What the control IS changed in revamp 01 session seven — the multi-select
   listbox became .access-list's tick rows (see "the access control" below),
   because a listbox needs a modifier key on a desktop and a long press on a
   phone. It stays one compact control either way. */
.access-form {
  display: block;
}

/* Phone. Two sticky bars on a 390px screen leave nothing for the work, so
   app.js leaves the frame unstuck below this width and only the app nav stays;
   the figures stop being a row and wrap; the tab strip still scrolls sideways. */
@media (max-width: 640px) {
  .proj-head__bar { flex-direction: column; gap: var(--space-3); }
  .proj-head__actions { width: 100%; }
  .proj-head__actions .btn,
  .proj-head__actions .menu { flex: 1 1 auto; }
  .proj-head__actions .menu > summary { width: 100%; justify-content: center; }
  .proj-head__figures {
    flex-wrap: wrap;
    gap: var(--space-3) var(--space-5);
    align-items: flex-start;
  }
  .proj-figures__disclosure { margin-left: 0; width: 100%; }
  .figure-pop { right: auto; left: 0; width: 100%; }
  .menu__items { right: auto; left: 0; width: 100%; }
}

/* Actions follow the selection. "Create PO from selected" used to be a card
   400 lines below the checkboxes; as a tray it states what is selected, what
   it is worth, and how many of those lines can actually take a PO — and it
   exists only while something is ticked. */
.sel-tray {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 40;
  background: rgba(29, 29, 31, 0.97);
  backdrop-filter: saturate(180%) blur(12px);
  -webkit-backdrop-filter: saturate(180%) blur(12px);
  color: #fff;
  padding: var(--space-3) var(--app-gutter);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-6);
}

.sel-tray__count { font-size: 0.9375rem; font-weight: 600; font-variant-numeric: tabular-nums; }
.sel-tray__note { font-size: 0.8125rem; color: rgba(255, 255, 255, 0.7); }
.sel-tray__figures { display: flex; gap: var(--space-5); font-size: 0.8125rem; }
.sel-tray__figures b { font-variant-numeric: tabular-nums; }
.sel-tray__actions { display: flex; align-items: center; gap: var(--space-2); }

.sel-tray .btn-secondary {
  background: rgba(255, 255, 255, 0.14);
  border-color: transparent;
  color: #fff;
}

.sel-tray .btn-ghost {
  background: transparent;
  border: 1.5px solid rgba(255, 255, 255, 0.3);
  color: #fff;
}

@media (max-width: 640px) {
  .sel-tray { flex-direction: column; align-items: stretch; gap: var(--space-2); }
  .sel-tray__actions .btn { flex: 1 1 auto; }
}

/* Filter chips: an applied filter is a removable chip, so the state of the
   list is visible without opening the filter card. */
.chip-filter {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  min-height: 36px;
  padding-inline: var(--space-3);
  border-radius: var(--radius-full);
  font-size: 0.8125rem;
  font-weight: 600;
  background: var(--color-primary-tint);
  border: 1.5px solid var(--color-primary);
  color: var(--color-primary);
}

.chip-filter--add {
  background: var(--color-surface);
  border-color: var(--color-border);
  color: var(--color-text-muted);
  cursor: pointer;
}

/* The ✕ is its own tap target, not a corner of the chip's text. */
.chip-filter__remove {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 24px;
  min-height: 24px;
  margin-right: calc(var(--space-2) * -1);
  border-radius: var(--radius-full);
  color: inherit;
  font-size: 0.75rem;
  line-height: 1;
}

.chip-filter__remove:hover {
  background: var(--color-primary);
  color: #fff;
  text-decoration: none;
}

/* With nothing filtered the row says so rather than sitting empty — an empty
   toolbar reads as a page still loading. */
.line-toolbar__none { font-size: 0.8125rem; }

/* The Add-filter popover holds the fields the chips stand for. Wider than a
   plain menu because it is a form, and scrollable because a project with many
   sections and categories can outgrow the viewport. */
.menu__items--form {
  min-width: 20rem;
  gap: var(--space-3);
  padding: var(--space-4);
  max-height: 70vh;
  overflow-y: auto;
}

.sel-tray__facts {
  display: flex;
  align-items: center;
  gap: var(--space-5);
  flex-wrap: wrap;
  min-width: 0;
}

/* A fixed tray sits over the last rows of the table; this is the room it needs
   at the foot of the panel so nothing is unreachable behind it. */
.sel-tray__spacer { height: 84px; }

/* Row detail: the columns this reader's preset leaves out, opened in place. */
.line-detail {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  gap: var(--space-2) var(--space-5);
  margin: var(--space-3) 0 0;
}

.line-detail__pair { display: flex; gap: var(--space-2); font-size: 0.875rem; }
.line-detail__pair dt { color: var(--color-text-muted); flex: none; }
.line-detail__pair dd { margin: 0; font-weight: 500; min-width: 0; overflow-wrap: anywhere; }

/* ============================================================
   REVAMP 01 — THE DENSE LIST SCREENS
   ============================================================
   The eight wide tables (PO list, vendors, payables, receivables, vendor
   invoices, RA bills, work orders, line history) on the toolbar-plus-table
   pattern the lines tab arrived at. Everything here is about telling three
   axes apart at a glance: a cool lifecycle pill (where it is), one red
   exception flag (somebody must act) and quiet structure chips (what it is).
   The colours themselves come from list_screens.py, not from this file — CSS
   draws what that module decided.

   Money alignment needs nothing new: .num above already right-aligns and sets
   tabular figures on every .app-table, and _list_head.html puts that class on
   every column the caption table marks `num`. So a rupee column lines up digit
   for digit whatever a row's lakh separators do. */

table.app-table--list th[data-col="detail"],
table.app-table--list td[data-col="detail"] {
  width: 1%;
  white-space: nowrap;
}

/* Two cool lifecycle states the shared pills had no cool version of. Both are
   places, not alarms: .pill-rejected is red because a PO detail page is
   reporting an outcome, but on a LIST the red belongs to the exception flag
   beside it — one red per row, and it has to be the one saying what to do. */
.pill-returned {
  background: #fff4e0;
  color: #9a6412;
}

.pill-revoked {
  background: #eceff1;
  color: #607d8b;
}

/* The vendor review state as a lifecycle pill. .pill-unverified stays exactly
   as it was — a small grey marker beside a vendor name in tables and pickers —
   so this is a second class rather than a change to that one. */
.pill-verified {
  background: var(--color-good-tint);
  color: var(--color-good);
}

.pill-unverified-state {
  background: #eceff1;
  color: #455a64;
}

/* Structure: what a row IS. Deliberately the quietest thing on the row —
   smaller and lower-contrast than any pill, in the same register
   .pill-unverified and .pill-mandatory already use for "this is a label, not
   an alert". */
.chip-structure {
  display: inline-block;
  background: #f5f5f5;
  color: #757575;
  border-radius: var(--radius-full);
  font-size: 0.6875rem;
  font-weight: 600;
  padding: 1px var(--space-2);
  white-space: nowrap;
}

.row-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  margin-top: 2px;
}

/* The quiet second line under a row's subject — a project's client and the
   client's own code, under the project name. The same three declarations
   .plist__sub uses on the landing, because it is the same line saying the same
   thing one level down. */
.row-sub {
  font-size: 0.8125rem;
  color: var(--color-text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The progress bar needs room to be read as a proportion; below about 8rem the
   three segments stop being distinguishable and the bar says nothing. */
table.app-table--list td[data-col="progress"] {
  min-width: 9rem;
}

/* Row detail, opened in place. Rendered open; app.js sets [hidden] on load and
   the toggle clears it, so a browser without JavaScript keeps every detail
   visible rather than losing it behind a control that cannot work. */
table.app-table--list tr.list-detail > td {
  background: var(--color-surface-muted);
  /* Undo the dense-cell rules for this one cell: a body cell is nowrap,
     ellipsised and capped at 16rem, which is right for a column of values and
     ruinous for a panel of them. */
  white-space: normal;
  max-width: none;
  overflow: visible;
}

/* The chevron is a control, and a control on a phone is 44px (design
   language). On a pointer device 44px would set the height of EVERY row on a
   screen whose whole purpose is density, so it is only that big where a thumb
   has to hit it — the same bargain .chip-filter makes below. */
.detail-toggle {
  padding: 0 var(--space-1);
  line-height: 1;
  color: var(--color-text-muted);
}

.detail-toggle[aria-expanded="false"] { transform: rotate(-90deg); }

@media (max-width: 640px) {
  /* Chips are controls, and a control on a phone is 44px (design language). */
  .chip-filter { min-height: var(--touch-min); }
  .detail-toggle { min-width: var(--touch-min); min-height: var(--touch-min); }
  .chip-filter__remove { min-width: 32px; min-height: 32px; }
  /* A popover anchored to a chip is as narrow as the chip — six unusable
     select boxes in a 100px column. On a phone the filter panel stops
     floating and opens in flow instead, where it has the toolbar's full
     width and cannot hang off the right edge whatever the chip it sits
     under. */
  .line-toolbar__filters .menu { width: 100%; }
  .line-toolbar__filters .menu > summary { justify-content: center; }
  .menu__items--form {
    position: static;
    min-width: 0;
    width: 100%;
    max-height: none;
    box-shadow: none;
  }
  .sel-tray__facts { gap: var(--space-1) var(--space-4); }
  .sel-tray__spacer { height: 148px; }
}

/* ============================================================
   REVAMP 01 — EMPTY STATES AND SYSTEM PAGES
   ============================================================
   The pattern, everywhere: what this is · why it is empty · the ways out.
   Never a bare "No records found." Mounted on every list, tab and panel that
   can be empty on 2026-08-06 — the words come from ONE table
   (dashboard/templatetags/plist_extras.py's EMPTY_STATES) through ONE partial
   (templates/dashboard/_empty_state.html), which is also where the rule that
   nobody is offered an action they cannot take is enforced. */

.empty-state {
  border: 1.5px dashed var(--color-border-strong);
  border-radius: var(--radius-md);
  background: #fafafc;
  padding: var(--space-5);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-3);
}

.empty-state__title { font-weight: 600; font-size: 1rem; }
.empty-state__body { margin: 0; font-size: 0.875rem; line-height: 1.5; color: var(--color-text-muted); }
.empty-state__actions { display: flex; flex-wrap: wrap; gap: var(--space-2); }

/* Spacing belongs to the container, not to the component, so no call site has
   to remember a utility class. Standing in for a card's content it needs the
   gap the table it replaced had; standing alone in a card it is the whole
   card, and a top margin there would only double the card's own padding;
   inside a table cell (the dense lists put it in a full-width <td>) the cell's
   padding is already the gap. */
.card > .empty-state:not(:first-child) { margin-top: var(--space-4); }

/* 403 / 404 / 500 — body class w-form, one card, one way back. */
.sys-page {
  min-height: 60vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--space-4);
}

.sys-page__code {
  font: 600 0.75rem/1 ui-monospace, SFMono-Regular, Menlo, monospace;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}

.sys-page__title { font-size: 1.5rem; font-weight: 600; margin: 0; letter-spacing: -0.01em; }

/* ============================================================
   REVAMP 01 — THE DETAIL SCREENS
   ============================================================
   The nine pages about ONE RECORD (PO, RA bill and its summary, vendor invoice
   and its match, work order, line receipts) on one frame, mounted 2026-08-05.
   Same bargain the project page struck in ADR-0048 and the eight dense lists
   struck after it: a persistent header carrying identity, the one worst
   exception, the money that matters and the lifecycle position, with
   everything else below it as content.

   The frame is the project page's frame with a different subject, so it shares
   that component's rules rather than restating them — see the .proj-head,
   .rec-head selectors above and in app.js. What is new here is the lifecycle
   TRACK: a position in a sequence rather than a lone pill. */

.rec-head__id { min-width: 0; }

.rec-head__crumbs {
  margin: 0 0 var(--space-1);
  font-size: 0.8125rem;
  color: var(--color-text-muted);
}

.rec-head__name {
  font-size: 1.375rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  margin: 0;
  /* A line description is the title on the receipts screen and it can run to a
     paragraph. Two lines of title, then an ellipsis — the full text is on the
     record below. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Facts, then the position, then the wait, then the one exception — the same
   order and the same register as .proj-head__meta, so the two frames read as
   one component with two subjects. */
.rec-head__meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: var(--space-1) 0 0;
  font-size: 0.875rem;
  color: var(--color-text-muted);
}

/* A middot between facts, drawn rather than typed, so a fact the record does
   not have leaves no orphaned separator behind it. */
.rec-head__fact + .rec-head__fact::before {
  content: "·";
  margin-right: var(--space-2);
  color: var(--color-border-strong);
}

.rec-head .row-chips { margin-top: var(--space-2); }

.rec-head__figures {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-8);
  border-top: 1px solid var(--color-surface-muted);
  padding-block: var(--space-3);
  align-items: baseline;
}

.rec-figure { min-width: 0; }
.rec-figure__label { font-size: 0.75rem; color: var(--color-text-muted); }

/* Unrounded, Indian-grouped, tabular — the same figures accounts reconciles
   against on the dense lists, so the same .money-cell inside. */
.rec-figure__value {
  font-size: 1.125rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}

.rec-figure__value--negative,
.rec-figure__value .money-cell--negative { color: var(--color-bad); }
.rec-figure__unit { font-size: 0.8125rem; font-weight: 400; color: var(--color-text-muted); }

/* ---------- The lifecycle track ----------
   Where the record is, as a position in a sequence. Cool throughout: an
   off-path step (Rejected, Revoked, Returned) borrows .pill-returned's amber
   and a revoked one .pill-revoked's slate, because both are places and neither
   is an alarm — the red on this page is the exception flag in the bar above.

   It scrolls inside itself on a narrow screen rather than wrapping mid-step:
   a six-step PO sequence broken across two lines stops reading as a sequence. */
.rec-head__track { padding-block: 0 var(--space-3); }

.lc-track {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
}

.lc-track__steps {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  list-style: none;
  margin: 0;
  padding: 0;
  overflow-x: auto;
  scrollbar-width: none;
  min-width: 0;
}

.lc-track__steps::-webkit-scrollbar { display: none; }

.lc-step {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 2px var(--space-2) 2px var(--space-1);
  border-radius: var(--radius-full);
  font-size: 0.75rem;
  font-weight: 600;
  white-space: nowrap;
  color: var(--color-text-muted);
}

/* The connector between steps, so a row of pills reads as one sequence. */
.lc-step + .lc-step::before {
  content: "";
  width: var(--space-3);
  height: 1px;
  background: var(--color-border-strong);
  margin-right: var(--space-1);
  flex: none;
}

.lc-step__mark {
  width: 8px;
  height: 8px;
  border-radius: var(--radius-full);
  border: 1.5px solid var(--color-border-strong);
  flex: none;
}

.lc-step--done { color: var(--color-text); }
.lc-step--done .lc-step__mark { background: var(--color-primary); border-color: var(--color-primary); }

/* The one step drawn as a filled pill: a reader's eye should land on where the
   record IS before it reads anything else in the sequence. */
.lc-step--current {
  background: var(--color-primary-tint);
  color: var(--color-info);
}

.lc-step--current .lc-step__mark {
  background: var(--color-primary);
  border-color: var(--color-primary);
  box-shadow: 0 0 0 3px rgba(10, 132, 255, 0.18);
}

.lc-step--ahead .lc-step__mark { background: var(--color-surface); }

.lc-step--off {
  background: #fff4e0;
  color: #9a6412;
}

.lc-step--off .lc-step__mark { background: #9a6412; border-color: #9a6412; }

/* A step this SCREEN cannot resolve — the invoice detail page cannot run the
   three-way match. Drawn as a question rather than guessed either way. */
.lc-step--unknown { color: var(--color-text-muted); font-style: italic; }
.lc-step--unknown .lc-step__mark {
  border-style: dashed;
  background: var(--color-surface);
}

.lc-track__next {
  margin: 0;
  font-size: 0.75rem;
  color: var(--color-text-muted);
  white-space: nowrap;
}

/* ---------- Action groups, split by consequence ----------
   Routine actions in the header bar; the irreversible ones behind their own
   disclosure with a red summary, so Revoke and Hard delete cannot sit in the
   same row as Save. These replace six full-width cards on the PO page, four of
   which held exactly one button. */
.rec-actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  justify-content: flex-end;
}

.rec-actions__grave > summary {
  color: var(--color-bad);
  border-color: var(--color-bad-tint);
  background: var(--color-bad-tint);
}

.rec-actions__grave .menu__items {
  min-width: 20rem;
  padding: var(--space-3);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.rec-actions__grave .menu__items a,
.rec-actions__grave .menu__items button {
  color: var(--color-bad);
  text-align: left;
}

.rec-grave-item { display: flex; flex-direction: column; gap: var(--space-1); }
.rec-grave-item__why { font-size: 0.75rem; color: var(--color-text-muted); white-space: normal; }

/* One decision, with its remark box and its two buttons, sitting in the header
   rather than in a card of its own two screens down. */
.rec-decide { display: flex; flex-direction: column; gap: var(--space-2); min-width: 18rem; }
.rec-decide .filter-actions { margin: 0; }

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

@media (max-width: 640px) {
  /* Two sticky bars on a 390px screen leave nothing for the record — the same
     ruling the project frame made, for the same reason (app.js unsticks it). */
  .rec-head__bar { flex-direction: column; gap: var(--space-3); }
  .rec-head__actions { width: 100%; }
  .rec-head__actions .btn,
  .rec-head__actions .menu { flex: 1 1 auto; }
  .rec-head__actions .menu > summary { width: 100%; justify-content: center; }
  .rec-actions { justify-content: stretch; }
  .rec-head__figures {
    gap: var(--space-4) var(--space-6);
    overflow-x: auto;
    scrollbar-width: none;
  }
  .rec-head__figures::-webkit-scrollbar { display: none; }
  .rec-decide { min-width: 0; width: 100%; }
  .rec-actions__grave .menu__items { min-width: 0; width: 100%; position: static; box-shadow: none; }
}

/* ============================================================
   REVAMP 01 — THE MULTI-STEP WIZARDS
   ============================================================
   The four flows that are more than one screen long (BOQ import, vendor
   master import, measurement-sheet import, vendor invoice capture), mounted
   2026-08-05. One header, drawn by _wizard_frame.html.

   Deliberately NOT .lc-track's classes, though it borrows their look: a
   lifecycle step is where a RECORD is and is never a link, and a wizard step
   is where the READER is and a completed one always is. Sharing the selectors
   would mean every later change to one had to be checked against the other. */

.wiz-head {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: var(--space-5) var(--space-5) var(--space-4);
}

.wiz-head__crumbs {
  margin: 0;
  font-size: 0.8125rem;
  color: var(--color-text-muted);
}

.wiz-head__title {
  font-size: 1.375rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  margin: 0;
}

/* The position said in WORDS as well as drawn in the strip: on a phone the
   strip scrolls and the step you are on may be the one off the right edge. */
.wiz-head__position {
  margin: 0;
  font-size: 0.875rem;
  color: var(--color-text-muted);
}

.wiz-track { min-width: 0; }

.wiz-track__steps {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  list-style: none;
  margin: var(--space-1) 0 0;
  padding: 0 0 var(--space-1);
  overflow-x: auto;
  scrollbar-width: none;
  min-width: 0;
  /* Positioned so the .visually-hidden marker on the current step resolves its
     containing block HERE and is clipped by this scroller. Without it that
     absolutely positioned span escapes to the initial containing block and
     pushes the page 3px wide at 390px — the detail screens' identical track
     never showed it because .rec-head is sticky and was already containing it. */
  position: relative;
}

.wiz-track__steps::-webkit-scrollbar { display: none; }

.wiz-step > a,
.wiz-step > span {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  /* 44px of tappable height even though the pill itself is smaller — these
     are the back-links on a phone, and a 20px target is not one. */
  min-height: var(--touch-min);
  padding: var(--space-1) var(--space-3) var(--space-1) var(--space-1);
  border-radius: var(--radius-full);
  font-size: 0.8125rem;
  font-weight: 600;
  white-space: nowrap;
  color: var(--color-text-muted);
  text-decoration: none;
}

/* The connector, so a row of steps reads as one sequence rather than as tags. */
.wiz-step + .wiz-step::before {
  content: "";
  display: inline-block;
  width: var(--space-3);
  height: 1px;
  background: var(--color-border-strong);
  vertical-align: middle;
  margin-right: var(--space-1);
}

.wiz-step { display: inline-flex; align-items: center; }

.wiz-step__num {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: var(--radius-full);
  border: 1.5px solid var(--color-border-strong);
  font-size: 0.6875rem;
  font-variant-numeric: tabular-nums;
  flex: none;
}

/* A completed step is a way back, and reads as one — underlined on hover,
   full-strength ink, a filled number. */
.wiz-step--done > a { color: var(--color-text); }
.wiz-step--done > a:hover .wiz-step__label { text-decoration: underline; }
.wiz-step--done .wiz-step__num {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: #fff;
}

/* The one step drawn as a filled pill: where you are, before anything else. */
.wiz-step--current > a,
.wiz-step--current > span {
  background: var(--color-primary-tint);
  color: var(--color-info);
}

.wiz-step--current .wiz-step__num {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: #fff;
  box-shadow: 0 0 0 3px rgba(10, 132, 255, 0.18);
}

.wiz-step--ahead .wiz-step__num { background: var(--color-surface); }

/* Why this step cannot go forward — the fact, then the instruction, then the
   step that fixes it. Above the controls, never after the submit. */
.wiz-blocked { margin: 0; }
.wiz-blocked__why { margin: 0; font-weight: 600; }
.wiz-blocked__fix { margin: var(--space-2) 0 0; font-weight: 400; }

/* A count on a wizard summary that has a cause worth naming — the merged
   item-code block, the blank-row block. Quiet: these are explanations, not
   alarms, and none of them blocks anything. */
.wiz-note { margin-top: var(--space-4); }
.wiz-note__rows {
  margin: var(--space-2) 0 0;
  padding-left: var(--space-5);
  font-size: 0.8125rem;
  color: var(--color-text-muted);
}

@media (max-width: 640px) {
  .wiz-head { padding: var(--space-4); border-radius: var(--radius-md); }
  .wiz-head__title { font-size: 1.1875rem; }
  /* The step strip is the one thing on these screens allowed to scroll
     sideways; the page itself must not (tests/test_wizards_browser.py). */
  .wiz-track__steps { padding-bottom: var(--space-2); }
}

/* ============================================================
   REVAMP 01 — THE COMPARISON GRID
   ============================================================
   The screen where a purchase order gets awarded (ADR-0052), mounted
   2026-08-06. Two things, in this order down the page: the AWARD PANEL, which
   is the decision, and the GRID, which is the evidence for it.

   What this replaces: a table and a whole separate phone rendering of the same
   data, each with its own copy of the decision controls. The panel is the
   decision at every width now, so there is one Approve button per vendor on
   the page instead of two, and the grid is free to be a grid.

   The colours are decided in procurement/comparison_screen.py, not here: rank
   is a cool lifecycle pill, an incomplete quote is the one red, "Not started"
   is a quiet structure chip. CSS draws what that module decided. */

/* ---------- The award panel ---------- */

.cmp-verdict {
  margin: 0;
  font-size: 1.0625rem;
  line-height: 1.5;
}

.cmp-verdict__figure {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  white-space: nowrap;
}

/* The qualifier on an L1 that only leads because it is unfinished. Red, and
   part of the sentence rather than a note beside it: the claim and its caveat
   have to be read together or the caveat is not doing anything. */
.cmp-verdict__caveat {
  display: block;
  margin-top: var(--space-2);
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--color-bad);
}

.cmp-note {
  margin: var(--space-2) 0 0;
  font-size: 0.8125rem;
  color: var(--color-text-muted);
}

/* A disclosure is a control, not a band across the card. */
.cmp-reject > summary { width: fit-content; }

/* The blocking condition ADR-0042 created. Bordered rather than merely tinted:
   it is the only red on the screen and it stops both gates, so it has to read
   as a wall, not as a note. */
.cmp-blocker {
  margin-top: var(--space-4);
  padding: var(--space-3) var(--space-4);
  border: 1.5px solid var(--color-bad);
  border-radius: var(--radius-md);
  background: var(--color-bad-tint);
}

.cmp-blocker__head { margin: 0; }

.cmp-blocker__why {
  margin: var(--space-2) 0 0;
  font-size: 0.875rem;
  line-height: 1.5;
}

.cmp-options {
  list-style: none;
  margin: var(--space-4) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.cmp-option {
  display: grid;
  grid-template-columns: minmax(14rem, 2fr) auto minmax(0, 1.4fr);
  align-items: center;
  gap: var(--space-3) var(--space-4);
  padding: var(--space-3) var(--space-4);
  border: 1.5px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
}

/* L1 is distinguished on BOTH surfaces with the same green — the option here
   and its column in the grid below — so "the cheapest one" is one visual fact
   read twice, not two conventions to learn. */
.cmp-option--l1 {
  border-color: var(--color-good);
  background: var(--color-good-tint);
}

.cmp-option--awarded {
  box-shadow: 0 0 0 2px rgba(46, 125, 50, 0.25);
}

.cmp-option__id {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  min-width: 0;
}

.cmp-option__vendor {
  font-weight: 600;
  font-size: 1rem;
}

.cmp-option__figures {
  display: flex;
  gap: var(--space-6);
}

.cmp-option__figure {
  display: flex;
  flex-direction: column;
  gap: 2px;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  text-align: right;
}

.cmp-option__caption {
  font-size: 0.6875rem;
  font-weight: 400;
  color: var(--color-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.cmp-option__decide {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.cmp-option__decide .field { margin: 0; }

/* Why this option cannot be awarded, where its button would have been —
   nobody is shown a control they cannot use, and nobody is left guessing why
   it is missing either. */
.cmp-option__blocked {
  margin: 0;
  font-size: 0.8125rem;
  color: var(--color-text-muted);
  line-height: 1.4;
}

.cmp-required {
  color: var(--color-bad);
  font-weight: 600;
  font-size: 0.75rem;
}

.cmp-minimum {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-8);
  margin: var(--space-4) 0 0;
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-border);
}

.cmp-minimum dt {
  font-size: 0.6875rem;
  color: var(--color-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.cmp-minimum dd {
  margin: 2px 0 0;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.cmp-reject { margin-top: var(--space-4); }

/* ---------- The toolbar above the grid ---------- */

.cmp-toolbar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-3);
}

.cmp-legend {
  flex: 1 1 22rem;
  margin: 0;
  font-size: 0.8125rem;
  line-height: 1.5;
  color: var(--color-text-muted);
}

.cmp-legend .cmp-blank--key { margin-right: var(--space-1); }

.cmp-add {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.cmp-add select { min-width: 14rem; }
.cmp-add .btn { white-space: nowrap; }

/* ---------- The grid ---------- */

/* Its own scroll box on BOTH axes, bounded in height. That bound is what makes
   the sticky headers work at all: thead th inside a plain .table-wrap never
   actually pins, because the wrap scrolls sideways while the PAGE scrolls
   down, so the header's containing block is never the thing moving (SPEC.md's
   known gap on .table-wrap). Here both axes move inside this box.
   Not .table-wrap, deliberately — app.js hangs a synced top scrollbar off
   every .table-wrap, and a bar that scrolls one axis of a box that scrolls two
   is a control that half works. */
.cmp-scroll {
  max-height: min(72vh, 46rem);
  overflow: auto;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  background: var(--color-surface);
  -webkit-overflow-scrolling: touch;
}

.cmp-scroll:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
}

table.app-table--grid { width: max-content; min-width: 100%; }

/* Both headers pin against .cmp-scroll. The vendor row wins the corner over
   the item column (z-index), and the item column wins over the cells, so a
   rate never paints on top of the name of the line it belongs to.
   top: 0 is what the base rule says too since ADR-0131; this repeats it only
   because the row it applies to is this grid's own two-deep header. */
table.app-table--grid thead th {
  top: 0;
  z-index: 3;
  vertical-align: top;
  white-space: normal;
  min-width: 9rem;
}

table.app-table--grid th.cmp-rowhead {
  position: sticky;
  left: 0;
  z-index: 2;
  background: var(--color-surface);
  text-align: left;
  padding: var(--space-3);
  border-bottom: 1px solid var(--color-border);
  white-space: normal;
  min-width: 12rem;
  max-width: 18rem;
  font-weight: 600;
  /* border-right drops out under border-collapse: collapse on a sticky cell,
     the same reason the dense table's frozen group uses a shadow. */
  box-shadow: 2px 0 0 var(--color-border-strong);
}

table.app-table--grid thead th.cmp-rowhead {
  z-index: 4;
  background: var(--color-surface-muted);
}

table.app-table--grid tfoot th.cmp-rowhead { background: var(--color-surface-muted); }

.cmp-rowhead__item { display: block; }

.cmp-rowhead__sub {
  display: block;
  margin-top: 2px;
  font-weight: 400;
  font-size: 0.75rem;
  color: var(--color-text-muted);
}

/* A grid cell holds a control, a money figure and sometimes a state chip; the
   generic body-cell rule (nowrap + 16rem cap + ellipsis) was written for a
   column of plain values and silently clips all three. */
table.app-table--grid tbody td,
table.app-table--grid tfoot td {
  max-width: none;
  overflow: visible;
  text-overflow: clip;
  vertical-align: top;
}

.cmp-vendor { min-width: 11rem; }
.cmp-vendor__rank { display: block; margin-bottom: var(--space-1); }
.cmp-vendor__name { display: block; font-weight: 600; }
.cmp-vendor__flag { display: block; margin-top: var(--space-1); }

/* The L1 column, top to bottom — the same green as its option in the panel. */
table.app-table--grid .cmp-col--l1 { background: var(--color-good-tint); }
table.app-table--grid thead th.cmp-col--l1 { background: #d8ecda; }

/* A blank cell is a STATE, not an absence: since ADR-0042 a column with holes
   in it can be neither submitted nor awarded, so the holes are drawn rather
   than left as whitespace an eye slides over. Striping the cell is what makes
   an incomplete column readable as a column — a run of them down one vendor is
   visible from across the grid, which a row of em-dashes never was. */
.cmp-cell--blank {
  background: repeating-linear-gradient(
    -45deg,
    var(--color-bad-tint),
    var(--color-bad-tint) 6px,
    transparent 6px,
    transparent 12px
  );
}

.cmp-blank {
  display: inline-block;
  font-size: 0.6875rem;
  font-weight: 600;
  color: var(--color-bad);
  white-space: nowrap;
}

.cmp-blank--key {
  padding: 1px var(--space-2);
  border-radius: var(--radius-full);
  background: var(--color-bad-tint);
}

.cmp-amount {
  display: block;
  margin-top: var(--space-1);
  color: var(--color-text-muted);
  font-size: 0.75rem;
}

.cmp-rowflag { white-space: nowrap; }

/* ---------- The pre-award twin (ADR-0067) ----------
   The tender rate grid reuses everything above — the scroll box, the pinned
   headers, the L1 tint, the blank-cell stripes — and adds two things the PO
   comparison has no counterpart for.

   .cmp-open is the SAME position on the page as .cmp-blocker and says the
   opposite thing, so it is deliberately not red: post-award a column with
   holes in it means the PO cannot be submitted or awarded (ADR-0042);
   pre-award it means a vendor priced what they sell. Neutral surface, no
   warning colour, and the sentence inside it says "this blocks nothing" in
   words — a person trained by the other screen reads red as a stop. */
/* The same qualifier as .cmp-verdict__caveat and NOT red. Post-award, an L1
   that leads by being unfinished is a quote nobody may award, so the caveat is
   the colour of a stop. Pre-award it is a total that covers fewer rows — worth
   reading before the next step, and not an emergency. Same weight, same place
   in the sentence, warn rather than bad. */
.cmp-verdict__caveat--open { color: var(--color-warn); }

.cmp-open {
  margin-top: var(--space-3);
  padding: var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  background: var(--color-surface-muted);
}

.cmp-open__head { margin: 0; }

.cmp-open__why {
  margin: var(--space-2) 0 0;
  font-size: 0.8125rem;
  color: var(--color-text-muted);
}

/* A column that priced part of the enquiry. The blank CELLS still stripe (that
   is a per-cell state and reads the same either side of the award), but the
   HEADER does not shout: this is a chip's worth of information. */
table.app-table--grid th.cmp-col--part { background: var(--color-surface-muted); }

/* Which vendor a row is being costed from — the column with no post-award
   twin. One control per row, never one per cell: a radio in every cell of a
   6-vendor by 200-row grid is 1,200 controls for 200 decisions. */
.cmp-basis { min-width: 15rem; white-space: normal; }

.cmp-basis__select { width: 100%; min-height: 44px; }

.cmp-basis__chosen { font-weight: 600; }

.cmp-basis__note {
  display: block;
  margin-top: var(--space-1);
  font-size: 0.6875rem;
  color: var(--color-text-muted);
}

/* A suggestion is dressed as one. It is pre-selected so the common case is one
   click, and it says outright that nothing is recorded until it is saved —
   ADR-0061's rule, that a suggested figure never wears the clothes of a
   chosen one. */
.cmp-basis__note--suggested { color: var(--color-primary); font-weight: 600; }

.cmp-basis__note--dearer { color: var(--color-warn); font-weight: 600; }

/* The cell a row is actually costed from, so the chosen rate is findable
   across a grid that scrolls. */
table.app-table--grid td.cmp-cell--basis {
  box-shadow: inset 0 0 0 2px var(--color-primary);
}

/* The grand-total row is the one the ranking is on. */
table.app-table--grid tfoot tr.cmp-total-row td,
table.app-table--grid tfoot tr.cmp-total-row th {
  border-top: 2px solid var(--color-border-strong);
  font-size: 0.9375rem;
}

table.app-table--grid tfoot td { border-top: none; }

@media (max-width: 640px) {
  /* The panel is the decision surface, so on a phone it stacks into one
     column of full-width rows — a GM approving from a phone reads L1, the gap,
     the flag and the button in that order without scrolling sideways. The grid
     below still scrolls in both axes inside its own box, which is exactly what
     the sticky headers were built for. */
  .cmp-option {
    grid-template-columns: 1fr;
    align-items: stretch;
  }
  .cmp-option__figures { justify-content: space-between; gap: var(--space-3); }
  .cmp-option__figure { text-align: left; }
  .cmp-option__decide .btn { width: 100%; }
  .cmp-toolbar > * { width: 100%; }
  .cmp-add select { min-width: 0; flex: 1 1 auto; }
  .cmp-scroll { max-height: 68vh; }
  table.app-table--grid th.cmp-rowhead { min-width: 9rem; max-width: 11rem; }
}

/* ============================================================
   REVAMP 01 — THE FORMS (session seven)
   ============================================================
   Twelve screens somebody types into. What they had in common: fields in
   model order, required marked by a bare asterisk on half of them and by
   nothing on the other half, errors piled at the top of the form, money
   entered into left-aligned proportional-figure inputs, and a two-column
   .filter-grid — a FILTER BAR's layout — carrying twenty-one labelled
   inputs on the widest of them.

   The rules, all of them here so the next form inherits them:

     width    a form is w-form (30rem, base.html's own token). A form INSIDE
              a wider page — the PO details block on the PO record, the stage
              editor beside its stage table, the access control on the project
              page — takes .form-col, the same 30rem as a column.
     stack    one column, always. A form is read top to bottom and filled in
              top to bottom, and two columns of labelled inputs on a phone is
              one column with the reading order shuffled.
     group    a heading and a sentence per group of fields, so the sentence
              that explains four fields is said once above them rather than
              four times inside them.
     mark     required and optional both named in words, on every field.
     error    under the input it belongs to.
     money    right-aligned tabular figures, with the Indian-grouped reading
              underneath.
   ============================================================ */

/* A form living inside a page that is wide for other reasons. 30rem, matching
   body.w-form's --app-width exactly — the two are the same measure and are
   allowed to be the same literal only because this comment says so. */
.form-col {
  max-width: 30rem;
}

/* The size of the job, before the job. */
.form-scope {
  margin: 0 0 var(--space-5);
  font-size: 0.875rem;
  color: var(--color-text-muted);
}

.form-scope b {
  color: var(--color-text);
  font-weight: 600;
}

.form-group {
  margin-bottom: var(--space-6);
}

.form-group:last-of-type {
  margin-bottom: 0;
}

.form-group__title {
  margin: 0 0 var(--space-2);
  font-size: 0.9375rem;
  font-weight: 600;
  letter-spacing: -0.01em;
}

.form-group__hint {
  margin: 0 0 var(--space-4);
  font-size: 0.8125rem;
  line-height: 1.5;
  color: var(--color-text-muted);
  max-width: 72ch;
}

/* The label line: the caption, and the required/optional flag pushed to the
   far end of it so a column of fields answers "which of these must I fill in"
   down one edge instead of by reading every label to its end.
   Scoped to the three kinds form_screens draws, deliberately — .field is used
   by every filter bar in the app and none of those has a flag to align. */
.field--plain > label,
.field--money > label {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
}

.field__caption {
  min-width: 0;
  overflow-wrap: anywhere;
}

.field__flag {
  flex: 0 0 auto;
  font-size: 0.6875rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

/* Required is the state that costs you a round trip if you miss it, so it is
   the one that carries the page's own ink. Optional is grey and quiet — but it
   is THERE, which is the whole point: "no marking" is not a state a reader can
   see. Deliberately NOT red: red on this screen means an error, and sixteen
   red flags on a form where nothing is yet wrong is sixteen things for a real
   error to hide behind. */
.field__flag--req {
  color: var(--color-text);
}

.field__flag--opt {
  color: var(--color-text-muted);
  font-weight: 500;
}

.field__hint {
  margin: var(--space-2) 0 0;
  font-size: 0.8125rem;
  line-height: 1.5;
  color: var(--color-text-muted);
}

/* Something wrong-looking that is nevertheless allowed (ADR-0086) — a
   duplicate item code. Amber, never red: red on this screen means the form
   refused the save, and everything wearing this colour saved. */
.field__warning {
  margin: var(--space-2) 0 0;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  background: var(--color-warn-tint);
  font-size: 0.8125rem;
  line-height: 1.5;
  color: #8a5a00;
}

/* What an uploaded document says a box holds (ADR-0107) — the mark and the line
   it was read off. Quieter than a hint's own prose is loud: the pill carries the
   state (amber where the box and the document disagree) and the quote is the
   thing worth reading, so it is set apart from the hint above it rather than
   competing with it. */
.field__source {
  margin: var(--space-2) 0 0;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2);
  font-size: 0.8125rem;
  line-height: 1.5;
}

.field__source-quote {
  color: var(--color-text-muted);
  font-style: italic;
}

/* Against the field, in the field's own colour, and never collapsed into a
   list at the top of the form. */
.field__error {
  margin: var(--space-2) 0 0;
  font-size: 0.8125rem;
  line-height: 1.4;
  font-weight: 500;
  color: var(--color-bad);
}

.field--invalid .input,
.field--invalid input,
.field--invalid select,
.field--invalid textarea {
  border-color: var(--color-bad);
}

/* The base control rule above covers text / password / date / file, which
   leaves a money field, a GST percentage, a day count and an email address
   rendering as unstyled 21px browser defaults beside their styled siblings —
   below --touch-min, on the screens most likely to be filled in on a phone.
   Scoped to the form frame rather than added to the base rule: the dense line
   table and the cover editor have their own number-input styling and are no
   part of this session. */
.form-group input[type="number"],
.form-group input[type="email"],
.form-group input[type="tel"],
.form-group input[type="url"] {
  width: 100%;
  min-height: var(--touch-min);
  padding: 0 var(--space-3);
  border: 1.5px solid var(--color-border);
  border-radius: var(--radius-md);
  font-size: 0.9375rem;
  font-family: var(--font-sans);
  background: var(--color-surface);
  color: var(--color-text);
}

.form-group input[type="number"]:focus,
.form-group input[type="email"]:focus,
.form-group input[type="tel"]:focus,
.form-group input[type="url"]:focus {
  outline: none;
  border-color: var(--color-primary);
  box-shadow: 0 0 0 3px rgba(10, 132, 255, 0.18);
}

/* Money is entered right-aligned in tabular figures — the same treatment it
   gets in every table on every other screen, for the same reason: nine figures
   down a column only read as a column when their digits line up. */
.field--money input {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* What was typed, read back in rupees and lakhs. Display only: it never
   touches the input's own value, so what posts is exactly what was typed. */
.field__grouped {
  margin: var(--space-1) 0 0;
  min-height: 1.2em;
  text-align: right;
  font-size: 0.8125rem;
  font-variant-numeric: tabular-nums;
  color: var(--color-text-muted);
}

.field--checkbox {
  padding: var(--space-1) 0;
}

.form-actions {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  margin-top: var(--space-6);
  padding-top: var(--space-5);
  border-top: 1px solid var(--color-border);
  flex-wrap: wrap;
}

.form-actions .btn {
  min-height: var(--touch-min);
}

/* --- What the invoice's nine figures imply ------------------------------- */

.arith {
  margin-top: var(--space-4);
  padding: var(--space-4);
  border: 1.5px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface-muted);
}

.arith__title {
  margin: 0 0 var(--space-3);
  font-size: 0.8125rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-text-muted);
}

.arith__row + .arith__row {
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-border);
}

.arith__sum {
  margin: 0;
  font-size: 0.8125rem;
  color: var(--color-text-muted);
}

.arith__value {
  margin: 0;
  font-size: 1.0625rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.arith__against {
  margin: var(--space-1) 0 0;
  font-size: 0.8125rem;
  line-height: 1.45;
  color: var(--color-text-muted);
}

/* A disagreement is amber, not red: this is the figure the vendor printed and
   it is going to be recorded either way (ADR-0020). Red here would read as a
   refusal, and nothing on this screen refuses. */
.arith__row--off .arith__value {
  color: var(--color-warn);
}

.arith__row--off .arith__against b {
  color: var(--color-warn);
}

.arith__stance {
  margin: var(--space-3) 0 0;
  font-size: 0.75rem;
  line-height: 1.5;
  color: var(--color-text-muted);
}

/* --- What has to exist before a form is worth filling in ----------------- */

.prereq-list {
  list-style: none;
  margin: var(--space-4) 0 0;
  padding: 0;
}

.prereq + .prereq {
  margin-top: var(--space-4);
  padding-top: var(--space-4);
  border-top: 1px solid var(--color-border);
}

.prereq__state {
  margin: 0 0 var(--space-2);
}

.prereq__why {
  margin: 0;
  font-size: 0.9375rem;
  font-weight: 500;
  line-height: 1.45;
}

.prereq__fix {
  margin: var(--space-2) 0 0;
  font-size: 0.8125rem;
  line-height: 1.5;
  color: var(--color-text-muted);
}

/* --- Who can see a project (the access control) -------------------------- */
/* A multi-select listbox needs a modifier key on a desktop and a long press on
   a phone, and shows six names in a scrolling box whatever the pool is. The
   same POST — one `assigned_staff` value per person — as a list of tick rows,
   each one a full-width target. */

.access-list {
  list-style: none;
  margin: 0;
  padding: 0;
  border: 1.5px solid var(--color-border);
  border-radius: var(--radius-md);
  /* Bounded like the listbox it replaced (size="6"): a CONTROL the size of a
     control, not a list that grows down the page with the staff count — which
     is the shape the owner ruled against when access was six full-width rows
     heading this tab. It scrolls inside itself past that. */
  max-height: 15rem;
  overflow-y: auto;
}

.access-list li + li {
  border-top: 1px solid var(--color-border);
}

.access-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: var(--touch-min);
  padding: var(--space-2) var(--space-4);
  font-size: 0.9375rem;
  cursor: pointer;
}

.access-row:hover {
  background: var(--color-surface-muted);
}

.access-row input[type="checkbox"] {
  width: 20px;
  height: 20px;
  accent-color: var(--color-primary);
  flex-shrink: 0;
}

.access-row__who {
  min-width: 0;
  overflow-wrap: anywhere;
}

.access-row__login {
  color: var(--color-text-muted);
}

@media (max-width: 640px) {
  /* Every control on a form is a thumb target on a phone, including the ones
     the base rules leave at their intrinsic height. */
  .form-actions .btn,
  .form-actions a {
    width: 100%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: var(--touch-min);
  }
  .form-col {
    max-width: 100%;
  }
}

/* --- A vendor's addresses, under the vendor form ------------------------- */
/* Five table columns carrying one short value each, on a screen that is
   otherwise a 30rem form. As a list it reads the same and stops the page
   needing a table's width. */

.addr-list {
  list-style: none;
  margin: var(--space-4) 0 0;
  padding: 0;
}

.addr + .addr {
  margin-top: var(--space-4);
  padding-top: var(--space-4);
  border-top: 1px solid var(--color-border);
}

.addr__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
}

.addr__edit {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  min-height: var(--touch-min);
  padding: 0 var(--space-2);
}

.addr__where {
  margin: var(--space-1) 0 0;
  font-size: 0.875rem;
  line-height: 1.45;
  color: var(--color-text-muted);
}

/* ---------- Tender lifecycle pills (ADR-0064) ----------

   The pre-award register's six states. One consistent colour per value, the
   same convention every status pill above follows, and COOL throughout —
   including Lost, which is the normal outcome of most tenders and is a place,
   not an alarm. The red on a tender row belongs to its exception flag, which
   says what to DO: the bid date has gone, or the EMD cover is empty.

   No new colours: identified borrows the grey every not-started state in this
   file uses, preparing the purple of work in hand, won the good-tint green of
   an approved record, withdrawn .pill-revoked's slate (both are records that
   stopped on purpose). Submitted is not here at all — it reuses
   .pill-submitted, which already means "it has gone to the other side and the
   wait is theirs". */
.pill-identified {
  background: #eceff1;
  color: #455a64;
}

.pill-preparing {
  background: #ede7f6;
  color: #5e35b1;
}

.pill-won {
  background: var(--color-good-tint);
  color: var(--color-good);
}

/* Lost is grey, not red, and the same grey .pill-unranked uses: a tender that
   went elsewhere is not an error and not last — it is closed. */
.pill-lost {
  background: #f2f2f7;
  color: var(--color-text-muted);
}

.pill-withdrawn {
  background: #eceff1;
  color: #607d8b;
}

/* One form on a tender's checklist, ticked. The row itself stays plain — a
   checklist where the done rows shout is a checklist where the outstanding
   ones whisper. */
.check-row--done .check-row__name {
  color: var(--color-text-muted);
  text-decoration: line-through;
}

/* ============================================================
   VENDOR INVOICE ENTRY, ONE SCREEN (ADR-0105)
   ============================================================ */

/* The mapping cell is the ONE cell in this table that wraps. Every other cell
   holds a single input and takes .app-table's nowrap; this one holds a picker,
   a quantity, a split control and the sentence saying which challans the
   suggested quantity came from — four things that have to stack.

   Every rule below is written tag-and-class (`.app-table input.entry-num`)
   rather than class alone, because the base control rule is
   `input[type="text"] { width: 100% }` — an attribute selector, which outranks
   a bare class and would leave every box the width of its column. */
.app-table td.entry-map-cell {
  white-space: normal;
  min-width: 26rem;
}

/* One BOQ mapping on an entry row: the line it goes to and how much of the row
   goes there. Several stack when a row is split across BOQ lines, which is the
   Festo case — one printed adapter line billed against three BOQ items. */
.entry-map {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  margin-bottom: var(--space-2);
}

/* The picker takes what the cell has and the quantity keeps its own width —
   the picker's label is long (item no · description · quantity), and a flex
   default would let it squeeze the quantity box to nothing. */
.entry-map select {
  flex: 1 1 auto;
  min-width: 0;
}

.entry-map input.entry-num {
  flex: 0 0 6rem;
}

/* A typed figure on an entry row. Narrow, because eleven columns share one
   page and none of these numbers is long. */
.app-table input.entry-num {
  width: 6rem;
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* The vendor's own words on a row. The description is what a reader checks
   against the paper invoice, so it gets twice the width of the codes beside
   it rather than all three sharing one measure. */
.app-table input.entry-text {
  width: 9rem;
}

.app-table input.entry-text--wide {
  width: 18rem;
}
