/*
 * 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 {
  /* The names every rule below was written against, kept so no screen had to
     change — each one now points at a design-system token from tokens.css
     (ADR-0185), which is generated from docs/design-system/tokens.json. New
     rules use the token names directly (var(--ink), var(--surface-card) …);
     these aliases exist so the old ones cannot drift from them. The spacing
     scale (--space-1 … --space-8) and --radius-sm / -md / -lg are not here at
     all: tokens.css defines them under the same names. */
  --color-bg: var(--surface-page);
  --color-surface: var(--surface-card);
  --color-surface-muted: var(--surface-sunken);
  --color-text: var(--ink);
  --color-text-muted: var(--ink-muted);
  --color-border: var(--border);
  --color-primary: var(--accent);
  --color-primary-tint: var(--accent-tint);

  /* The traffic-light families (revamp 01) are the design system's status
     colours: good = success, warn = warning, bad = danger, and info is the
     accent on its tint, which is what the README's "info" pill is. */
  --color-good: var(--success);
  --color-good-tint: var(--success-tint);
  --color-warn: var(--warning);
  --color-warn-tint: var(--warning-tint);
  --color-bad: var(--danger);
  --color-bad-tint: var(--danger-tint);
  --color-info: var(--accent);
  --color-info-tint: var(--accent-tint);

  /* A line that has to read across 1,600px of table needs more than the
     hairline that separates two cards — the control outline is that line. */
  --color-border-strong: var(--control-border);

  --radius-full: var(--radius-pill);

  /* Cards never get a shadow (docs/design-system/README.md: "Borders, not
     shadows"). What still floats over the page — a dropdown menu, the figure
     picker, the paste guide — keeps this one. The design system has no token
     for a popover yet (only the sheet's upward shadow), so it is the one
     literal shadow left; ADR-0185 lists it. */
  --shadow-float: 0 1px 2px rgba(0, 0, 0, 0.04), 0 8px 24px rgba(0, 0, 0, 0.06);

  --touch-min: var(--tap-min);

  /* 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;
}

/* The hidden attribute always hides (V3). The browser's own `[hidden]` rule
   sits below every rule in this file, so any component that sets a display —
   .btn's inline-flex, `.sheet__actions form`'s grid, .viewer__frame's block —
   used to show an element its template or app.js had hidden. On the phone
   approve sheet that put the send-back box and "Yes, final approve" on screen
   at open, so one tap approved without the "Approve … for ₹…?" step. One rule
   here, instead of a patch per component: nothing in the app means to show a
   hidden element. tests/test_hidden_stays_hidden_browser.py holds it. */
[hidden] {
  display: none !important;
}

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 ---------- */

/* The top bar is only for a signed-out page now (the login page's siblings,
   a 404 to a stranger): the brand and Log in. A signed-in page has the
   sidebar or rail below, or the phone frame. Solid card, one border — the
   frosted glass it used to be was two literal colours (ADR-0185's ratchet). */
.app-nav--top {
  position: sticky;
  top: 0;
  z-index: 30;
  background: var(--surface-card);
  border-bottom: 1px solid var(--border);
}

/* 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--top .app-nav__brand {
  font-weight: 600;
  font-size: var(--type-heading-size);
  color: var(--ink);
  min-height: var(--tap-min);
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  /* The brand may wrap onto a second line on a narrow phone rather than
     force the bar wider than the viewport (the "Log out clipped" bug). */
  flex: 1 1 auto;
  min-width: 0;
}

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

.app-nav--top .app-nav__links {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-4);
  font-size: var(--type-body-size);
}

.app-nav--top .app-nav__links a {
  color: var(--ink-muted);
  min-height: var(--tap-min);
  display: inline-flex;
  align-items: center;
}

.app-nav--top .app-nav__links a:hover {
  color: var(--ink);
  text-decoration: none;
}

/* ---------- Sidebar (laptop) and rail (tablet) — design-system session B ----------

   One element (core/_side_nav.html), three widths. At 640px and under it is
   hidden and the phone frame stands in. From 641px it is an 80px icon rail
   (`--rail`); from 1024px a 240px labelled sidebar (`--sidebar`). It is
   fixed to the left edge, and the page makes room for it with
   --frame-left, which anything else pinned to the viewport (the selection
   tray) reads too. */

:root { --frame-left: 0px; }

.side-nav { display: none; }

.icon {
  width: 24px;
  height: 24px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.icon--sm { width: 18px; height: 18px; }

@media (min-width: 641px) {
  body:has(> .side-nav) {
    --frame-left: var(--rail);
    padding-left: var(--frame-left);
  }

  .side-nav {
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    z-index: 30;
    width: var(--frame-left);
    display: flex;
    flex-direction: column;
    background: var(--surface-card);
    border-right: 1px solid var(--border);
  }

  .side-nav__brand {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    min-height: var(--topbar);
    padding: var(--space-3) var(--space-4);
    color: var(--ink);
  }

  .side-nav__brand:hover { text-decoration: none; }

  .side-nav__brand-text {
    display: flex;
    flex-direction: column;
    min-width: 0;
    font-size: var(--type-body-size);
    line-height: var(--type-body-line);
    font-weight: 700;
  }

  .side-nav__company {
    font-size: var(--type-caption-size);
    line-height: var(--type-caption-line);
    font-weight: 400;
    color: var(--ink-muted);
  }

  .side-nav__mark {
    display: none;
    font-size: var(--type-meta-size);
    font-weight: 700;
    letter-spacing: 0.02em;
  }

  .side-nav__scroll {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    padding: 0 var(--space-2) var(--space-3);
  }

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

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

  .side-nav__heading {
    margin: 0;
    padding: var(--space-1) var(--space-3);
    font-size: var(--type-section-label-size);
    line-height: var(--type-section-label-line);
    font-weight: var(--type-section-label-weight);
    letter-spacing: var(--type-section-label-tracking);
    text-transform: uppercase;
    color: var(--ink-muted);
  }

  .side-nav__link {
    position: relative;
    display: flex;
    align-items: center;
    gap: var(--space-3);
    width: 100%;
    min-height: var(--tap-min);
    padding: 0 var(--space-3);
    border: 0;
    border-radius: var(--radius-sm);
    background: none;
    color: var(--ink);
    font-family: var(--font-sans);
    font-size: var(--type-body-sm-size);
    line-height: var(--type-body-sm-line);
    font-weight: 500;
    text-align: left;
    cursor: pointer;
  }

  .side-nav__link .icon { color: var(--ink-muted); }

  .side-nav__link:hover {
    background: var(--surface-sunken);
    text-decoration: none;
  }

  /* The page being read: accent text on the accent tint (README, Layout). */
  .side-nav__link--active,
  .side-nav__link--active:hover,
  .side-nav__link--active .icon {
    background: var(--accent-tint);
    color: var(--accent);
  }

  .side-nav__label {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .side-nav__short { display: none; }

  .side-nav__link .nav-count,
  .app-nav__bell-count { margin-left: auto; }

  .app-nav__bell-count .notif-badge { position: static; display: inline-block; }

  .side-nav__kind {
    flex: none;
    font-size: var(--type-caption-size);
    color: var(--ink-muted);
  }

  .side-nav__foot {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    padding: var(--space-2);
    border-top: 1px solid var(--divider);
  }

  .side-nav__user {
    padding: var(--space-1) var(--space-3) 0;
    font-size: var(--type-meta-size);
    color: var(--ink-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .side-nav__logout { margin: 0; }
}

/* The rail: the same rows as an icon over a one- or two-word label. No
   headings — a hairline between departments instead — and no Recent, which
   needs the sidebar's width to say anything. */
@media (min-width: 641px) and (max-width: 1023px) {
  .side-nav__brand { justify-content: center; padding-inline: var(--space-1); }
  .side-nav__brand-text,
  .side-nav__brand .app-nav__logo { display: none; }
  .side-nav__mark { display: inline; }

  .side-nav__scroll { padding-inline: var(--space-1); }
  .side-nav__heading,
  .side-nav__recent,
  .side-nav__user { display: none; }

  /* No side margin: a department's rows get the rail's whole width, so the
     longest one-word labels ("Receivables", "Credentials") fit instead of
     being cut (cleanup C1). The hairline spans it too. */
  .side-nav__group {
    margin: var(--space-2) 0 0;
    padding-top: var(--space-2);
    border-top: 1px solid var(--divider);
  }

  .side-nav__link {
    flex-direction: column;
    justify-content: center;
    gap: var(--space-1);
    min-height: 56px;
    padding: var(--space-1) 0;
    text-align: center;
  }

  .side-nav__label { display: none; }

  .side-nav__short {
    display: block;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: var(--type-tab-label-size);
    line-height: var(--type-tab-label-line);
  }

  /* Counts sit on the icon's corner, as on the phone tab bar. */
  .side-nav__link .nav-count,
  .app-nav__bell-count {
    position: absolute;
    top: var(--space-1);
    left: calc(50% + 6px);
    margin: 0;
    font-size: var(--type-caption-size);
  }

  .side-nav__foot { padding: var(--space-1); }
}

@media (min-width: 1024px) {
  body:has(> .side-nav) { --frame-left: var(--sidebar); }
}

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

/* 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-pill);
  background: var(--badge);
  color: var(--on-accent);
  font-size: var(--type-tab-label-size);
  font-weight: 700;
  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(--surface-sunken);
  color: var(--ink-muted);
  font-size: var(--type-caption-size);
  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(--warning-tint);
  color: var(--warning);
}

.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;
}

/* A row action, so the compact size (doubled class: it must outrank .btn,
   which comes later in this file). */
.notif-row__read.btn {
  flex-shrink: 0;
  padding: var(--space-1) var(--space-3);
  min-height: 36px;
  font-size: 0.8125rem;
  white-space: nowrap;
}

/* ---------- Reminders (ADR-0180 stage 2) ---------- */

/* The Reminders link's due-or-overdue count, in the sidebar and the phone
   Menu: plain ink-muted text after the word. Only Inbox gets the red badge
   (docs/design-system/README.md, Counts). */
.nav-count {
  color: var(--ink-muted);
  font-size: var(--type-meta-size);
  font-weight: 400;
  font-variant-numeric: tabular-nums;
}

/* The form panel a "Remind me" button opens: fixed to the corner, above the
   selection tray (z-index 40), so it opens beside whatever row was pressed. */
.reminder-sheet {
  position: fixed;
  right: var(--space-4);
  bottom: var(--space-4);
  z-index: 50;
  width: min(28rem, calc(100vw - 2 * var(--space-4)));
  max-height: calc(100vh - 6rem);
  overflow-y: auto;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.18);
}

.reminder-sheet .card-title {
  margin-bottom: var(--space-2);
}

.reminder-page {
  max-width: 36rem;
}

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

.reminder-form__pair {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-3);
}

.reminder-form__people {
  border: none;
  padding: 0;
  margin-inline: 0;
}

.reminder-form__people legend {
  font-size: 0.875rem;
  font-weight: 500;
  margin-bottom: var(--space-2);
}

/* An office's worth of names: scroll inside the panel, not the panel. */
.reminder-form__people-list {
  max-height: 14rem;
  overflow-y: auto;
}

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

.reminder-form__delete {
  margin-top: var(--space-3);
}

.reminder-row {
  align-items: flex-start;
  flex-wrap: wrap;
}

.reminder-row__body {
  flex: 1 1 18rem;
  min-width: 0;
}

.reminder-row__when {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.reminder-row__note {
  margin: var(--space-1) 0;
  overflow-wrap: anywhere;
}

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

.reminder-row__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--space-2);
}

.reminder-row__snooze > summary {
  list-style: none;
}

.reminder-row__snooze > summary::-webkit-details-marker {
  display: none;
}

.reminder-row__snooze form {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

.reminder-row__snooze label {
  font-size: 0.8125rem;
}

.reminder-row__snooze input[type="date"] {
  width: auto;
}

.reminder-closed > summary {
  cursor: pointer;
  font-weight: 600;
  min-height: var(--touch-min);
  display: flex;
  align-items: center;
}

@media (max-width: 640px) {
  .reminder-sheet {
    left: 0;
    right: 0;
    bottom: 0;
    width: auto;
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
  }

  .reminder-form__pair {
    grid-template-columns: 1fr;
  }
}

/* ---------- Push on this device (ADR-0167) ---------- */

/* The one-time card under the nav: whatever the page starts with comes after it. */
.push-prompt {
  margin-bottom: var(--space-4);
}

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

.push-devices li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
  padding: var(--space-3) 0;
  border-top: 1px solid var(--color-border);
}

.push-devices form {
  margin: 0;
}

.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 ---------- */

/* surface-card on the surface-page ground, radius-md, and no shadow — the
   ground is what separates two cards (docs/design-system/README.md). */
.card {
  background: var(--surface-card);
  border-radius: var(--radius-md);
  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(--surface-card);
  border-radius: var(--radius-md);
  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: var(--control-border);
}

.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;
}

/* A list given a max-height scrolls; its rows must not shrink to fit it. A
   row squeezed to its 44px minimum spills its name onto the next row, and the
   last row's spill is cut off at the foot of the list. */
.option-list > .option-row {
  flex-shrink: 0;
}

/* 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 ---------- */

/* Draw small, tap big (docs/design-system/README.md): a button is drawn
   `--button` (40px) tall and answers across `--tap-min` (44px). The ::after
   is the difference — an invisible strip above and below the drawn pill that
   still belongs to the button, so a thumb that lands 2px off the paint still
   presses it. On a button already 44px or taller the strip is zero. */
.btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--button);
  padding: 0 var(--space-5);
  border-radius: var(--radius-pill);
  font-size: var(--type-body-size);
  font-weight: 600;
  border: none;
  cursor: pointer;
  transition: opacity 0.15s ease;
  font-family: var(--font-sans);
}

.btn::after {
  content: "";
  position: absolute;
  inset: min(0px, calc((100% - var(--tap-min)) / 2)) 0;
}

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

/* The one filled button on a screen. */
.btn-primary {
  background: var(--accent-fill);
  color: var(--on-accent);
}

/* Everything else is outlined: the card's surface and one 1px edge in the
   control colour — no inset ring, which drew a second edge (ADR-0184's
   follow-up). */
.btn-secondary {
  background: var(--surface-card);
  color: var(--ink);
  border: 1px solid var(--control-border);
  box-shadow: none;
}

/* A text button: accent text, no box. */
.btn-ghost {
  background: none;
  color: var(--accent);
  padding-inline: var(--space-2);
}

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

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

/* A destructive action is never the filled one (README, principle 3):
   outlined like a secondary button, with the label in danger red. */
.btn-danger {
  background: var(--surface-card);
  color: var(--danger);
  border: 1px solid var(--control-border);
}

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

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

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

/* .field label is for a caption above an input, and it outranks the tick-box
   rows: it made each one a block, so the words dropped under the box — the
   reminder form's "Also notify" names, and the bulk panel's "Clear … instead"
   on a phone. A row inside a .field keeps its own layout. */
.field label.option-row,
.field label.checkbox-row {
  display: flex;
}

/* An option row is a bordered row of its own, not a caption: no caption gap
   under it, and the same type as an option row anywhere else. */
.field label.option-row {
  margin-bottom: 0;
  font-size: inherit;
  font-weight: inherit;
}

.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: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font-size: var(--type-body-sm-size);
  font-family: var(--font-sans);
  background: var(--surface-card);
  color: var(--ink);
}

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 {
  border-color: var(--accent);
}

/* One focus ring for every control: 2px of accent, 2px clear of the edge. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* 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;
}

/* The form grid (cleanup C2): a panel of fields, not a filter bar. Every cell
   is top-aligned, so labels sit in one row and inputs in the next however
   much a field carries under it (the bulk panel's "Clear … instead" box);
   auto-fill keeps empty tracks, so two of these grids in one panel share one
   column width and their fields line up down the page. 10rem gives two
   columns at phone width and six across a laptop panel. */
.action-grid--form {
  grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
  align-items: start;
}

/* 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);
}

/* A button keeps its label on one line (ADR-0185): it does not shrink, and
   when the row runs out of room the row wraps instead of squeezing a label
   onto two lines or pushing the page sideways. A sentence beside the buttons
   stays beside them while it has 16rem, and drops under them when it has not. */
.filter-actions {
  flex-wrap: wrap;
}

.filter-actions > .btn {
  flex-shrink: 0;
  max-width: 100%;
}

.filter-actions > span {
  flex: 1 1 16rem;
  min-width: 0;
}

.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 ---------- */

/* A table box: a 1px border edge and radius-sm, no shadow. */
.table-wrap {
  overflow-x: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-card);
  -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: var(--control-border);
  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: var(--ink-faint);
}

.table-scroll-top__thumb:active {
  background: var(--ink-muted);
  cursor: grabbing;
}

table.app-table {
  border-collapse: collapse;
  width: 100%;
  font-size: var(--type-body-sm-size);
}

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;
  /* The header row (docs/design-system/README.md, Tables): surface-sunken,
     36px, caption-size labels in ink-muted. */
  background: var(--surface-sunken);
  text-align: left;
  height: 36px;
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
  font-size: var(--type-caption-size);
  line-height: var(--type-caption-line);
  font-weight: 600;
  color: var(--ink-muted);
}

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-2) * -1) calc(var(--space-3) * -1);
  padding: var(--space-2) 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);
}

/* Body rows are --table-row (44px): 12px above and below a 20px line of
   body-sm, a divider hairline between rows. */
table.app-table tbody td {
  height: var(--table-row);
  padding: var(--space-3);
  line-height: var(--type-body-sm-line);
  border-bottom: 1px solid var(--divider);
  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;
}

.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(--warning-tint);
  color: var(--warning);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
  font-size: 0.875rem;
}

.app-message--error {
  background: var(--danger-tint);
  color: var(--danger);
}

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

   Every pill class below is one of the design system's FIVE kinds (ADR-0185,
   docs/design-system/README.md "Status pill"), each its status colour on its
   own tint:
     danger   danger on danger-tint     — blocks, failed, late, over 7/60 days
     warning  warning on warning-tint   — worth a look, waiting on someone
     success  success on success-tint   — approved, passed, paid, done
     info     accent on accent-tint     — a place in a lifecycle ("Sent", "Active")
     neutral  ink-muted on surface-sunken — not started, a label, nothing at stake
   A class still names its VALUE (class name = slugify of the label), so a
   status keeps its one colour on every screen; many values now share a kind.
   The older comments below explain WHY a value is in its kind — the hues they
   name (teal, purple, red-orange …) were folded into the five. */

.pill {
  display: inline-flex;
  align-items: center;
  padding: 2px var(--space-2);
  border-radius: var(--radius-pill);
  font-size: var(--type-caption-size);
  line-height: var(--type-caption-line);
  font-weight: 600;
  white-space: nowrap;
}

.pill-not-started {
  background: var(--surface-sunken);
  color: var(--ink-muted);
}

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

.pill-quote-received {
  background: var(--accent-tint);
  color: var(--accent);
}

.pill-po-issued {
  background: var(--accent-tint);
  color: var(--accent);
}

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

.pill-drawing-approved-code-1 {
  background: var(--success-tint);
  color: var(--success);
}

.pill-drawing-approved-with-comments-code-2 {
  background: var(--success-tint);
  color: var(--success);
}

.pill-drawing-returned-for-revision-code-3 {
  background: var(--danger-tint);
  color: var(--danger);
}

.pill-under-manufacturing {
  background: var(--accent-tint);
  color: var(--accent);
}

.pill-ready-for-dispatch {
  background: var(--accent-tint);
  color: var(--accent);
}

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

.pill-delivered-at-site {
  background: var(--success-tint);
  color: var(--success);
}

.pill-closed {
  background: var(--surface-sunken);
  color: var(--ink-muted);
}

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

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

.pill-vendor-not-responding {
  background: var(--warning-tint);
  color: var(--warning);
}

.pill-client-clearance-pending {
  background: var(--warning-tint);
  color: var(--warning);
}

.pill-quality-issue {
  background: var(--danger-tint);
  color: var(--danger);
}

.pill-payment-hold {
  background: var(--danger-tint);
  color: var(--danger);
}

.pill-other {
  background: var(--surface-sunken);
  color: var(--ink-muted);
}

/* 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: var(--surface-sunken);
  color: var(--ink-muted);
}

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

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

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

/* 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(--success-tint);
  color: var(--success);
}

.pill-ranked {
  background: var(--surface-sunken);
  color: var(--ink-muted);
}

.pill-unranked {
  background: var(--surface-sunken);
  color: var(--ink-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: var(--surface-sunken);
  color: var(--ink-muted);
}

.pill-pending-final-signoff {
  background: var(--accent-tint);
  color: var(--accent);
}

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

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

/* 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(--danger-tint);
  color: var(--danger);
}

/* 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(--danger-tint);
  color: var(--danger);
}
.pill-warn {
  background: var(--warning-tint);
  color: var(--warning);
}
.pill-info {
  background: var(--accent-tint);
  color: var(--accent);
}
.pill-good {
  background: var(--success-tint);
  color: var(--success);
}

/* 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(--accent-tint);
  color: var(--accent);
}

/* 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: 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: var(--surface-sunken);
  color: var(--ink-muted);
  font-size: var(--type-caption-size);
  font-weight: 600;
  padding: 2px 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(--success-tint);
  color: var(--success);
}
.pill-short-received,
.pill-held {
  background: var(--danger-tint);
  color: var(--danger);
}
.pill-over-billed {
  background: var(--danger-tint);
  color: var(--danger);
}
.pill-rate-variance {
  background: var(--warning-tint);
  color: var(--warning);
}
.pill-awaiting-invoice {
  background: var(--accent-tint);
  color: var(--accent);
}
.pill-awaiting-receipt {
  background: var(--accent-tint);
  color: var(--accent);
}
.pill-no-activity-yet,
.pill-no-po {
  background: var(--surface-sunken);
  color: var(--ink-muted);
}

/* A pill on a line of its own under a table cell's value (ADR-0172). A body
   cell is nowrap, capped at 16rem and ellipsised, so a pill BESIDE a long
   value overhangs the cap and is drawn as "…" — and widens the column on its
   way there. Under the value it is always whole, and the column sizes to the
   value alone. */
.cell-pill-below {
  display: block;
  margin-top: var(--space-1);
}

/* 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. Danger: matched, but
   there's nothing to land it on (no installation stage, or no rate on it) —
   the same danger over-billing uses, because it blocks a figure. */
.pill-code-ready {
  background: var(--success-tint);
  color: var(--success);
}
.pill-code-ambiguous {
  background: var(--warning-tint);
  color: var(--warning);
}
.pill-code-unmatched {
  background: var(--surface-sunken);
  color: var(--ink-muted);
}
.pill-code-unresolved {
  background: var(--danger-tint);
  color: var(--danger);
}

/* "Unverified" vendor chip (SPEC.md's "Vendor verification & cleanup") —
   sits with a vendor name: under it in a table cell (.cell-pill-below,
   ADR-0174), beside it elsewhere. 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: var(--surface-sunken);
  color: var(--ink-muted);
  font-size: var(--type-caption-size);
  font-weight: 600;
  padding: 2px 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: var(--surface-sunken);
  color: var(--ink-muted);
  font-size: var(--type-caption-size);
  font-weight: 600;
  padding: 2px var(--space-2);
}

.pill-selectable {
  background: var(--surface-sunken);
  color: var(--ink-muted);
  font-size: var(--type-caption-size);
  font-weight: 600;
  padding: 2px 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: var(--ink-faint);
  margin-right: var(--space-1);
}
.pill-sub-item,
.pill-main-item {
  background: var(--surface-sunken);
  color: var(--ink-muted);
  font-size: var(--type-caption-size);
  font-weight: 600;
  padding: 2px 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: var(--type-caption-size);
  color: var(--ink-muted);
  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: 1px solid var(--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;
}

/* A vendor's terms in the comparison grid: any length, so it wraps and grows
   with what is typed. The width must stay fixed, or field-sizing grows that too
   and the text runs out on one line. Where a browser has no field-sizing, it
   opens at the row count the screen worked out and can be dragged taller. */
.rate-cell-input--term {
  display: block;
  width: 11rem;
  padding: var(--space-2);
  resize: vertical;
  field-sizing: content;
}

/* The Lines tab's Clearance cell (ADR-0177): no wider than the column's own
   "Clearance qty" heading, so the input costs the line table no width. */
.rate-cell-input--qty {
  width: 5.5rem;
}

/* A refusal said under an input in a table cell (that same cell) — wrapped,
   since a body cell is nowrap and ellipsised and would cut the reason off
   mid-sentence. */
table.app-table td .field__error {
  white-space: normal;
}

/* A split line's other pieces and their POs (ADR-0178), said under its
   description — wrapped for the same reason as the refusal above. */
table.app-table td .split-pieces {
  font-size: 0.75rem;
  white-space: normal;
}

.rate-cell-input:focus {
  border-color: var(--accent);
}

/* ---------- 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(--accent-fill);
  border-color: var(--accent-fill);
  color: var(--on-accent);
}

/* ---------- 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 success of final approval — the record is closed. */
.pill-ready {
  background: var(--success-tint);
  color: var(--success);
}

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

/* 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: var(--warning-tint);
  color: var(--warning);
}

.pill-certified {
  background: var(--success-tint);
  color: var(--success);
}

/* 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 supply stage's review figures on the bill entry screen (ADR-0150): what
   arrived, what other bills already claim, and what this bill may take. */
.supply-review {
  margin-top: var(--space-2);
  font-size: 0.8125rem;
  color: var(--color-text-muted);
  text-align: left;
}

.supply-review dl {
  display: grid;
  grid-template-columns: auto auto;
  gap: 0 var(--space-3);
  margin: var(--space-2) 0 0;
}

.supply-review dd {
  margin: 0;
  text-align: right;
  white-space: nowrap;
}

.supply-review__billable {
  color: var(--color-text);
  font-weight: 600;
}

.supply-review__approve {
  margin-left: var(--space-2);
}

/* A refusal said in the row it refuses (ADR-0150) — not a queued message the
   coordinator would only see on the next page load. */
.row-error {
  margin: var(--space-2) 0 0;
  font-size: 0.8125rem;
  color: var(--color-bad);
  text-align: left;
}

/* 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: var(--warning-tint);
}

/* 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(--success-tint);
  color: var(--success);
}
.pill-part-paid {
  background: var(--warning-tint);
  color: var(--warning);
}
.pill-unpaid {
  background: var(--surface-sunken);
  color: var(--ink-muted);
}

/* 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: var(--surface-sunken);
  color: var(--ink-muted);
  font-size: var(--type-caption-size);
  font-weight: 600;
  padding: 2px var(--space-2);
}
.pill-unreconciled {
  background: var(--warning-tint);
  color: var(--warning);
  font-size: var(--type-caption-size);
  font-weight: 600;
  padding: 2px 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 info too — a record that carries on
   into a successor is a place, not an outcome. */
.pill-issued {
  background: var(--accent-tint);
  color: var(--accent);
}

.pill-extended {
  background: var(--accent-tint);
  color: var(--accent);
}

/* 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(--success-tint);
  color: var(--success);
}

/* 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: var(--warning-tint);
}

/* ---------- 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: var(--surface-sunken);
  color: var(--ink-muted);
}

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

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

/* ---------- 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. Danger: damaged — some of it can't be used. Danger: 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(--success-tint);
  color: var(--success);
}

.pill-damaged {
  background: var(--danger-tint);
  color: var(--danger);
}

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

/* ---------- 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: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font-size: var(--type-body-sm-size);
  font-family: var(--font-sans);
  background: var(--surface-card);
  color: var(--ink);
}

.cover-input:focus {
  border-color: var(--accent);
}

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

.cover-input--serial {
  min-width: 7rem;
}

.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 {
  /* The field's largest amount, 999999999999.99, whole in the box (L1). */
  min-width: 11rem;
}

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

/* Urgency bands — the age pills. Fine is neutral (nothing is late yet), watch
   is warning, overdue is danger (ADR-0185). The day boundaries are not here:
   dashboard/urgency.py holds both scales — work waiting on us ≤3 / 4–7 / >7
   days, work waiting on the client ≤30 / 31–60 / >60 — and is their only
   definition. */
.pill-fine {
  background: var(--surface-sunken);
  color: var(--ink-muted);
}

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

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

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

/* 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: var(--surface-sunken);
}

/* Cleanup C2: the segments wear the share-bar tokens (ADR-0216) — done the
   strongest shade, in flight a lighter one, not started the neutral rest — so
   the fill a reader sees ends where the caption's "x% started" says it does,
   and the darker part of it is the caption's "y% done". */
.progress-track__seg--done {
  background: var(--share-1);
}

/* 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(--share-4);
}

.progress-track__seg--not-started {
  background: var(--share-rest);
}

/* 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: var(--surface-sunken);
  color: var(--ink-muted);
}

/* ============================================================
   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. */

/* The caption carries two figures since cleanup C2 ("57% started · 0% done").
   Where the cell is too narrow for a bar and the caption side by side — a
   landing row's progress track — the caption drops under the bar rather than
   squeezing it to a sliver. */
.progress-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1) var(--space-2);
}

.progress-row .progress-track { flex: 1 1 calc(var(--space-8) * 2); 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 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;
}

@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%; }

  /* 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(--surface-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font: inherit;
  font-size: var(--type-body-sm-size);
  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: var(--tap-min);
  padding-inline: var(--space-4);
  /* The search field (docs/design-system/README.md): 44px, surface-card, a
     1px border, radius-sm + 2px, 16px text. Focus draws the shared ring. */
  background: var(--surface-card);
  border: 1px solid var(--border);
  border-radius: calc(var(--radius-sm) + 2px);
}

.line-search:focus-within {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

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

.line-search__key {
  flex: none;
  font: 600 var(--type-caption-size) var(--font-sans);
  color: var(--ink-muted);
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  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-float);
  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); }

/* The tender BOQ review table (ADR-0182 session 2): twelve columns that must
   all fit at 1440px, and a reader deciding a row's category and scope from its
   words. So the DESCRIPTION wraps in full and takes the most width, its
   numbered parent's text muted before it (the row label, E2); the sheet
   is a short label; the category cell wraps, so a flagged row's proposed name
   is never cut. The room comes from the narrow columns: tighter padding, and
   source pills that are smaller and wrap onto a second line. */
table.review-table thead th,
table.review-table tbody td { padding-inline: var(--space-2); }
table.review-table thead th { white-space: normal; }
table.review-table tbody td.review-table__description,
table.review-table tbody td.review-table__category {
  white-space: normal;
  overflow: visible;
  overflow-wrap: anywhere;
}
table.review-table tbody td.review-table__description { min-width: 18rem; max-width: none; }

/* On a laptop the words column is a fixed width (C1a): the parent's one
   ellipsised line is cut inside it rather than widening the table, so
   Category and Scope stay on screen at 1440 without scrolling sideways. */
@media (min-width: 641px) {
  table.review-table tbody td.review-table__description { width: 15rem; min-width: 15rem; max-width: 15rem; }
  table.review-table .review-table__description .row-label__parent { max-width: 15rem; }
}
table.review-table tbody td.review-table__sheet { max-width: 5rem; }
table.review-table tbody td.review-table__category { min-width: 7rem; max-width: 10rem; }
table.review-table .pill {
  white-space: normal;
  overflow-wrap: normal;
  font-size: 0.75rem;
  padding: 2px var(--space-2);
  line-height: 1.25;
}
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: var(--warning-tint); }

/* 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;
  /* Wraps: the reader chooses how many figures head the page (ADR-0158), and a
     strip that cannot wrap pushes the tenth one off the side of the frame. */
  flex-wrap: wrap;
  gap: var(--space-3) 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); }

/* The chosen figures — four by default — then a disclosure for the rest. 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 figures head the page is a setting touched once a
   month. Each row carries its figure's value and its plain-English meaning as
   well as its checkbox (ADR-0158), so this is also where the unchosen figures
   stay readable and where every definition is on screen without a hover —
   one disclosure, not three. */
.figure-pop {
  position: absolute;
  right: 0;
  top: calc(100% + var(--space-2));
  z-index: 30;
  width: 30rem;
  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-float);
  padding: var(--space-3);
}

.figure-pop__intro {
  font-size: 0.75rem;
  color: var(--color-text-muted);
  margin: 0 0 var(--space-2);
}

/* The LIST scrolls, not the popover: fourteen figures with a definition each
   are taller than a laptop screen, and the popover hangs off a STICKY frame —
   whatever falls below the window cannot be scrolled to. So the list takes the
   window's height less the nav, the frame above it and its own Save row
   (about 30rem together), and Save stays on screen under it.

   A plain block, NOT a column flexbox: a flex column with a max-height shrinks
   its rows to fit, and a row's min-height is the 44px touch minimum — so every
   row was squeezed to 44px and its two-line meaning printed over the next
   row's name. As blocks the rows keep the height their text needs and the
   list scrolls. */
.figure-pop__list {
  max-height: clamp(11rem, calc(100vh - 30rem), 34rem);
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* One figure, one block: the tick, the name and its value on the first line,
   the meaning under them at the row's full width, then a rule and clear air
   before the next name. */
.figure-pop__row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: start;
  gap: var(--space-1) var(--space-3);
  min-height: var(--touch-min);
  padding-block: var(--space-3);
  font-size: 0.8125rem;
  line-height: 1.35;
  cursor: pointer;
}

.figure-pop__row + .figure-pop__row { border-top: 1px solid var(--color-border); }
.figure-pop__label { font-weight: 600; min-width: 0; }
.figure-pop__meaning {
  /* From the name's column to the row's right edge, on the line below. */
  grid-column: 2 / -1;
  font-size: 0.75rem;
  color: var(--color-text-muted);
  line-height: 1.4;
}
.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); white-space: nowrap; }

.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-float);
  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;
  /* Clear of the sidebar or rail, which is fixed to the left edge above
     640px (design-system session B). */
  left: var(--frame-left);
  right: 0;
  bottom: 0;
  z-index: 40;
  /* The README's dark bar: ink behind, the card colour in front — the pair
     swaps in the dark theme, so it stays a dark-on-light inverse either way
     (design-system session E; it was eleven literal colours). */
  background: var(--ink);
  color: var(--surface-card);
  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: var(--control-border); }
.sel-tray__figures { display: flex; gap: var(--space-5); font-size: 0.8125rem; }
.sel-tray__figures b { font-variant-numeric: tabular-nums; }
.sel-tray__sum { display: none; }
.sel-tray__actions { display: flex; align-items: center; gap: var(--space-2); }

.sel-tray .btn-secondary {
  background: transparent;
  border: 1px solid var(--control-border);
  color: var(--surface-card);
}

.sel-tray .btn-ghost {
  background: transparent;
  border: 0;
  color: var(--surface-card);
}

/* 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-pill);
  font-size: 0.8125rem;
  font-weight: 600;
  background: var(--accent-tint);
  border: 1px solid transparent;
  color: var(--accent);
}

/* "Add filter" is a control, not an applied filter: outlined like a
   secondary button. */
.chip-filter--add {
  background: var(--surface-card);
  border-color: var(--control-border);
  color: var(--ink);
  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(--accent-fill);
  color: var(--on-accent);
  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 lifecycle states that 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. Returned is a warning (it waits on procurement),
   revoked is neutral (it stopped on purpose). */
.pill-returned {
  background: var(--warning-tint);
  color: var(--warning);
}

.pill-revoked {
  background: var(--surface-sunken);
  color: var(--ink-muted);
}

/* 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(--success-tint);
  color: var(--success);
}

.pill-unverified-state {
  background: var(--surface-sunken);
  color: var(--ink-muted);
}

/* 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: var(--surface-sunken);
  color: var(--ink-muted);
  border-radius: var(--radius-pill);
  font-size: var(--type-caption-size);
  font-weight: 600;
  padding: 2px 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;
  }
}

/* ============================================================
   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: var(--surface-sunken);
  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 var(--accent-tint);
}

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

.lc-step--off {
  background: var(--warning-tint);
  color: var(--warning);
}

.lc-step--off .lc-step__mark { background: var(--warning); border-color: var(--warning); }

/* 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;
}

/* "Learn more" (README, Words): help is one line; the longer explanation
   sits behind this text button and opens in place (cleanup C1). */
.learn-more > summary {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  font-size: var(--type-meta-size);
  color: var(--accent);
  list-style: none;
  cursor: pointer;
}

.learn-more > summary::-webkit-details-marker { display: none; }

.learn-more > :not(summary) { margin-top: 0; }

/* One "More" menu on a record header (cleanup C1): Remind me first, then the
   grave actions in their own section under a red "Undo" title — still apart
   from the routine bar, without a second red button beside it. */
.rec-actions__more .menu__items {
  min-width: 20rem;
  padding: var(--space-3);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.rec-actions__grave {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--divider);
}

.rec-actions__grave-title {
  margin: 0;
  font-size: var(--type-caption-size);
  font-weight: 600;
  color: var(--color-bad);
}

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

.rec-grave-item { display: flex; flex-direction: column; gap: var(--space-1); }
/* An outcome or a stage move drawn as a text link ("Mark withdrawn", "It went
   in — mark submitted") is drawn as text and answers across tap-min with its
   own 44px box, not the .btn::after strip: these submit at once, so the target
   never reaches over the words beside it (T3b). */
.rec-grave-item .btn-link,
.next-card__actions .btn-link {
  display: inline-flex;
  align-items: center;
  align-self: flex-start;
  min-height: var(--tap-min);
}
.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; }
/* The PO number procurement gives on Submit for approval (P10b, ADR-0259):
   the field and its one-line hint held to the form's own width, the hint
   wrapping, so the header's name keeps its room. */
.rec-decide .po-number-field { max-width: 24rem; 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__more .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(--accent-fill);
  border-color: var(--accent-fill);
  color: var(--on-accent);
}

/* 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(--accent-fill);
  border-color: var(--accent-fill);
  color: var(--on-accent);
  box-shadow: 0 0 0 3px var(--accent-tint);
}

.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: 1px solid var(--border);
  border-radius: var(--radius-sm);
  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);
  /* With the cells beside it: a long term's row can be taller than the grid's
     box, and a centred label would be out of sight at the start of the text. */
  vertical-align: top;
}

.cmp-rowhead__item { display: block; }

/* The row's words under its item label (E2): weight back to body, the
   parent on its one line and the row's own words wrapping inside the frozen
   column's 18rem rather than cut. */
.cmp-rowhead__words {
  margin-top: 2px;
  font-weight: 400;
  overflow-wrap: anywhere;
}

.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;
}

/* A vendor's terms for someone reading the grid. The cell is nowrap and the
   table is max-content wide, so the width cap is what makes the text wrap: without
   it, a long term stretches its whole column to one line. */
.cmp-term {
  display: block;
  max-width: 16rem;
  font-weight: 400;
  white-space: pre-line;
  overflow-wrap: anywhere;
}

.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: var(--success-tint); }

/* 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; }

/* ---------- The tender rate grid, E8 (ADR-0067 amendment) ----------
   The vendor header is two rows: who and their rank, then their state and a
   quiet Remove… that opens its one-line confirm. The name wraps inside 10rem,
   so a long vendor name makes its column taller, not the grid wider. */
.cmp-vendor__top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-2);
  max-width: 9rem;
}
.cmp-vendor__top .cmp-vendor__name {
  flex: 1 1 auto;
  min-width: 0;
  color: var(--ink);
  overflow-wrap: anywhere;
}
.cmp-vendor__top .pill { flex: 0 0 auto; }
/* Remove… is drawn one text line tall and answers across tap-min, the
   .btn::after strip ("draw small, tap big"). */
.cmp-vendor__remove > summary {
  position: relative;
  display: inline-flex;
  align-items: center;
}
.cmp-vendor__remove > summary::after {
  content: "";
  position: absolute;
  inset: min(0px, calc((100% - var(--tap-min)) / 2)) 0;
}
.cmp-vendor__status {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1) var(--space-2);
  max-width: 9rem;
  margin-top: var(--space-1);
  font-weight: 400;
}

/* A rate cell's state sits under its input — "Not quoted", or "Lowest" on
   the row's cheapest — so it makes the column taller, not wider. */
.cmp-rate__value {
  display: inline-flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--space-1);
}

/* The row's one red under its label, where a "Needs action" column used to
   take a column's width for a flag most rows do not have. */
.cmp-rowhead__flag {
  display: block;
  margin-top: var(--space-1);
  font-weight: 400;
}

/* Qty is a number and a unit, not a 9rem header. */
.cmp-scroll--rates table.app-table--grid thead th.num { min-width: 4rem; }

/* Costed from pins to the right of the box, as the item column pins to the
   left: a grid wider than a laptop scrolls its vendors between the two, and
   the choice is never cut at the box's edge. */
.cmp-scroll--rates table.app-table--grid .cmp-basis {
  position: sticky;
  right: 0;
  z-index: 2;
  background: var(--surface-card);
  box-shadow: -2px 0 0 var(--border);
}
.cmp-scroll--rates table.app-table--grid thead th.cmp-basis {
  z-index: 4;
  background: var(--surface-sunken);
}
.cmp-scroll--rates table.app-table--grid tfoot .cmp-basis { background: var(--surface-sunken); }

/* Each vendor's whole total says how many rows it covers, where the vendors
   priced different rows; and the cost basis total splits saved from
   suggested. Meta-sized, ink-muted: the figure above them is the one read. */
.cmp-option__rows,
.cmp-minimum dd.cmp-minimum__split {
  font-size: var(--type-meta-size);
  line-height: var(--type-meta-line);
  font-weight: 400;
  color: var(--ink-muted);
}

@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; }
}

/* The tender rate grid on a phone (E8): one card per BOQ row, not a squeezed
   table. The same <table> and the same inputs as the laptop's — one form, one
   set of names — drawn as blocks: the row's label, its qty, a line per vendor
   (their name, "Lowest" on the row's cheapest, the rate) and Costed from.
   The header row and the totals go; the panel above carries both. The page
   never scrolls sideways, and every control is 44px with a 16px input. */
@media (max-width: 640px) {
  .cmp-scroll--rates {
    max-height: none;
    overflow: visible;
    border: 0;
    border-radius: 0;
    background: none;
  }
  .cmp-scroll--rates table.app-table--grid,
  .cmp-scroll--rates table.app-table--grid tbody {
    display: block;
    width: auto;
    min-width: 0;
  }
  .cmp-scroll--rates table.app-table--grid thead,
  .cmp-scroll--rates table.app-table--grid tfoot { display: none; }
  .cmp-scroll--rates table.app-table--grid tbody tr {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    margin-bottom: var(--space-3);
    padding: var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface-card);
  }
  .cmp-scroll--rates table.app-table--grid tbody tr:hover { background: var(--surface-card); }
  .cmp-scroll--rates table.app-table--grid tbody th.cmp-rowhead,
  .cmp-scroll--rates table.app-table--grid tbody td {
    position: static;
    display: block;
    height: auto;
    min-width: 0;
    max-width: none;
    padding: 0;
    border: 0;
    box-shadow: none;
    text-align: left;
    white-space: normal;
  }
  .cmp-scroll--rates table.app-table--grid tbody th.cmp-rowhead,
  .cmp-scroll--rates table.app-table--grid tbody td.cmp-qty { background: none; }
  .cmp-scroll--rates table.app-table--grid tbody td.cmp-basis {
    position: static;
    background: none;
    box-shadow: none;
  }
  /* Up to three figures per vendor: they wrap inside the card, never past it. */
  .cmp-ranked .cmp-option__figures { flex-wrap: wrap; justify-content: flex-start; }
  .cmp-scroll--rates table.app-table--grid tbody td.cmp-qty { color: var(--ink-muted); }
  .cmp-scroll--rates table.app-table--grid tbody td.cmp-rate {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    padding: var(--space-1) var(--space-2);
    border-radius: var(--radius-sm);
  }
  .cmp-rate__vendor { min-width: 0; overflow-wrap: anywhere; }
  .cmp-rate__value {
    flex-direction: row;
    align-items: center;
    gap: var(--space-2);
    flex: 0 0 auto;
  }
  .cmp-scroll--rates .rate-cell-input {
    width: 7.5rem;
    min-height: var(--tap-min);
    font-size: var(--type-input-size);
  }
  .cmp-basis__label {
    display: block;
    margin-bottom: var(--space-1);
    font-size: var(--type-caption-size);
    color: var(--ink-muted);
  }
}

/* ============================================================
   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: var(--warning);
}

/* 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: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font-size: var(--type-body-sm-size);
  font-family: var(--font-sans);
  background: var(--surface-card);
  color: var(--ink);
}

.form-group input[type="number"]:focus,
.form-group input[type="email"]:focus,
.form-group input[type="tel"]:focus,
.form-group input[type="url"]:focus {
  border-color: var(--accent);
}

/* 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 neutral every not-started state in
   this file uses, preparing the info of work in hand, won the success of an
   approved record, withdrawn .pill-revoked's neutral (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: var(--surface-sunken);
  color: var(--ink-muted);
}

.pill-preparing {
  background: var(--accent-tint);
  color: var(--accent);
}

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

/* 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: var(--surface-sunken);
  color: var(--ink-muted);
}

.pill-withdrawn {
  background: var(--surface-sunken);
  color: var(--ink-muted);
}

/* 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;
}

/* ==========================================================================
   The new-project upload flow (ADR-0155) — the paste grid as the first
   screen after "New project", and the Check step's plain-language questions,
   on the approved design. Existing tokens only; the grid's engine and its
   data-grid-* hooks are ADR-0146's and unchanged.
   ========================================================================== */

/* The page head: the step frame on the left, the one primary action on the
   right, as the design draws it. Stacked on a narrow screen. */
.paste-head {
  display: flex;
  align-items: flex-start;
  gap: var(--space-4);
}

.paste-head__frame {
  flex: 1 1 auto;
  min-width: 0;
}

.paste-head__go {
  flex: none;
  margin-top: var(--space-5);
}

@media (max-width: 720px) {
  .paste-head { flex-direction: column; align-items: stretch; }
  .paste-head__go { margin-top: 0; }
}

/* "0 lines" on the left, the demoted file door on the right. */
.paste-toolbar {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-2);
  font-size: 0.875rem;
  color: var(--color-text-muted);
}

.paste-toolbar__upload { font-size: 0.8125rem; }

/* A tab's two questions, on one line above its grid. */
.paste-section {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--space-3) var(--space-4);
  margin-top: var(--space-3);
}

.paste-section__field {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  font-size: 0.8125rem;
  color: var(--color-text-muted);
  min-width: 12rem;
}

.paste-section__hint { margin: 0 0 var(--space-3); }

/* The sheet. Positioned so the paste guide can sit on the empty rows. */
.paste-sheet { position: relative; }

.paste-table thead th { vertical-align: bottom; }

.paste-col__name {
  display: block;
  font-weight: 600;
  white-space: nowrap;
}

/* A spreadsheet's rows, not a form's: one line per cell at the touch minimum.
   A description with an Alt+Enter break still grows by its resize handle. */
.paste-table textarea {
  min-height: var(--touch-min);
  height: var(--touch-min);
  padding-top: var(--space-2);
  padding-bottom: var(--space-2);
}

.paste-table tbody td {
  padding-top: var(--space-1);
  padding-bottom: var(--space-1);
}

/* The "→" on the flow's forward buttons, kept off the words. */
.btn .btn-arrow { margin-left: var(--space-2); }

/* What goes in the column, under its name — the design's grey example. */
.paste-col__eg {
  display: block;
  margin-top: 2px;
  font-size: 0.75rem;
  font-weight: 400;
  color: var(--color-text-muted);
  white-space: nowrap;
}

/* The column's paste slot, drawn as a drop target so it cannot be mistaken
   for row 1's cell: a paste here fills the whole column from row 1. */
.paste-table input.paste-col__box {
  margin-top: var(--space-2);
  min-height: 2rem;
  min-width: 6rem;
  font-size: 0.75rem;
  border: 1.5px dashed var(--color-primary);
  background: var(--color-primary-tint);
}

.paste-table input.paste-col__box::placeholder {
  color: var(--color-primary);
  font-weight: 600;
}

/* Beside the paste slots, in the Type column's head: which row they are. */
.paste-type-head__hint {
  display: flex;
  align-items: center;
  min-height: 2rem;
  margin-top: var(--space-2);
  font-size: 0.75rem;
  font-weight: 600;
  line-height: 1.2;
  color: var(--color-primary);
}

/* Nine columns since the rate columns (ADR-0163), on a sheet as wide as the
   app. Each paste slot sets its column's least width, so the slots are sized
   to what the column holds — a rate, or a few characters — and cells and
   fields give up side padding. The heads' own words wrap. Together that keeps
   Scope and Amount on the sheet instead of off its right edge. */
table.paste-table thead th,
table.paste-table tbody td {
  padding-left: var(--space-2);
  padding-right: var(--space-2);
}

.paste-table tbody input,
.paste-table textarea {
  padding-left: var(--space-2);
  padding-right: var(--space-2);
}

.paste-col--rate .paste-col__name,
.paste-col--rate .paste-col__eg,
.paste-col--narrow .paste-col__eg,
.paste-col--locked .paste-col__eg,
.paste-type-head .paste-col__eg { white-space: normal; }

.paste-table .paste-col--narrow input.paste-col__box { min-width: 4.5rem; }

.paste-type-head__hint { max-width: 6.5rem; }

.paste-table textarea { min-width: 10rem; }

/* Amount is the system's: locked, shaded, never typed into. */
.paste-col--locked,
.paste-table td.paste-amount {
  background: var(--color-surface-muted);
}

.paste-table td.paste-amount {
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.paste-lock {
  width: 12px;
  height: 12px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  vertical-align: -1px;
}

/* The row number, which is also the row's exclude button. */
.paste-table .paste-gutter {
  width: 3.25rem;
  padding-left: var(--space-1);
  padding-right: var(--space-1);
  background: var(--color-surface-muted);
  text-align: center;
}

.paste-gutter__btn {
  width: 100%;
  min-height: var(--touch-min);
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-text-muted);
  font: inherit;
  font-size: 0.8125rem;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}

.paste-gutter__btn:hover {
  background: var(--color-border);
  color: var(--color-text);
}

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

/* An excluded row stays in place, struck through (app.js strikes the cells),
   shaded, out of the total. */
.paste-table tr[data-excluded] td { background: var(--color-surface-muted); }

.paste-table tr[data-excluded] .paste-gutter__btn {
  background: var(--color-border-strong);
  color: var(--color-text);
}

.paste-table tr[data-excluded] .paste-gutter__btn::before { content: "⊘ "; }

.paste-table tr[data-excluded] td.paste-amount {
  color: var(--color-text-muted);
  text-decoration: line-through;
}

/* "Paste your BOQ columns here", on the empty grid itself. It lets every
   click through to the cell under it, and goes the moment anything lands. */
.paste-guide {
  display: none;
  position: absolute;
  left: 50%;
  top: 14rem;
  transform: translateX(-50%);
  width: min(34rem, calc(100% - 2 * var(--space-4)));
  padding: var(--space-6) var(--space-6);
  background: var(--color-surface);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-float);
  text-align: center;
  pointer-events: none;
}

[data-grid-blank] .paste-guide { display: block; }

.paste-guide__icon {
  width: 28px;
  height: 28px;
  fill: none;
  stroke: var(--color-primary);
  stroke-width: 1.5;
}

.paste-guide__title {
  margin: var(--space-2) 0;
  font-size: 1.5rem;
  font-weight: 700;
  letter-spacing: -0.01em;
}

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

.paste-guide__more {
  margin: 0;
  font-size: 0.875rem;
  color: var(--color-text-muted);
}

.paste-guide kbd {
  display: inline-block;
  min-width: 1.75rem;
  padding: 1px var(--space-2);
  border: 1px solid var(--color-border-strong);
  border-radius: 4px;
  background: var(--color-surface);
  font-family: var(--font-sans);
  font-size: 0.8125rem;
  box-shadow: 0 1px 0 var(--color-border-strong);
}

/* The subtotal flag: what it found, and the one button that resolves it. */
.paste-flag {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2) var(--space-4);
}

.paste-flag p { margin: 0; }

.paste-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-1) var(--space-4);
}

.paste-foot__total {
  margin: 0;
  font-size: 1.125rem;
  font-variant-numeric: tabular-nums;
}

.paste-foot .field__hint { margin: 0; }

.paste-tips summary {
  cursor: pointer;
  font-size: 0.875rem;
  color: var(--color-primary);
}

.paste-tips ul {
  margin: var(--space-2) 0 0;
  padding-left: var(--space-5);
  font-size: 0.875rem;
  color: var(--color-text-muted);
}

/* --- The Check step: questions on the left, the working on the right. --- */
.po-check {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-6);
}

@media (min-width: 960px) {
  .po-check { grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr); }
}

.po-check__lead {
  margin: 0;
  font-size: 1.5rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums;
}

.po-check__sub {
  margin: var(--space-1) 0 var(--space-5);
  font-size: 0.875rem;
  color: var(--color-text-muted);
}

/* The PO total box: a rupee prefix and a big figure. */
.po-money {
  display: flex;
  align-items: stretch;
  border: 1.5px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  overflow: hidden;
}

.po-money:focus-within {
  border-color: var(--color-primary);
  box-shadow: 0 0 0 3px var(--accent-tint);
}

.po-money__sign {
  display: flex;
  align-items: center;
  padding: 0 var(--space-4);
  background: var(--color-surface-muted);
  color: var(--color-text-muted);
  font-size: 1.25rem;
}

.po-money input[type="text"] {
  border: 0;
  border-radius: 0;
  min-height: 3.5rem;
  font-size: 1.5rem;
  font-variant-numeric: tabular-nums;
  box-shadow: none;
}

/* One question per row: the question on the left, its answer on the right. */
.po-q {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2) var(--space-4);
  padding: var(--space-4) 0;
  border-top: 1px solid var(--color-border);
}

.po-q__text { margin: 0; font-size: 0.9375rem; }

.po-q__sub {
  display: block;
  font-size: 0.8125rem;
  color: var(--color-text-muted);
}

.po-q__more {
  flex-basis: 100%;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
}

.po-q__more .field { margin: 0; flex: 1 1 10rem; }

.po-q__more .po-q__hint { flex-basis: 100%; margin: 0; }

/* The step's buttons keep their words on one line and wrap as whole buttons. */
.po-actions { flex-wrap: wrap; }
.po-actions .btn { white-space: nowrap; }

/* Hidden only where the answer that does not need them is picked, so a
   browser without :has() shows the boxes rather than hiding them for good. */
.po-q:has(input[name="includes_gst"][value="no"]:checked) .po-q__gst-rate,
.po-q:has(input[name="has_adjustment"][value="none"]:checked) .po-q__more {
  display: none;
}

.po-q__gst-rate {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: 0.875rem;
  color: var(--color-text-muted);
}

.po-q__gst-rate input[type="text"] { width: 4.5rem; text-align: right; }

/* A two- or three-way answer as one segmented control: radios underneath, so
   it posts and works without a script. */
.seg {
  display: inline-flex;
  border: 1.5px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  overflow: hidden;
}

.seg label {
  position: relative;
  display: inline-flex;
  align-items: center;
  min-height: var(--touch-min);
  padding: 0 var(--space-4);
  border-left: 1px solid var(--color-border-strong);
  font-size: 0.875rem;
  cursor: pointer;
  background: var(--color-surface);
}

.seg label:first-child { border-left: 0; }

.seg input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.seg label:has(input:checked) {
  background: var(--accent-fill);
  color: var(--on-accent);
}

.seg label:has(input:focus-visible) {
  outline: 2px solid var(--color-primary);
  outline-offset: -3px;
}

/* The working: every term of the sum, so "expected" is never taken on trust. */
.po-working {
  padding: var(--space-5);
  border-radius: var(--radius-md);
  background: var(--color-surface-muted);
}

.po-working__title {
  margin: 0 0 var(--space-3);
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}

.po-working__row {
  display: flex;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-2) 0;
  font-variant-numeric: tabular-nums;
}

.po-working__row--sum {
  border-top: 1.5px solid var(--color-text);
  font-weight: 700;
}

.po-working__row--po { font-weight: 700; }

.po-verdict {
  display: flex;
  gap: var(--space-3);
  align-items: flex-start;
  margin-top: var(--space-4);
  padding: var(--space-4);
  border-radius: var(--radius-md);
}

.po-verdict--match { background: var(--color-good-tint); color: var(--color-good); }
.po-verdict--gap { background: var(--warning-tint); color: var(--warning); }

.po-verdict__title { margin: 0; font-size: 1.125rem; font-weight: 700; }
.po-verdict__sub { margin: var(--space-1) 0 0; font-size: 0.8125rem; }

.po-gap-ways {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-top: var(--space-4);
}

.po-gap-ways .option-row { text-decoration: none; color: var(--color-text); }
.po-gap-ways .option-row__helper { color: var(--color-text-muted); }

.po-gap-ways__reason {
  flex-direction: column;
  align-items: stretch;
  cursor: default;
}

.po-sections { margin-top: var(--space-5); }

.po-sections summary {
  cursor: pointer;
  font-size: 0.875rem;
  color: var(--color-primary);
}

/* ============================================================
   THE PHONE FRAME (ADR-0184)
   ============================================================
   At 640px and under — the breakpoint this file already uses for phones —
   the app gets a phone's frame: a slim top bar holding the page title, a
   fixed bottom bar (Home · Inbox · Menu), and denser cards, headers and
   toolbars under them. ABOVE 640px NOTHING HERE APPLIES: the three rules
   outside the media query below only hide phone-only parts or keep a new
   wrapper out of the layout, so a desktop page renders exactly as it did.

   The top nav is hidden on a phone rather than restyled: its links are on the
   Menu page, which renders from the same list (core/navigation.py). */

.phone-bar,
.tab-bar,
.phone-only {
  display: none;
}

/* The Menu page (ADR-0184): every link the user's bar offers, grouped under
   department headings, as grouped-list rows — 48px, a chevron, any count at
   the right. Only the phone frame links here, but it reads at any width. */
.menu-group + .menu-group { margin-top: var(--space-5); }

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

.menu-list {
  list-style: none;
  margin: 0;
  padding: 0;
  background: var(--surface-card);
  border-radius: var(--radius-md);
  overflow: hidden;
}

.menu-list > li + li { border-top: 1px solid var(--divider); }

.menu-list form { margin: 0; }

.menu-list__row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  min-height: 48px;
  padding: 0 var(--space-4);
  background: none;
  border: 0;
  color: var(--color-text);
  font-family: var(--font-sans);
  font-size: 1rem;
  text-align: left;
  cursor: pointer;
}

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

.menu-list__label {
  flex: 1 1 auto;
  min-width: 0;
}

.menu-list__chevron {
  color: var(--color-border-strong);
  font-size: 1.375rem;
  line-height: 1;
}

.menu-list__row--static {
  color: var(--color-text-muted);
  font-size: 0.875rem;
  cursor: default;
}

.menu-list__row--static:hover { background: none; }

.menu-list__row--danger { color: var(--color-bad); font-weight: 600; }

/* The record header's facts share one span so a phone can hold them to one
   line; anywhere wider the span is not a box at all and the facts lay out
   exactly as they did before it existed. */
.rec-head__facts {
  display: contents;
}

@media (max-width: 640px) {
  :root {
    --phone-bar-h: 52px;
    /* 64px of bar, plus the home indicator's strip on a notched phone. */
    --tab-bar-h: calc(64px + env(safe-area-inset-bottom));
  }

  .phone-only { display: inline; }
  .phone-hide { display: none; }

  /* Only where the phone frame exists — a signed-in page. The login page and
     a signed-out error page keep the top nav and its Log in link. */
  body:has(> .phone-bar) .app-nav { display: none; }

  /* ---------- Top bar ---------- */

  .phone-bar {
    position: sticky;
    top: 0;
    z-index: 30;
    display: flex;
    align-items: center;
    gap: var(--space-1);
    height: calc(var(--phone-bar-h) + env(safe-area-inset-top));
    padding: env(safe-area-inset-top) var(--app-gutter) 0 var(--space-1);
    /* Solid, not the desktop nav's frosted glass: a title over scrolling rows
       has to be readable whatever is under it. */
    background: var(--color-surface);
    border-bottom: 1px solid var(--color-border);
  }

  .phone-bar__lead {
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: var(--touch-min);
    min-height: var(--touch-min);
    color: var(--color-text);
  }

  .phone-bar__lead:hover { text-decoration: none; }

  .phone-bar__brand {
    padding-inline: var(--space-2);
    font-weight: 700;
    font-size: 0.9375rem;
    letter-spacing: -0.01em;
  }

  .phone-bar__logo {
    height: 28px;
    width: auto;
    max-width: 96px;
    object-fit: contain;
  }

  .phone-bar__back svg {
    width: 26px;
    height: 26px;
    fill: none;
    stroke: var(--color-primary);
    stroke-width: 2.2;
    stroke-linecap: round;
    stroke-linejoin: round;
  }

  .phone-bar__title {
    flex: 1 1 auto;
    min-width: 0;
    font-size: 1.25rem;
    font-weight: 600;
    letter-spacing: -0.01em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  /* ---------- Bottom bar ---------- */

  .tab-bar {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 35;
    display: flex;
    height: var(--tab-bar-h);
    padding-bottom: env(safe-area-inset-bottom);
    background: var(--color-surface);
    border-top: 1px solid var(--color-border);
  }

  .tab-bar__tab {
    flex: 1 1 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    min-height: var(--touch-min);
    color: var(--color-text-muted);
    font-size: 0.75rem;
    font-weight: 500;
  }

  .tab-bar__tab:hover { text-decoration: none; }

  .tab-bar__icon {
    position: relative;
    width: 26px;
    height: 26px;
  }

  .tab-bar__icon svg {
    display: block;
    width: 26px;
    height: 26px;
  }

  .tab-bar__shape,
  .tab-bar__detail {
    fill: none;
    stroke: currentColor;
    stroke-width: 1.7;
    stroke-linejoin: round;
    stroke-linecap: round;
  }

  /* The active tab: a FILLED icon in the primary colour, and a bolder label —
     never a thin underline, which a thumb covers. */
  .tab-bar__tab--active {
    color: var(--color-primary);
    font-weight: 600;
  }

  .tab-bar__tab--active .tab-bar__shape { fill: currentColor; }
  .tab-bar__tab--active .tab-bar__detail { stroke: var(--color-surface); }
  /* The magnifier's handle sits outside its filled lens, so it keeps the
     tab's own colour when active. */
  .tab-bar__tab--active .tab-bar__detail--stroke { stroke: currentColor; stroke-width: 2.2; }

  .tab-bar__badge {
    position: absolute;
    top: -5px;
    left: 15px;
    min-width: 20px;
    height: 20px;
    padding: 0 5px;
    border: 2px solid var(--color-surface);
    border-radius: var(--radius-pill);
    background: var(--badge);
    color: var(--on-accent);
    font-size: var(--type-tab-label-size);
    font-weight: 700;
    line-height: 16px;
    text-align: center;
  }

  /* ---------- What the bars leave for the page ---------- */

  .app-main {
    padding-top: var(--space-3);
    /* The last row clears the bottom bar, with room to spare. */
    padding-bottom: calc(var(--tab-bar-h) + var(--space-8));
  }

  /* And clears the selection tray as well, while there is one. */
  body:has(.sel-tray:not(.sel-tray--page)) .app-main {
    padding-bottom: calc(var(--tab-bar-h) + 14rem);
  }

  /* Anything scrolled into view — a focused field, an opened panel, a popover's
     Save button — stops clear of both bars instead of under them. */
  html {
    scroll-padding-top: calc(var(--phone-bar-h) + env(safe-area-inset-top));
    scroll-padding-bottom: calc(var(--tab-bar-h) + var(--space-2));
  }

  html:has(.sel-tray:not(.sel-tray--page)) {
    scroll-padding-bottom: calc(var(--tab-bar-h) + 10rem);
  }

  /* The frames are flush with the bar above them; their negative margin
     follows the phone's smaller top padding. */
  .proj-head,
  .rec-head {
    margin-top: calc(var(--space-3) * -1);
  }

  /* The selection tray sits ON the bottom bar, not behind it. The bar already
     pays for the home indicator, so the tray needs no inset of its own. */
  .sel-tray { bottom: var(--tab-bar-h); }

  /* Three lines, each one line: the count, the two figures, the buttons.
     The wide tray's "All N selected" beside each figure and the Create PO
     button's own count and value are the desktop's (the template carries
     both and hides one); a phone states the count once, above. */
  .sel-tray {
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-2);
    padding-block: var(--space-2);
  }

  .sel-tray__facts {
    flex-direction: column;
    align-items: stretch;
    flex-wrap: nowrap;
    gap: 2px;
  }

  .sel-tray__count { font-size: 0.875rem; line-height: 1.3; }
  .sel-tray__note { font-size: 0.75rem; }
  .sel-tray__figures { display: none; }

  .sel-tray__sum {
    display: block;
    font-size: 0.75rem;
    line-height: 1.35;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    color: var(--control-border);
  }

  .sel-tray__sum b { color: var(--surface-card); }

  /* 12px a side leaves "Create PO →" whole on a 320px phone. */
  .sel-tray__actions .btn {
    flex: none;
    white-space: nowrap;
    padding-inline: var(--space-3);
    font-size: 0.875rem;
  }

  .sel-tray__actions .btn-ghost {
    border: none;
    padding-inline: var(--space-2);
  }

  .sel-tray__actions .btn-secondary {
    background: transparent;
    border: 1px solid var(--control-border);
  }

  .sel-tray__actions .btn-primary {
    flex: 1 1 0;
    min-width: 0;
  }

  .sel-tray__actions .btn-primary > span {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  /* The room the tray needs at the foot of the lines panel. */
  .sel-tray__spacer { height: 112px; }

  /* The reminder sheet is a sheet: it covers the bar while it is open, and
     keeps its last button above the home indicator. */
  .reminder-sheet {
    padding-bottom: calc(var(--space-6) + env(safe-area-inset-bottom));
  }

  /* ---------- Title cards ---------- */

  /* A screen's title card (.page-head — the list and landing screens) hands
     its title to the top bar and drops its description. What else it holds
     stays; with only header buttons left it loses its card, and with nothing
     left at all it goes. */
  .page-head > .card-title,
  .page-head > .card-subtitle,
  .page-head > .card-header > div:first-child:has(> .card-title) {
    display: none;
  }

  .page-head:not(:has(> :not(.card-title, .card-subtitle, .card-header))) {
    background: none;
    box-shadow: none;
    padding: 0;
  }

  .page-head:not(:has(> :not(.card-title, .card-subtitle, .card-header))):not(:has(> .card-header > :not(div:first-child))) {
    display: none;
  }

  .page-head:not(:has(> :not(.card-title, .card-subtitle, .card-header))):not(:has(> .card-header > :not(div:first-child))) + * {
    margin-top: 0;
  }

  /* ---------- Record headers ---------- */

  /* Stretch, not the desktop's flex-start: in a column that left the title
     block as wide as its longest line, which pushed a 390px page out to 409px
     on the PO page — the sideways scroll the audit found. */
  .proj-head__bar,
  .rec-head__bar {
    align-items: stretch;
    gap: var(--space-2);
    padding-block: var(--space-3) var(--space-2);
  }

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

  .proj-head__name,
  .rec-head__name {
    font-size: 1.1875rem;
  }

  /* The back arrow in the top bar is the way up now. */
  .rec-head__crumbs { display: none; }

  /* One meta line — client · code, then the status pill — and every other
     pill on one wrapping line of small pills under it. The facts give way
     with an ellipsis before the status pill is pushed off the line. */
  .proj-head__meta {
    row-gap: 0;
    margin-top: 2px;
    font-size: 0.8125rem;
  }

  .proj-head__meta > * { order: 5; }

  .proj-head__facts {
    order: 1;
    min-width: 0;
    max-width: calc(100% - 8.5rem);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .proj-head__status { order: 2; }

  .proj-head__meta::after {
    content: "";
    order: 3;
    flex-basis: 100%;
  }

  .rec-head__id {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    column-gap: var(--space-2);
  }

  .rec-head__name { flex: 0 0 100%; }

  .rec-head__meta { display: contents; }

  .rec-head__meta > * { order: 5; }

  .rec-head__facts {
    order: 1;
    display: block;
    min-width: 0;
    max-width: calc(100% - 8.5rem);
    margin-top: 2px;
    font-size: 0.8125rem;
    color: var(--color-text-muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .rec-head__facts + .pill { order: 2; margin-top: 2px; }

  .rec-head__id::after {
    content: "";
    order: 3;
    flex-basis: 100%;
  }

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

  /* The lifecycle track scrolls inside itself, but the screen-reader text on
     its current step is absolutely positioned against an ancestor OUTSIDE
     that scroll box — so it escaped the clip and widened the PO page to
     409px. Positioning the track contains it. */
  .lc-track__steps { position: relative; }

  .proj-head__meta > .pill:not(.proj-head__status),
  .rec-head__meta > :not(.rec-head__facts):not(.rec-head__facts + .pill),
  .rec-head .row-chips .pill {
    margin-top: var(--space-1);
  }

  /* Record actions: one row, each button as wide as its label, the primary
     filled and the rest outlined. app.js moves what does not fit (always
     anything past the third) into a ⋯ menu at the end of the row. */
  .proj-head__actions,
  .rec-actions {
    position: relative;
    justify-content: flex-start;
  }

  .proj-head__actions .btn,
  .proj-head__actions .menu,
  .rec-head__actions .btn,
  .rec-head__actions .menu {
    flex: none;
  }

  .proj-head__actions .menu > summary,
  .rec-head__actions .menu > summary {
    width: auto;
  }

  /* Still 44px tall: the tap target is the box (tests/test_project_page_tabs.py
     measures it), and a shorter button with a stretched hit area is a second
     thing to keep in step for 4px of height. */
  .proj-head__actions .btn,
  .rec-head__actions .btn {
    padding-inline: var(--space-4);
    font-size: 0.875rem;
  }

  .proj-head__actions .btn-secondary,
  .rec-head__actions .btn-secondary {
    background: transparent;
  }

  /* The ⋯ menu opens across the whole action row, not under its own 44px. */
  .phone-more { position: static; }

  .phone-more > summary {
    min-width: var(--touch-min);
    padding-inline: var(--space-3);
    font-size: 1.125rem;
    letter-spacing: 0.05em;
  }

  .phone-more__items {
    left: 0;
    right: 0;
    width: auto;
    min-width: 0;
    gap: 0;
  }

  .phone-more__items > *,
  .phone-more__items form .btn,
  .phone-more__items > .btn {
    width: 100%;
  }

  .phone-more__items .btn {
    justify-content: flex-start;
    min-height: var(--touch-min);
    background: none;
    box-shadow: none;
    border-radius: var(--radius-sm);
    color: var(--color-text);
    font-weight: 500;
  }


  /* ---------- Search and filters ---------- */

  /* One row: the search box and a Filters button. Active filters are chips
     under it; the sentence saying there are none, and the paragraph saying
     what a search crosses, are not shown. */
  .card.toolbar-card {
    background: none;
    box-shadow: none;
    padding: 0;
  }

  .line-toolbar {
    position: relative;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
  }

  .line-toolbar > .line-search {
    order: 1;
    flex: 1 1 0;
    min-width: 0;
    min-height: var(--touch-min);
    padding-inline: var(--space-3);
    box-shadow: none;
  }

  .line-search__key,
  .line-search__scope,
  .line-toolbar__none {
    display: none;
  }

  .line-toolbar__filters { display: contents; }
  .line-toolbar__filters > * { order: 4; }
  .line-toolbar__filters > .menu:first-of-type { order: 2; position: static; }
  .line-toolbar__filters > .menu { width: auto; flex: none; }

  /* Open, the filter form drops across the whole toolbar rather than
     inside the narrow Filters button it now hangs from. */
  .line-toolbar__filters > .menu:first-of-type > .menu__items--form {
    position: absolute;
    top: calc(100% + var(--space-1));
    left: 0;
    right: 0;
    width: auto;
    max-height: 70vh;
    box-shadow: var(--shadow-float);
  }

  .line-toolbar__filters > .menu:first-of-type > summary {
    min-height: var(--touch-min);
    padding-inline: var(--space-4);
  }

  .line-toolbar::after {
    content: "";
    order: 3;
    flex-basis: 100%;
    margin-top: calc(var(--space-2) * -1);
  }

  .line-toolbar__filters > .chip-filter {
    font-size: 0.75rem;
  }

  /* ---------- Phone sizes ---------- */

  /* 16px in every field, table cells included: under 16px, iOS zooms the
     whole page on focus and leaves it zoomed. !important because a dozen
     component rules set smaller field sizes, each more specific than this. */
  input,
  select,
  textarea {
    font-size: 16px !important;
  }

  .card {
    padding: var(--space-3);
  }

  .card .card { padding: var(--space-2); }

  /* The figure picker opens in place on a phone rather than floating: a
     floating panel that is the last thing on a short page has nothing below
     it to scroll by, so its Save button sat under the bottom bar for good. */
  .figure-pop {
    position: static;
    width: 100%;
    max-width: none;
    margin-top: var(--space-2);
  }

  /* A tab strip with more tabs than fit says so with a fade at its right
     edge (app.js sets tabs--more, and scrolls the active tab into view). */
  .tabs.tabs--more {
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 2.5rem), transparent);
    mask-image: linear-gradient(to right, #000 calc(100% - 2.5rem), transparent);
  }

  /* A reminder's snooze date runs to the screen edge no longer. */
  .reminder-row__snooze form { flex-wrap: wrap; }

  .reminder-row__snooze input[type="date"] {
    min-width: 0;
    max-width: 100%;
  }
}

/* ============================================================
   THE README'S COMPONENTS (design-system sessions C–F)
   ============================================================
   Built from docs/design-system/README.md's Components, on tokens only:
   the page header, section labels, list groups and list rows (one- and
   two-line), number cards, the segmented control, the search field, icon
   circles and the text button. Home and the Inbox were the first screens on
   them (session C). */

.page-header {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-3) var(--space-4);
  margin-bottom: var(--space-6);
}

.page-header__text { min-width: 0; }

.page-header__title {
  margin: 0;
  font-size: var(--type-title-size);
  line-height: var(--type-title-line);
  font-weight: var(--type-title-weight);
  color: var(--ink);
}

.page-header__meta {
  margin: var(--space-1) 0 0;
  font-size: var(--type-meta-size);
  line-height: var(--type-meta-line);
  color: var(--ink-muted);
}

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

.inline-form { display: inline; margin: 0; }

/* Accent text, no box (README, Buttons). Answers across 44px. */
.text-btn {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  padding: 0 var(--space-2);
  border: 0;
  background: none;
  color: var(--accent);
  font-family: var(--font-sans);
  font-size: var(--type-body-sm-size);
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
}

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

/* A filled or outlined button drawn at the compact 36px (top bar, toolbars). */
.btn.btn-compact {
  min-height: var(--button-compact);
  padding: 0 var(--space-3);
  font-size: var(--type-body-sm-size);
}

.section-label {
  margin: 0 0 var(--space-2) var(--space-1);
  font-size: var(--type-section-label-size);
  line-height: var(--type-section-label-line);
  font-weight: var(--type-section-label-weight);
  letter-spacing: var(--type-section-label-tracking);
  text-transform: uppercase;
  color: var(--ink-muted);
}

.section-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
}

.section-head .section-label { margin-bottom: 0; }

/* A list group: rows on one card, a divider hairline between them. */
.list-group {
  margin: 0;
  padding: 0;
  list-style: none;
  background: var(--surface-card);
  border-radius: var(--radius-md);
  overflow: hidden;
}

.list-group > li + li { border-top: 1px solid var(--divider); }

.list-group__empty {
  padding: var(--space-4);
  font-size: var(--type-body-sm-size);
  line-height: var(--type-body-sm-line);
  color: var(--ink-muted);
}

/* The list row (README, Components): the whole row is the tap target. Line 1
   the title and, right-aligned, the amount; line 2 the meta and an age pill. */
.list-row {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--space-1);
  min-height: var(--row-2);
  padding: var(--space-3) var(--space-4);
  color: var(--ink);
}

a.list-row:hover {
  background: var(--surface-sunken);
  text-decoration: none;
}

.list-row__line,
.list-row__main {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  min-width: 0;
}

.list-row__main {
  flex: 1 1 auto;
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-1);
}

.list-row__title {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--type-body-size);
  line-height: var(--type-body-line);
  font-weight: 500;
}

.list-row__amount,
.list-row__count {
  flex: none;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  white-space: nowrap;
}

.list-row__amount { font-size: var(--type-body-size); }

.list-row__count {
  min-width: var(--space-6);
  text-align: right;
  font-size: var(--type-heading-size);
}

.list-row__meta {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--type-meta-size);
  line-height: var(--type-meta-line);
  color: var(--ink-muted);
}

.list-row .pill { flex: none; }

/* A count-only row ("Needs you"): label, then pill, count and a chevron. */
.list-row--count,
.list-row--icon {
  flex-direction: row;
  align-items: center;
  justify-content: flex-start;
  gap: var(--space-3);
}

.list-row--count { min-height: var(--row-1); }

.list-row__chevron {
  flex: none;
  width: 16px;
  height: 16px;
  color: var(--ink-faint);
}

/* A queue with nothing in it stays and says so (README principle 2). */
.list-row__clear {
  flex: none;
  margin-left: auto;
  font-size: var(--type-meta-size);
  font-weight: 600;
  color: var(--success);
}

.list-row--clear .list-row__title { color: var(--ink-muted); font-weight: 400; }

/* News rather than work (material that arrived): greyed, never urgent. */
.list-row--quiet .list-row__title { color: var(--ink-muted); }

.list-end {
  margin: var(--space-4) 0;
  text-align: center;
  font-size: var(--type-meta-size);
  color: var(--ink-muted);
}

/* The tinted circle an Inbox or approval row leads with. */
.icon-circle {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: calc(var(--space-8) + var(--space-1));
  height: calc(var(--space-8) + var(--space-1));
  border-radius: var(--radius-pill);
  background: var(--accent-tint);
  color: var(--accent);
}

.icon-circle--danger {
  background: var(--danger-tint);
  color: var(--danger);
}

.unread-dot {
  display: inline-block;
  width: var(--space-2);
  height: var(--space-2);
  margin-right: var(--space-1);
  border-radius: var(--radius-pill);
  background: var(--accent);
  vertical-align: middle;
}

.inbox-row--unread .list-row__title { font-weight: 600; }

/* Number card: a caption over a number, under 80px tall. */
.number-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-2);
}

.number-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-3);
  border-radius: var(--radius-md);
  background: var(--surface-card);
  min-width: 0;
}

.number-card__label {
  font-size: var(--type-caption-size);
  line-height: var(--type-caption-line);
  font-weight: var(--type-caption-weight);
  color: var(--ink-muted);
}

.number-card__value {
  font-size: var(--type-number-size);
  line-height: var(--type-number-line);
  font-weight: var(--type-number-weight);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.number-card__suffix {
  font-size: var(--type-meta-size);
  font-weight: 400;
  color: var(--ink-muted);
}

/* Segmented control: views of the same list (README). */
.segmented {
  display: flex;
  gap: 2px;
  padding: 2px;
  margin-bottom: var(--space-4);
  border-radius: calc(var(--radius-sm) + 1px);
  background: var(--surface-sunken);
}

.segmented__item {
  flex: 1 1 0;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: var(--button);
  padding: 0 var(--space-3);
  border-radius: var(--radius-sm);
  color: var(--ink);
  font-size: var(--type-body-sm-size);
  font-weight: 500;
  white-space: nowrap;
}

.segmented__item:hover { text-decoration: none; }

.segmented__item--on {
  background: var(--surface-card);
  box-shadow: var(--shadow-segment);
  font-weight: 600;
}

/* The search field (README): 44px, a magnifier, 16px text. On Home it is a
   button that opens Jump anywhere (session D). */
.search-field {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: min(22rem, 100%);
  min-height: var(--tap-min);
  padding: 0 var(--space-3);
  border: 1px solid var(--border);
  border-radius: calc(var(--radius-sm) + 2px);
  background: var(--surface-card);
  color: var(--ink-muted);
  font-family: var(--font-sans);
  font-size: var(--type-input-size);
  text-align: left;
  cursor: text;
}

.search-field__placeholder { flex: 1 1 auto; }

.search-field__key {
  padding: 0 var(--space-1);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font-family: var(--font-sans);
  font-size: var(--type-caption-size);
  color: var(--ink-muted);
}

/* ---------- Home ---------- */

.home__grid {
  display: grid;
  gap: var(--space-5);
  grid-template-columns: minmax(0, 1fr);
  grid-template-areas: "needs" "numbers" "list";
}

.home__needs { grid-area: needs; }
.home__numbers { grid-area: numbers; }
.home__list { grid-area: list; }

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

@media (min-width: 1024px) {
  .home__grid {
    grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
    grid-template-areas: "needs numbers" "list numbers";
    align-items: start;
    gap: var(--space-6);
  }

  .home__numbers .number-grid { grid-template-columns: minmax(0, 1fr); }
}

/* On a phone the top bar carries the title, so the page header keeps only
   what is not in it; and a phone's quick action sits in that bar. */
.phone-bar__actions:empty { display: none; }

@media (max-width: 640px) {
  .page-header { margin-bottom: var(--space-4); }
  .page-header__title,
  .page-header .phone-hide { display: none; }
  .page-header:not(:has(.page-header__meta)):not(:has(.page-header__actions > :not(.phone-hide))) { display: none; }

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

/* An Inbox message is the whole point of its row: two lines before it
   ellipsises, not one. */
.inbox-row .list-row__title {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  white-space: normal;
}

.home__focus { margin-top: var(--space-4); }
.home__focus .card { padding: var(--space-3) var(--space-4); }
.home__empty { margin-bottom: var(--space-5); }

/* ---------- Jump anywhere (design-system session D) ---------- */

/* A search field that IS an input (the Search page, the dialog): the label
   draws the box, the input inside it draws nothing of its own. */
.search-input {
  width: 100%;
  cursor: text;
}

.search-input:focus-within {
  border-color: var(--accent);
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.search-input input {
  flex: 1 1 auto;
  min-width: 0;
  min-height: calc(var(--tap-min) - 2px);
  padding: 0;
  border: 0;
  background: none;
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: var(--type-input-size);
  line-height: var(--type-input-line);
  outline: none;
  box-shadow: none;
}

.search-input input::-webkit-search-cancel-button { display: none; }

.search-page__form { margin-bottom: var(--space-5); }

.jump-results {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-5);
}

.jump-results__none {
  margin: 0;
  padding: var(--space-4);
  font-size: var(--type-body-sm-size);
  color: var(--ink-muted);
}

.jump-group .section-head { margin-bottom: var(--space-2); }

.jump-row--on,
a.jump-row--on:hover { background: var(--accent-tint); }

/* The dialog: 640px, radius-lg, over the overlay, the sheet's shadow. */
.jump {
  width: min(640px, calc(100vw - 2 * var(--space-4)));
  max-height: min(70vh, 40rem);
  margin: 12vh auto auto;
  padding: var(--space-3);
  border: 0;
  border-radius: var(--radius-lg);
  background: var(--surface-page);
  color: var(--ink);
  box-shadow: var(--shadow-sheet);
}

.jump[open] {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.jump::backdrop { background: var(--overlay); }

.jump__form { flex: none; margin: 0; }

.jump__results {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}


/* ---------- The project's lines: side panel, bulk bar, column views (design-system session E) ---------- */

.line-toolbar__count {
  font-size: var(--type-meta-size);
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Column views in a table toolbar: the segmented control at the compact 36px,
   44px on a phone; scrolls inside itself rather than pushing the page wide. */
.segmented--compact {
  display: inline-flex;
  margin: 0;
  max-width: 100%;
  overflow-x: auto;
}

.segmented--compact .segmented__item {
  flex: none;
  min-height: calc(var(--button-compact) - 4px);
}

@media (max-width: 640px) {
  .segmented--compact .segmented__item { min-height: calc(var(--tap-min) - 4px); }
}

.icon-btn {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--tap-min);
  height: var(--tap-min);
  border: 0;
  border-radius: var(--radius-pill);
  background: none;
  color: var(--ink-muted);
  cursor: pointer;
}

.icon-btn:hover { background: var(--surface-sunken); color: var(--ink); }

.fact-tiles {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-2);
}

.fact-tile {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-3);
  border-radius: var(--radius-md);
  background: var(--surface-sunken);
  min-width: 0;
}

.fact-tile__label {
  font-size: var(--type-caption-size);
  font-weight: var(--type-caption-weight);
  color: var(--ink-muted);
}

.fact-tile__value {
  font-size: var(--type-number-size);
  line-height: var(--type-number-line);
  font-weight: var(--type-number-weight);
  font-variant-numeric: tabular-nums;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.fact-tile__unit {
  font-size: var(--type-meta-size);
  font-weight: 400;
  color: var(--ink-muted);
}

.qty-bar { display: grid; gap: var(--space-1); }
.qty-bar + .qty-bar { margin-top: var(--space-3); }

.qty-bar__text {
  display: flex;
  justify-content: space-between;
  gap: var(--space-2);
  font-size: var(--type-body-sm-size);
}

.qty-bar__figure {
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
}

.qty-bar__track {
  height: var(--space-2);
  border-radius: var(--radius-pill);
  background: var(--surface-sunken);
  overflow: hidden;
}

.qty-bar__fill {
  height: 100%;
  border-radius: var(--radius-pill);
  background: var(--accent-fill);
}

.line-panel {
  display: grid;
  /* One column that never grows past the panel: a list row's one-line,
     ellipsised title (the tender quotes, ADR-0264) would otherwise size the
     column to its full text and push the panel off the screen. */
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-5);
  padding: var(--space-4);
  border-radius: var(--radius-md);
  background: var(--surface-card);
}

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

.line-panel__id { flex: 1 1 auto; min-width: 0; }

.line-panel__code {
  margin: 0;
  font-size: var(--type-meta-size);
  font-weight: 600;
  color: var(--ink-muted);
}

.line-panel__title {
  margin: var(--space-1) 0 0;
  font-size: var(--type-heading-size);
  line-height: var(--type-heading-line);
  font-weight: var(--type-heading-weight);
}

.line-panel__meta {
  margin: var(--space-1) 0 0;
  font-size: var(--type-meta-size);
  color: var(--ink-muted);
}

.line-panel__status {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  margin: var(--space-2) 0 0;
}

.line-panel__links { border: 1px solid var(--divider); }

/* The tender quotes in a row's Detail drawer (ADR-0264), which is how a phone
   reads them. The drawer cell spans the whole table, wider than a phone's
   screen, so the list keeps to the side panel's width and sticks to the left
   edge of the scrolled table — it stays in view wherever the Detail link was. */
.detail-row .tender-quotes {
  position: sticky;
  left: 0;
  max-width: min(360px, 100%);
  margin-top: var(--space-4);
}
.line-panel__links .list-row { min-height: var(--tap-min); padding-block: var(--space-2); }

@media (min-width: 1024px) {
  tr[data-line-panel] { cursor: pointer; }

  tr.line-row--selected > td,
  tr.line-row--selected > td.is-frozen { background: var(--accent-tint); }

  .lines-layout--open {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 360px;
    gap: var(--space-4);
    align-items: start;
  }

  .lines-layout__panel {
    position: sticky;
    top: var(--space-4);
    max-height: calc(100vh - 2 * var(--space-4));
    overflow-y: auto;
  }

  /* The bulk bar floats on a laptop (README, Bulk action bar): centred in
     the page beside the sidebar, up to 760px, the sheet's shadow. */
  .sel-tray {
    left: calc(var(--frame-left) + (100vw - var(--frame-left)) / 2);
    right: auto;
    bottom: var(--space-4);
    width: min(760px, calc(100vw - var(--frame-left) - 2 * var(--space-6)));
    transform: translateX(-50%);
    padding: var(--space-3) var(--space-4);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sheet);
    gap: var(--space-4);
  }

  .sel-tray__spacer { height: 100px; }

  /* At 760px the wide tray's "All N selected · client …" pair and the Create
     PO button's own count and value wrapped every control onto two to four
     lines. The floating bar states the two figures on one line, as the
     phone's tray does, and keeps every button on one line. */
  .sel-tray__facts {
    flex: 1 1 auto;
    min-width: 0;
    flex-direction: column;
    align-items: flex-start;
    flex-wrap: nowrap;
    gap: 2px;
  }

  .sel-tray__count { font-size: var(--type-body-sm-size); }

  .sel-tray__note {
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .sel-tray__figures { display: none; }

  .sel-tray__sum {
    display: block;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: var(--type-meta-size);
    font-variant-numeric: tabular-nums;
    color: var(--control-border);
  }

  .sel-tray__sum b { color: var(--surface-card); }

  .sel-tray__actions { flex: none; }

  .sel-tray__actions .btn {
    white-space: nowrap;
    padding-inline: var(--space-3);
    font-size: var(--type-body-sm-size);
  }
}

/* ---------- The approve sheet (design-system session F; README, Bottom sheet) ---------- */

.sheet {
  position: fixed;
  inset: auto 0 0 0;
  width: 100%;
  max-width: none;
  max-height: 80vh;
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  background: var(--surface-card);
  color: var(--ink);
  box-shadow: var(--shadow-sheet);
}

.sheet[open] { display: flex; flex-direction: column; }
.sheet::backdrop { background: var(--overlay); }

.sheet__body {
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow-y: auto;
}

.sheet__content {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding: var(--space-2) var(--space-4) 0;
}

.sheet__grab {
  width: var(--space-8);
  height: var(--space-1);
  margin: 0 auto;
  border-radius: var(--radius-pill);
  background: var(--control-border);
}

.sheet__head { display: flex; align-items: flex-start; gap: var(--space-2); }
.sheet__id { flex: 1 1 auto; min-width: 0; }

.sheet__kind,
.sheet__meta {
  margin: 0;
  font-size: var(--type-meta-size);
  color: var(--ink-muted);
}

.sheet__title {
  margin: var(--space-1) 0;
  font-size: var(--type-title-size);
  line-height: var(--type-title-line);
  font-weight: var(--type-title-weight);
  overflow-wrap: anywhere;
}

.fact-tile__value--text {
  font-size: var(--type-body-size);
  font-weight: 500;
}

.sheet__links { border: 1px solid var(--divider); }
.sheet__links .list-row { min-height: var(--tap-min); padding-block: var(--space-2); }
.sheet__full { align-self: flex-start; padding-inline: 0; }

/* The two answers, pinned at the foot, above the home indicator. */
.sheet__actions {
  position: sticky;
  bottom: 0;
  display: grid;
  gap: var(--space-2);
  margin-inline: calc(var(--space-4) * -1);
  padding: var(--space-3) var(--space-4) calc(var(--space-3) + env(safe-area-inset-bottom));
  border-top: 1px solid var(--divider);
  background: var(--surface-card);
}

.sheet__actions form { display: grid; gap: var(--space-2); margin: 0; }
.sheet__row { display: flex; gap: var(--space-2); }

.btn.sheet__btn {
  flex: 1 1 0;
  min-height: calc(var(--button) + var(--space-2));
}

.sheet__confirm,
.sheet__note {
  margin: 0;
  font-size: var(--type-body-sm-size);
  color: var(--ink);
}

.sheet__send-back textarea { width: 100%; }

/* A form in the sheet (P9: New vendor from the comparison). On a phone it is
   the bottom sheet as drawn above; on a laptop the same sheet stands in the
   middle of the page at a form's width, every corner rounded, so a form does
   not stretch across a 1440px screen. The ✕ is a full tap target. */
.sheet__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--tap-min);
  min-height: var(--tap-min);
  text-decoration: none;
}

.sheet--form .sheet__content { padding-bottom: 0; }

/* Save, Create anyway and Cancel on one row where they fit, whole buttons on
   two rows where they don't (a phone with all three) — never a label broken
   over two lines inside its button. */
.sheet--form .sheet__row { flex-wrap: wrap; }
.sheet--form .sheet__row > .btn { flex: 1 1 auto; white-space: nowrap; }

@media (min-width: 641px) {
  .sheet--form {
    inset: 0;
    width: min(40rem, calc(100% - var(--space-8)));
    max-height: 90vh;
    margin: auto;
    border-radius: var(--radius-lg);
  }
}

/* ---------- Toasts (design-system session F; README, Feedback) ---------- */

.toast-stack {
  position: fixed;
  left: calc(var(--frame-left) + var(--space-6));
  bottom: var(--space-6);
  z-index: 60;
  display: grid;
  gap: var(--space-2);
  width: min(28rem, calc(100vw - var(--frame-left) - 2 * var(--space-6)));
}

.toast {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-min);
  padding: var(--space-1) var(--space-1) var(--space-1) var(--space-4);
  border-radius: var(--radius-md);
  background: var(--ink);
  color: var(--surface-card);
  box-shadow: var(--shadow-sheet);
  font-size: var(--type-body-sm-size);
  transition: opacity 0.2s ease;
}

/* An error or a warning keeps its banner colours, which hold in both themes. */
.toast--error { background: var(--danger-tint); color: var(--danger); }
.toast--warning { background: var(--warning-tint); color: var(--warning); }
.toast--leaving { opacity: 0; }

.toast__text { flex: 1 1 auto; min-width: 0; }

.toast__close {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--tap-min);
  height: var(--tap-min);
  padding: 0;
  border: 0;
  border-radius: var(--radius-pill);
  background: none;
  color: inherit;
  cursor: pointer;
}

@media (max-width: 640px) {
  .toast-stack {
    left: var(--space-4);
    right: var(--space-4);
    bottom: calc(var(--tab-bar-h) + var(--space-2));
    width: auto;
  }
}

/* ---------- Page to page (design-system session F) ----------
   A cross-document view transition, only for a reader who has not asked for
   less motion: the page crossfades while the top bar, the tab bar and the
   sidebar hold still. */
@media (prefers-reduced-motion: no-preference) {
  @view-transition { navigation: auto; }

  .phone-bar { view-transition-name: phone-bar; }
  .tab-bar { view-transition-name: tab-bar; }
  .side-nav { view-transition-name: side-nav; }

  ::view-transition-old(phone-bar),
  ::view-transition-new(phone-bar),
  ::view-transition-old(tab-bar),
  ::view-transition-new(tab-bar),
  ::view-transition-old(side-nav),
  ::view-transition-new(side-nav) {
    animation: none;
  }
}

/* ---------- Phone lists (design-system session F; README, List row) ----------
   The projects, POs and tenders lists draw two-line rows on a phone and keep
   their tables on a laptop; the template carries both. */
.phone-list { display: none; }

.list-row__title--record { font-weight: 600; }
.list-row__amount--date { font-weight: 400; font-size: var(--type-meta-size); color: var(--ink-muted); }

@media (max-width: 640px) {
  .phone-list { display: block; }
  /* A card left holding only the pagination under the phone list. */
  .tender-list-card:not(:has(.empty-state)) { background: none; padding: 0; }
}

/* A block that only a phone shows (the counterpart of .phone-only, which is
   inline). And on a phone a wizard's own title and crumbs go: the top bar
   carries the title (design-system session F — "no duplicate title card"). */
.phone-only-block { display: none; }

@media (max-width: 640px) {
  .phone-only-block { display: block; }
  .wiz-head__title,
  .wiz-head__crumbs { display: none; }
}

@media (min-width: 641px) {
  .side-nav__empty {
    margin: 0;
    padding: var(--space-1) var(--space-3);
    font-size: var(--type-meta-size);
    color: var(--ink-muted);
  }
}

/* ---------- Session G fixes ---------- */

/* A header or top-bar action is one line at every width ("+ Invoice" wrapped
   onto two in the laptop page header). */
.page-header__actions .btn,
.phone-bar__actions .btn {
  white-space: nowrap;
  flex: none;
}

/* A queue's name on "Needs you" is never cut: it wraps to a second line
   rather than ellipsising ("Comparisons with incomplet…"). */
.list-row--count .list-row__title {
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
}

/* The vendor invoice entry on a phone: one line of hint per section, the rest
   on the laptop (design-system session G). */
@media (max-width: 640px) {
  .entry-compact .form-group__hint,
  .entry-compact .card-subtitle,
  .entry-compact .arith__stance,
  .entry-compact .arith__against {
    display: -webkit-box;
    -webkit-line-clamp: 1;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }

  .entry-compact .field__hint,
  .entry-compact .field-hint { display: none; }

  .entry-compact .card-subtitle.phone-hide { display: none; }
}

/* The project page on a phone (design-system session G): the money figures
   as a compact 2×2 of number cards, "Choose figures" (a laptop setting) out
   of the way, so the first line of the BOQ is on the first screen. */
@media (max-width: 640px) {
  .proj-head__figures {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-2);
  }

  .proj-figure {
    min-width: 0;
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-md);
    background: var(--surface-sunken);
  }

  .proj-figure__label {
    font-size: var(--type-caption-size);
    line-height: var(--type-caption-line);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .proj-figure__value {
    font-size: var(--type-heading-size);
    line-height: var(--type-heading-line);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .proj-figure__note,
  .proj-figures__disclosure { display: none; }

  /* Ordered's "of ₹x · y%" is the card's note on a phone (README, Number
     card), so its value fits instead of being cut off. */
  .proj-figure__of {
    display: block;
    font-size: var(--type-caption-size);
    line-height: var(--type-caption-line);
    font-weight: 400;
    color: var(--ink-muted);
    overflow: hidden;
    text-overflow: ellipsis;
  }
}

/* .phone-hide always wins at 640px and under: a component's own display
   (a segmented control, a text button) must not bring back what a template
   marked laptop-only (design-system session G). */
@media (max-width: 640px) {
  .phone-hide { display: none !important; }
}

/* ---------- The tender page (ADR-0205; README "Page chip", "Timeline",
   "Read bar") ----------
   The tender's Overview tab: four key-figure number cards, then two columns
   on a laptop (the read and what the tender asks on the left, two thirds;
   its dates, quantum, what was read and forms on the right) and one column on
   a phone, where the red flags come before the qualifying list as the phone
   design has them. */


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

.number-card__note {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1) var(--space-2);
  font-size: var(--type-caption-size);
  line-height: var(--type-caption-line);
  color: var(--ink-muted);
}

.number-card__value--muted { color: var(--ink-muted); }

.ov-grid {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
  gap: var(--space-6);
  align-items: start;
}

.ov-col {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
  min-width: 0;
}

/* The columns' cards are spaced by the column's gap, not by .card + .card. */
.ov-col > .card + .card { margin-top: 0; }

.ov-card { display: flex; flex-direction: column; gap: var(--space-4); }

/* The card's gap spaces its paragraphs; their own margins would double it. */
.ov-card > p,
.ov-card .app-message p { margin: 0; }

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

.ov-card__head .section-title,
.ov-card__head .section-label { margin: 0; }

.ov-card__meta {
  font-size: var(--type-meta-size);
  line-height: var(--type-meta-line);
  color: var(--ink-muted);
}

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

.ov-history { margin-top: var(--space-6); }

/* A History change is a sentence — "file: BOQ as received → Corrigendum" —
   and is read in full: it wraps instead of being cut at the cell's edge. */
.ov-history table.app-table tbody td.ov-history__change {
  min-width: 16rem;
  max-width: 32rem;
  white-space: normal;
  overflow-wrap: anywhere;
}

/* On a phone History is a list, as the Documents tab is (ADR-0229): the change
   in full across the row, then when, who and the note on a meta line — nothing
   behind a sideways scroll. */
@media (max-width: 640px) {
  table.ov-history__table,
  table.ov-history__table tbody { display: block; }
  table.ov-history__table thead { display: none; }
  table.ov-history__table tr.ov-history__row {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1) var(--space-2);
    padding: var(--space-3) var(--space-4);
    border-top: 1px solid var(--divider);
  }
  table.app-table.ov-history__table tbody tr.ov-history__row > td {
    display: block;
    height: auto;
    max-width: none;
    min-width: 0;
    padding: 0;
    border: 0;
    background: none;
    white-space: normal;
  }
  table.app-table.ov-history__table tbody td.ov-history__change {
    order: -1;
    flex: 1 1 100%;
    font-size: var(--type-body-size);
  }
  table.app-table.ov-history__table tbody td.ov-history__when,
  table.app-table.ov-history__table tbody td.ov-history__who,
  table.app-table.ov-history__table tbody td.ov-history__note {
    font-size: var(--type-meta-size);
    color: var(--ink-muted);
  }
}

/* Page chip: "p.5", the page a fact came from, opening that one page. Drawn
   20px tall, it answers taps across the 44px tap-min. */
.page-chip {
  position: relative;
  display: inline-flex;
  align-items: center;
  padding: 2px var(--space-1);
  border-radius: var(--radius-sm);
  background: var(--accent-tint);
  color: var(--accent);
  font-size: var(--type-caption-size);
  line-height: var(--type-caption-line);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.page-chip::after {
  content: "";
  position: absolute;
  inset: calc(-1 * var(--space-3)) -2px;
}

.page-chip:hover { text-decoration: none; }

.page-chip--text {
  background: var(--surface-sunken);
  color: var(--ink-muted);
}

.page-chip--text::after { content: none; }

.page-chips { display: inline-flex; flex-wrap: wrap; gap: var(--space-1); }

/* The read: the lean pill beside its headline, then three numbered reasons. */
.lean {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
}

.lean__headline {
  margin: 0;
  font-size: var(--type-heading-size);
  line-height: var(--type-heading-line);
  font-weight: var(--type-heading-weight);
}

.ov-list {
  margin: 0;
  padding: 0;
  list-style: none;
  border-top: 1px solid var(--divider);
}

.ov-list > li {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  gap: var(--space-3);
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--divider);
  font-size: var(--type-body-sm-size);
  line-height: var(--type-body-sm-line);
}

.ov-list--numbered > li { grid-template-columns: var(--space-5) minmax(0, 1fr) auto; }

.ov-list__num { font-weight: 600; color: var(--ink-muted); }

/* A red flag (ADR-0237): what it says, then every page it was found on. */
.ov-list--flags > li { grid-template-columns: minmax(0, 1fr) auto; }

/* Several page chips side by side — a clause found on more than one page. */
.page-chips {
  display: inline-flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--space-1);
}

/* The red flags in their four groups, on the Overview and on the full list. */
.flag-group__title {
  margin: 0 0 var(--space-2);
  font-size: var(--type-body-sm-size);
  line-height: var(--type-body-sm-line);
  font-weight: 600;
}

.flag-group--overview + .flag-group--overview { margin-top: var(--space-4); }

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

/* The full list: search, group and file in one row on a laptop. */
.flag-filters {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--space-3);
  margin-bottom: var(--space-5);
}

.flag-filters__search { width: 100%; }

.flag-filters select { min-height: var(--tap-min); }

.flag-list {
  display: grid;
  gap: var(--space-5);
}

.flag-list__shown {
  margin: 0;
  font-size: var(--type-meta-size);
  line-height: var(--type-meta-line);
  color: var(--ink-muted);
}

.flag-rows {
  margin: 0;
  padding: 0;
  list-style: none;
  border-top: 1px solid var(--divider);
}

/* One flag: the desk's tick, what it says (and who checked it), its pages. */
.flag-row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: start;
  gap: var(--space-3);
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--divider);
  font-size: var(--type-body-sm-size);
  line-height: var(--type-body-sm-line);
}

.flag-row:last-child { border-bottom: 0; }

.flag-row__tick { margin: 0; }

.flag-row__box {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: var(--tap-min);
  min-height: var(--tap-min);
  margin: calc(var(--space-3) * -1) 0;
  cursor: pointer;
}

.flag-row__box input { width: var(--space-5); height: var(--space-5); accent-color: var(--accent-fill); }

.flag-row__body { display: grid; gap: 2px; min-width: 0; padding-top: var(--space-1); }

.flag-row__text { overflow-wrap: anywhere; }

.flag-row--checked .flag-row__text { color: var(--ink-muted); }

.flag-row__meta {
  font-size: var(--type-meta-size);
  line-height: var(--type-meta-line);
  color: var(--ink-muted);
}

.flag-row .page-chips { padding-top: var(--space-1); }

.ov-list__quiet { color: var(--ink-muted); }

.ov-list__label { font-weight: 500; }

/* The rest of a long list on a phone, behind "See all". */
.ov-list__more { display: none; }

/* Every requirement in full: the table rule's one-line ellipsis would cut
   exactly the words a bidder has to meet. */
table.app-table.ov-qualify-table tbody td {
  vertical-align: top;
  white-space: normal;
  max-width: none;
  overflow: visible;
  overflow-wrap: anywhere;
}

/* MESPL's column holds a pill, a line or two and, for the desk, a tick
   (ADR-0209): it wraps between words, never inside one, and keeps room for
   the line. */
table.app-table.ov-qualify-table tbody td.ov-qualify-table__ours {
  min-width: calc(var(--space-8) * 8);
  overflow-wrap: normal;
}

/* A requirement's short name wraps between words, never inside one. */
table.app-table.ov-qualify-table tbody td.ov-list__label { overflow-wrap: normal; }

.ov-placeholder { color: var(--ink-faint); }

.ov-scope {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-2);
}

.ov-scope .fact-tile__value--text,
.ov-scope__items {
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: var(--type-body-sm-size);
  line-height: var(--type-body-sm-line);
}

.ov-scope__items li + li { margin-top: var(--space-1); }

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

.ov-sites__part {
  font-size: var(--type-meta-size);
  font-weight: 600;
}

/* Timeline: the tender's dates, earliest first. One grid for the whole list,
   so the date column lines up: date, dot, what happens, its page. */
.timeline {
  display: grid;
  grid-template-columns: max-content var(--space-2) minmax(0, 1fr) auto;
  align-items: start;
  gap: var(--space-3);
  margin: 0;
  padding: 0;
  list-style: none;
}

.timeline > li { display: contents; }

.timeline__day {
  font-size: var(--type-meta-size);
  line-height: var(--type-body-sm-line);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.timeline__dot {
  width: var(--space-2);
  height: var(--space-2);
  margin-top: calc((var(--type-body-sm-line) - var(--space-2)) / 2);
  border-radius: var(--radius-pill);
  background: var(--control-border);
}

.timeline__dot--fine { background: var(--accent-fill); }
.timeline__dot--watch,
.timeline__dot--changed { background: var(--warning); }
.timeline__dot--overdue { background: var(--danger); }

.timeline__what {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1) var(--space-2);
  font-size: var(--type-body-sm-size);
  line-height: var(--type-body-sm-line);
}

.timeline__what--strong { font-weight: 600; }

.ov-dates-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  padding-top: var(--space-3);
  border-top: 1px solid var(--divider);
  font-size: var(--type-meta-size);
  color: var(--ink-muted);
}

/* Read bar: one document's pages, read in full / pricing / skipped, each
   segment growing by its page count (set inline from the data). */
.read-doc { display: flex; flex-direction: column; gap: var(--space-2); }

.read-doc__head {
  display: flex;
  justify-content: space-between;
  gap: var(--space-2);
  font-size: var(--type-body-sm-size);
}

.read-doc__name { font-weight: 500; min-width: 0; overflow-wrap: anywhere; }

.read-doc__pages { color: var(--ink-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }

.read-bar {
  display: flex;
  gap: 2px;
  height: var(--space-2);
  border-radius: var(--radius-pill);
  overflow: hidden;
  background: var(--surface-sunken);
}

.read-bar__seg--decision { background: var(--accent-fill); }
.read-bar__seg--pricing { background: var(--ink-faint); }
.read-bar__seg--skip { background: var(--border); }

.read-legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-4);
  font-size: var(--type-caption-size);
  line-height: var(--type-caption-line);
  color: var(--ink-muted);
}

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

.read-legend__swatch {
  width: var(--space-2);
  height: var(--space-2);
  border-radius: 2px;
}

/* ---------- Scope tags and share bars (ADR-0216; README "Scope tag",
   "Share bar") ----------
   What a group of BOQ rows asks MESPL to do, and how much of a whole each
   part is. A scope tag is NOT a status: only "Supply + install", the whole
   job, takes the accent tint; the three narrower answers are neutral. None is
   ever orange — orange means warning in this app.
   Share bars come in two forms. One row's bar: a thin track with an accent
   fill and its % beside it. A stacked bar: up to eight segments, the biggest
   share first in --share-1 and fainter shades after, then "Everything else" in
   neutral grey, with a legend underneath. Each segment's flex-grow is its
   share, set inline from the data, as the read bar's are. */

.pill-supply-install { background: var(--accent-tint); color: var(--accent); }

.pill-install-only,
.pill-supply-only { background: var(--surface-sunken); color: var(--ink-muted); }

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

.share-bar__track {
  flex: 1 1 0;
  min-width: 0;
  height: var(--space-2);
  border-radius: var(--radius-pill);
  background: var(--surface-sunken);
  overflow: hidden;
}

.share-bar__fill {
  display: block;
  height: 100%;
  min-width: 2px;
  border-radius: var(--radius-pill);
  background: var(--share-1);
}

.share-bar__pct {
  flex: 0 0 auto;
  min-width: calc(var(--space-8) + var(--space-4));
  text-align: right;
  font-size: var(--type-meta-size);
  line-height: var(--type-meta-line);
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
}

.share-stack {
  display: flex;
  gap: 2px;
  height: var(--space-3);
  border-radius: var(--radius-pill);
  overflow: hidden;
  background: var(--surface-sunken);
}

.share-stack--thin { height: var(--space-2); }

.share-fill--1 { background: var(--share-1); }
.share-fill--2 { background: var(--share-2); }
.share-fill--3 { background: var(--share-3); }
.share-fill--4 { background: var(--share-4); }
.share-fill--5 { background: var(--share-5); }
.share-fill--6 { background: var(--share-6); }
.share-fill--7 { background: var(--share-7); }
.share-fill--rest { background: var(--share-rest); }

.share-legend {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-2) var(--space-4);
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: var(--type-meta-size);
  line-height: var(--type-meta-line);
}

.share-legend--inline {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-5);
}

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

.share-legend__swatch {
  flex: 0 0 auto;
  width: var(--space-3);
  height: var(--space-3);
  border-radius: calc(var(--radius-sm) / 2);
}

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

.share-legend__pct {
  margin-left: auto;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.share-legend--inline .share-legend__pct { margin-left: 0; font-weight: 400; }

@media (min-width: 641px) {
  .share-legend { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

/* ---------- The tender's Quantum tab (ADR-0217) ----------
   The part switch and a summary line, up to four number cards, then the
   Overview's two columns (.ov-grid): what the job is made of and the quantum
   rows on the left, the install-only, review and enquiry cards on the right.
   A quantum row is one link: a grid on a laptop whose columns line up from
   row to row; on a phone two lines — name and quantity, then the scope tag
   and the share bar. */

.qt-page {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
  margin-top: var(--space-6);
}

.qt-page > .card + .card,
.qt-page .ov-col > .card + .card { margin-top: 0; }

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

.qt-parts {
  display: inline-flex;
  max-width: 100%;
  margin: 0;
  overflow-x: auto;
}

.qt-parts .segmented__item { flex: 0 0 auto; }

.qt-summary,
.qt-meta {
  margin: 0;
  font-size: var(--type-meta-size);
  line-height: var(--type-meta-line);
  color: var(--ink-muted);
}

.qt-tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); }

.qt-headline {
  font-size: var(--type-heading-size);
  line-height: var(--type-heading-line);
  font-weight: 600;
}

.qt-scope {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding-top: var(--space-3);
  border-top: 1px solid var(--divider);
}

.qt-card-tools {
  display: inline-flex;
  align-items: center;
  gap: var(--space-4);
  font-size: var(--type-meta-size);
}

.qt-list {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  overflow: hidden;
}

.qt-row {
  display: grid;
  grid-template-columns:
    minmax(0, 2fr) minmax(0, 1.6fr) calc(var(--space-8) * 4) minmax(0, 2fr)
    var(--space-4);
  align-items: center;
  gap: var(--space-3);
  min-height: var(--table-row);
  padding: var(--space-2) var(--space-3);
  border-top: 1px solid var(--divider);
  color: var(--ink);
  font-size: var(--type-body-sm-size);
  line-height: var(--type-body-sm-line);
}

.qt-list--no-share .qt-row {
  grid-template-columns: minmax(0, 2fr) minmax(0, 1.6fr) calc(var(--space-8) * 4) var(--space-4);
}

a.qt-row:hover { text-decoration: none; background: var(--surface-sunken); }

.qt-row--head {
  min-height: calc(var(--table-row) - var(--space-2));
  border-top: none;
  background: var(--surface-sunken);
  font-size: var(--type-caption-size);
  line-height: var(--type-caption-line);
  font-weight: 600;
  color: var(--ink-muted);
}

.qt-row__name { min-width: 0; font-weight: 600; overflow-wrap: anywhere; }

.qt-row__qty {
  min-width: 0;
  text-align: right;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}

.qt-row--head .qt-row__qty { font-weight: 600; }

.qt-qty__main { display: block; }

.qt-qty__more {
  display: block;
  font-size: var(--type-caption-size);
  line-height: var(--type-caption-line);
  font-weight: 400;
  color: var(--ink-muted);
}

/* On a laptop the tag and the bar are grid columns of the row itself. */
.qt-row__detail { display: contents; }

.qt-row__tag { min-width: 0; }

.qt-row__chevron { width: var(--space-4); height: var(--space-4); color: var(--ink-faint); }

/* Each quantum row sits in an item, which holds its tick box for the tender
   desk (ADR-0220) — a tick box cannot sit inside the row's link. */
.qt-item {
  display: flex;
  align-items: stretch;
  border-top: 1px solid var(--divider);
}

.qt-item--head,
.qt-item--sub { border-top: none; }

.qt-item > .qt-row {
  flex: 1 1 auto;
  min-width: 0;
  border-top: none;
}

.qt-tick {
  display: flex;
  flex: 0 0 var(--tap-min);
  align-items: center;
  justify-content: center;
  min-height: var(--tap-min);
  cursor: pointer;
}

.qt-item--head .qt-tick { background: var(--surface-sunken); cursor: default; }

/* The tick bar clears the last rows it would cover. */
.qt-list--pick { margin-bottom: var(--space-2); }

/* A kind's sub-row under its category (ADR-0219): indented, quieter, no tag or bar. */
.qt-row--sub {
  min-height: calc(var(--table-row) - var(--space-3));
  padding-left: calc(var(--space-3) + var(--space-5));
  border-top: none;
  color: var(--ink-muted);
}

.qt-row--sub .qt-row__name,
.qt-row--sub .qt-row__qty { font-weight: 400; }

.qt-row--all {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  color: var(--accent);
  font-weight: 600;
}

.qt-kv {
  display: flex;
  flex-direction: column;
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: var(--type-body-sm-size);
  line-height: var(--type-body-sm-line);
}

.qt-kv > li {
  display: flex;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-2) 0;
  border-top: 1px solid var(--divider);
}

.qt-kv__value {
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  text-align: right;
  overflow-wrap: anywhere;
}

.qt-review-button { align-self: flex-start; }

/* The Overview's quantum card: the same two-line rows, in a narrow column. */
.ov-quantum > li { flex-wrap: wrap; row-gap: var(--space-1); }
.ov-quantum > li:first-child { border-top: none; padding-top: 0; }
.ov-quantum__name { min-width: 0; font-weight: 500; overflow-wrap: anywhere; }
.ov-quantum .qt-row__detail {
  display: flex;
  flex: 1 0 100%;
  align-items: center;
  gap: var(--space-2);
}
.ov-quantum .qt-row__share { flex: 1 1 auto; }

/* The phone's own "Show all items", where a laptop already shows every row. */
.qt-row--all.phone-only-block { display: none; }

@media (min-width: 641px) {
  .qt-tiles {
    grid-auto-flow: column;
    grid-auto-columns: minmax(0, 1fr);
    grid-template-columns: none;
  }
}

@media (max-width: 640px) {
  .qt-page { gap: var(--space-5); margin-top: var(--space-4); }
  .qt-switch-bar { display: block; }
  .qt-parts { display: flex; width: 100%; }
  .qt-parts .segmented__item { flex: 1 1 0; }
  .qt-headline { font-size: var(--type-body-size); line-height: var(--type-body-line); }
  .qt-list { border: none; border-radius: 0; }
  .qt-row {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1) var(--space-2);
    padding: var(--space-3) 0;
    font-size: var(--type-body-size);
    line-height: var(--type-body-line);
  }
  .qt-row:first-of-type,
  .qt-row--head + .qt-row,
  .qt-item--head + .qt-item { border-top: none; }
  .qt-row--head,
  .qt-row--phone-more,
  .qt-row__chevron { display: none; }
  .qt-row__name { flex: 1 1 auto; }
  .qt-row__qty { flex: 0 1 auto; max-width: 60%; }
  .qt-row__detail {
    display: flex;
    flex: 1 0 100%;
    align-items: center;
    gap: var(--space-2);
  }
  .qt-row__share { flex: 1 1 auto; }
  .qt-row--all { padding: var(--space-3) 0; }
  .qt-row--all.phone-only-block { display: flex; }
  .qt-row--sub {
    flex-wrap: nowrap;
    align-items: baseline;
    padding: var(--space-1) 0 var(--space-1) var(--space-5);
    border-top: none;
    font-size: var(--type-body-sm-size);
    line-height: var(--type-body-sm-line);
  }
  .qt-row--sub .qt-row__name { flex: 1 1 auto; min-width: 0; }
  .qt-row--sub .qt-row__qty { flex: 0 0 auto; max-width: 50%; }
}

.ov-cost {
  display: flex;
  justify-content: space-between;
  padding-top: var(--space-3);
  border-top: 1px solid var(--divider);
  font-size: var(--type-meta-size);
  color: var(--ink-muted);
}

.ov-cost__value { color: var(--ink); font-weight: 500; font-variant-numeric: tabular-nums; }

/* A card of labelled facts, one row each: label, value, page chip. A term the
   read did not find is one quiet line, on a phone too. */
.ov-facts {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin: 0;
  font-size: var(--type-body-sm-size);
  line-height: var(--type-body-sm-line);
}

.ov-fact {
  display: grid;
  grid-template-columns: calc(var(--space-8) * 5) minmax(0, 1fr) auto;
  gap: var(--space-3);
}

.ov-fact dt { color: var(--ink-muted); }
.ov-fact dd { margin: 0; overflow-wrap: anywhere; }
.ov-fact--quiet dd { color: var(--ink-muted); }

/* The tender page's pills, each named for its value. The lean: */
.pill-worth-bidding { background: var(--success-tint); color: var(--success); }
.pill-look-closely { background: var(--warning-tint); color: var(--warning); }
.pill-likely-not-for-us { background: var(--danger-tint); color: var(--danger); }
/* A red flag's severity, worst first (missing and rejection block a bid,
   payment is worth a look, cost, deadline and other are labels): */
.pill-missing,
.pill-rejection { background: var(--danger-tint); color: var(--danger); }
.pill-payment { background: var(--warning-tint); color: var(--warning); }
.pill-cost,
.pill-deadline { background: var(--surface-sunken); color: var(--ink-muted); }
/* A date a corrigendum changed; a BOQ with no categories yet; forms left: */
.pill-changed,
.pill-not-categorised,
.pill-left { background: var(--warning-tint); color: var(--warning); }
.pill-all-done { background: var(--success-tint); color: var(--success); }
.pill-no-corrigendum { background: var(--surface-sunken); color: var(--ink-muted); }

@media (max-width: 1023px) {
  .ov-grid { grid-template-columns: minmax(0, 1fr); gap: var(--space-5); }
  .ov-col { gap: var(--space-5); }
}

@media (max-width: 640px) {
  .ov-history { margin-top: var(--space-5); }
  .ov-order-read { order: 1; }
  .ov-order-flags { order: 2; }
  .ov-order-qualify { order: 3; }
  .ov-order-rest { order: 4; }
  .lean { flex-direction: column; gap: var(--space-2); }
  .ov-list--flags > li { grid-template-columns: minmax(0, 1fr); gap: var(--space-1); }
  .ov-list--flags .page-chips { justify-content: flex-start; }
  .flag-filters { grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); }
  .flag-filters__search { grid-column: 1 / -1; }
  .flag-filters { gap: var(--space-2); margin-bottom: var(--space-3); }
  .flag-head { display: none; }
  .flag-filters select { font-size: var(--type-input-size); }
  .flag-row { grid-template-columns: auto minmax(0, 1fr); }
  .flag-row .page-chips { grid-column: 2; justify-content: flex-start; padding-top: 0; }
  .ov-list__more { display: block; }
  .ov-list > li.ov-list__extra { display: none; }
  .ov-list--open > li.ov-list__extra { display: grid; }
  .ov-scope { grid-template-columns: minmax(0, 1fr); }
  .ov-fact { grid-template-columns: minmax(0, 1fr) auto; gap: 0 var(--space-3); }
  .ov-fact dt { grid-column: 1 / -1; }
  .ov-fact--quiet { grid-template-columns: max-content minmax(0, 1fr); }
  .ov-fact--quiet dt { grid-column: auto; }
}

/* ---------- The tender's header (ADR-0205) ----------
   Name, one meta line, the actions in one row on a laptop (Close tender as a
   danger text button, outlined, then the one filled), the lifecycle steps
   with the wait in words, the tabs. On a phone the actions are in the top
   bar, so the header is name, meta, steps and tabs. */

.tender-head__meta {
  margin: var(--space-1) 0 0;
  font-size: var(--type-meta-size);
  line-height: var(--type-meta-line);
  color: var(--ink-muted);
}

.tender-head__chips { margin-top: var(--space-2); }

.tender-head__actions { flex: none; }

.tender-actions {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-2);
}

.tender-actions form { margin: 0; }

/* ---------- Search across a tender (S1, ADR-0251) ---------- */

/* The header's box: the README's search field, a little narrower so the row
   keeps Close tender and Edit beside it. */
.tender-actions__search .search-field { width: min(16rem, 100%); }

/* The search page: one column that never grows past the screen (a hit's
   one-line title would otherwise widen it), the box close to its results. */
.tender-search {
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-5);
}

.tender-search .search-page__form { margin-bottom: 0; }

.tender-search__field { width: 100%; }

/* A search hit (README "Search hit row", awaiting Darshan's approval): the
   list row's title and meta, then what matched, clamped to two lines, the
   matched words marked in the accent tint. */
.search-hit__snippet {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  font-size: var(--type-body-sm-size);
  line-height: var(--type-body-sm-line);
  color: var(--ink-muted);
}

.search-hit__snippet mark {
  background: var(--accent-tint);
  color: var(--ink);
  border-radius: var(--radius-sm);
}

.search-hit .list-row__line { min-width: 0; }

.search-hit .page-chip { flex: none; }

.search-hit__none {
  margin: 0;
  padding: var(--space-3) var(--space-4);
  font-size: var(--type-body-sm-size);
  color: var(--ink-muted);
}

/* The read's approved makes: the item in ink at 600, its makes after it. */
.ov-makes__names { color: var(--ink-muted); }

.tender-actions__text {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  padding-inline: var(--space-2);
  font-size: var(--type-body-sm-size);
  font-weight: 500;
}

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

/* The steps keep scrolling inside themselves (they are a sequence, never
   wrapped), so the wait words share their line instead of a line of their own. */
.tender-head__track .lc-track {
  flex: 0 1 auto;
  min-width: 0;
  flex-wrap: nowrap;
}

.tender-head__track .lc-track__steps { min-width: 0; overflow-x: auto; scrollbar-width: none; }

.tender-head__since {
  margin: 0;
  font-size: var(--type-caption-size);
  line-height: var(--type-caption-line);
  color: var(--ink-muted);
  white-space: nowrap;
}

.tender-more { position: static; }

.tender-more > summary { color: var(--ink); }

.tender-more__items {
  left: var(--space-2);
  right: var(--space-2);
  width: auto;
  min-width: 0;
}

.tender-more__items form { margin: 0; }

.tender-more__item {
  display: flex;
  align-items: center;
  width: 100%;
  min-height: var(--tap-min);
  padding: 0 var(--space-3);
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--ink);
  font: inherit;
  font-size: var(--type-body-size);
  text-align: left;
  cursor: pointer;
}

.tender-more__item:hover { background: var(--surface-sunken); text-decoration: none; }

@media (max-width: 640px) {
  .tender-head .rec-head__bar { padding-block: var(--space-3) var(--space-1); }
  .tender-head .rec-head__name {
    font-size: var(--type-title-size);
    line-height: var(--type-title-line);
  }
  .tender-head__meta {
    margin-top: 2px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .tender-head__track { padding-block: 0 var(--space-2); }
  .tender-head .lc-track__next { display: none; }
}

/* A qualifying requirement on a phone: its label, what it asks clamped to two
   lines, and "More" for the rest (ADR-0205). */
.ov-q { display: flex; flex-direction: column; gap: 2px; min-width: 0; }

.ov-more-text > summary {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  color: var(--accent);
  list-style: none;
  cursor: pointer;
}

.ov-more-text > summary::-webkit-details-marker { display: none; }

.ov-clamp {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.ov-q:has(.ov-more-text[open]) .ov-clamp { display: none; }
.ov-more-text[open] > summary { display: none; }

@media (max-width: 640px) {
  .tender-head__meta,
  .tender-head__chips { flex: 0 0 100%; }
}

/* Starting a read (ADR-0207): the button opens its one-line confirm in place —
   the pages and the estimate, and the one button that spends. The same
   confirm, as `.inline-confirm`, sits on a row of the tender's Documents tab
   (ADR-0228): Change opens the kind, Remove… opens "Remove “file”?". It
   opens in the flow, so a table's sideways-scroll box never clips it. */
.read-start > summary,
.inline-confirm > summary { list-style: none; align-self: flex-start; }
.read-start > summary::-webkit-details-marker,
.inline-confirm > summary::-webkit-details-marker { display: none; }
.read-start__cancel,
.inline-confirm__cancel { display: none; }
.read-start[open] > summary,
.inline-confirm[open] > summary {
  order: 2;
  background: none;
  color: var(--accent);
  box-shadow: none;
}
.inline-confirm[open] > summary { border-color: transparent; }
.read-start[open] .read-start__label,
.inline-confirm[open] .inline-confirm__label { display: none; }
.read-start[open] .read-start__cancel,
.inline-confirm[open] .inline-confirm__cancel { display: inline; }
.read-start[open],
.inline-confirm[open] {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-2);
}

.read-start__confirm,
.inline-confirm__confirm {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3);
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
}

.read-start__line,
.inline-confirm__line {
  margin: 0;
  font-size: var(--type-body-sm-size);
  font-weight: 600;
}

.read-start__actions,
.inline-confirm__actions { display: flex; gap: var(--space-2); }

/* A Documents row (ADR-0228): Download and Remove… side by side; Change sits
   under the kind. Opened, a row's confirm is one grey box — its line (or the
   kind to pick), then its button and Cancel together — beside Download where
   it was (ADR-0229). Cancel is a text button inside the box that goes back to
   the tab, so the summary is hidden while it is open: a <details> keeps its
   summary outside its content, so the summary could never sit beside Remove. */
.doc-row__actions {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: var(--space-2);
}
.doc-row__open { margin-top: var(--space-2); }
.doc-row__actions .doc-row__open { margin-top: 0; }
.doc-row__note {
  margin: var(--space-1) 0 0;
  font-size: var(--type-meta-size);
  color: var(--ink-muted);
}
.inline-confirm.doc-row__open[open] > summary { display: none; }
.doc-row__open .inline-confirm__confirm { flex-direction: column; align-items: flex-start; }

/* The two cells that open in place grow with what they open: a table cell is
   otherwise one clipped line, which would cut the confirm's file name off. */
table.app-table.doc-table tbody td.doc-table__kind,
table.app-table.doc-table tbody td.doc-table__actions {
  height: auto;
  white-space: normal;
  overflow: visible;
}
table.app-table.doc-table tbody td.doc-table__actions { max-width: 24rem; }
.doc-table .inline-confirm__line { overflow-wrap: anywhere; }

/* On a phone the Documents table becomes a list (README, Tables: record lists
   become list rows), the same move "To check" makes: the file's name, what it
   is with its Change under it, the description and when on one meta line, then
   Download and Remove…. Nothing sits behind a sideways scroll. */
@media (max-width: 640px) {
  table.doc-table,
  table.doc-table tbody { display: block; }
  table.doc-table thead { display: none; }

  table.doc-table tr.doc-row {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--space-1) var(--space-2);
    padding: var(--space-3) var(--space-4);
    border-top: 1px solid var(--divider);
  }

  table.app-table.doc-table tbody tr.doc-row > td {
    display: block;
    height: auto;
    max-width: none;
    padding: 0;
    border: 0;
    background: none;
  }

  table.app-table.doc-table td.doc-table__file {
    flex: 1 1 100%;
    font-size: var(--type-body-size);
    font-weight: 500;
    white-space: normal;
    overflow-wrap: anywhere;
  }
  table.doc-table td.doc-table__kind { flex: 1 1 100%; }
  table.app-table.doc-table td.doc-table__desc,
  table.app-table.doc-table td.doc-table__when {
    font-size: var(--type-meta-size);
    color: var(--ink-muted);
  }
  table.app-table.doc-table tbody tr.doc-row > td.doc-table__desc--none { display: none; }
  table.doc-table td.doc-table__actions { flex: 1 1 100%; margin-top: var(--space-1); }
  .doc-row__actions { flex-wrap: wrap; }
}

.read-card__check { margin: 0; }

/* ===========================================================================
   OUR CREDENTIALS (ADR-0208) — the credentials screen's layout, from the
   approved laptop design: Financials and Registrations side by side, the
   lists below. Cards, tables, list rows, fields and pills are the system's
   own; only the arrangement is here.
   =========================================================================== */

.cred-grid {
  display: grid;
  grid-template-columns: minmax(0, 4fr) minmax(0, 3fr);
  gap: var(--space-6);
  align-items: start;
}

.cred-grid > .card + .card { margin-top: 0; }

/* The 3-year average: one line on the sunken ground, label left, figure right. */
.cred-average {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--space-2) var(--space-3);
  padding: var(--space-3);
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
  font-size: var(--type-body-sm-size);
  line-height: var(--type-body-sm-line);
}

.cred-average__value {
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* Add a year: the picker and its button on one row. */
.cred-add-year {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
}

.cred-add-year select { width: auto; }

.cred-earlier > summary,
.cred-changes > summary {
  min-height: var(--tap-min);
  display: flex;
  align-items: center;
  cursor: pointer;
  font-size: var(--type-body-sm-size);
  font-weight: 600;
}

.cred-lists {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-6);
}

.cred-list {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-2);
}

.cred-list .section-label { margin: 0; }

.cred-list__rows {
  width: 100%;
  border: 1px solid var(--border);
}

.cred-phone-file .list-row { min-height: var(--tap-min); }

@media (max-width: 1023px) {
  .cred-grid { grid-template-columns: minmax(0, 1fr); gap: var(--space-5); }
}

@media (max-width: 640px) {
  .cred-lists { grid-template-columns: minmax(0, 1fr); gap: var(--space-5); }
  .cred-add-year select { flex: 1 1 auto; }
}

/* Completed jobs (session 4b): the section's two actions, side by side. */
.cred-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

/* A job's sector ticks sit in a fieldset drawn like any other field. */
.cred-fieldset {
  border: none;
  padding: 0;
  margin-inline: 0;
  min-width: 0;
}

.cred-fieldset legend { padding: 0; margin-bottom: var(--space-2); }

/* A job's sector ticks: the option list, two columns where there is room. */
.cred-sectors {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-2);
}

@media (max-width: 640px) {
  .cred-sectors { grid-template-columns: minmax(0, 1fr); }
}


/* ===========================================================================
   CAN WE QUALIFY? (ADR-0209) — how MESPL stands against one requirement: a
   pill, its line, MESPL's closest jobs for similar work, and the desk's tick.
   =========================================================================== */

/* A requirement nobody has checked and no rule decides: neutral, named for its value. */
.pill-not-checked { background: var(--surface-sunken); color: var(--ink-muted); }

.ov-mespl {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-1);
  min-width: 0;
}

.ov-mespl__head {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
}

.ov-suggest {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
}

.ov-suggest__jobs {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.ov-suggest__jobs li { display: flex; flex-direction: column; }

.ov-check > summary {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  cursor: pointer;
  list-style: none;
}

.ov-check > summary::-webkit-details-marker { display: none; }

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

.ov-check__form select { width: auto; }
.ov-check__form input[type="text"] { flex: 1 1 12rem; }

/* Credentials' file inputs and download links (ADR-0209): the picker's
   button drawn as a secondary button, and every download in the accent. */
.cred-file::file-selector-button {
  min-height: calc(var(--tap-min) - var(--space-2));
  margin-right: var(--space-3);
  padding: 0 var(--space-4);
  border: 1px solid var(--control-border);
  border-radius: var(--radius-pill);
  background: var(--surface-card);
  color: var(--ink);
  font: inherit;
  font-weight: 500;
  cursor: pointer;
}

.cred-file::file-selector-button:hover { border-color: var(--accent); }

.cred-download,
.list-row .cred-download {
  color: var(--accent);
  font-weight: 500;
}

/* ---------- Review the quantum: "To check" (ADR-0218) ----------
   The review screen's default view: the names proposed for the house list as
   tiles, and the lines the AI was not sure of as a table that stacks into a
   list on a phone. Built from the README's parts — cards, the segmented
   control, pills and scope tags, the dark bulk bar (.sel-tray) — and tokens
   only. */
.rq-head { margin-bottom: var(--space-4); }

/* What a write on "To check" did, said in place (the view is drawn again,
   not the page, so there is no toast). */
.app-message--success {
  background: var(--success-tint);
  color: var(--success);
}

.rq-crumb {
  margin: 0 0 var(--space-1);
  font-size: var(--type-meta-size);
  line-height: var(--type-meta-line);
  font-weight: 500;
}

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

.rq-switch {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2) var(--space-4);
  margin-bottom: var(--space-6);
}

.rq-views { flex: 0 0 auto; margin-bottom: 0; }
.rq-views .segmented__item { flex: 0 0 auto; }

.rq-page {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
}

.rq-page > .card + .card { margin-top: 0; }

.rq-card-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-1) var(--space-3);
  margin-bottom: var(--space-3);
}

.rq-card-head .section-title { margin: 0; }

.rq-card-head__note {
  font-size: var(--type-meta-size);
  line-height: var(--type-meta-line);
  color: var(--ink-muted);
}

.rq-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, calc(var(--space-8) * 13)), 1fr));
  gap: var(--space-2);
}

.rq-tile {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  min-width: 0;
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}

.rq-tile__top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  min-width: 0;
}

/* A tile opened to its lines (C1a) takes the card's whole width. */
.rq-tile--open { grid-column: 1 / -1; }

.rq-plines {
  margin: 0;
  padding: 0;
  list-style: none;
  border-top: 1px solid var(--divider);
}

.rq-pline {
  display: grid;
  grid-template-columns: var(--tap-min) calc(var(--space-8) * 2) minmax(0, 1fr) calc(var(--space-8) * 5);
  column-gap: var(--space-2);
  align-items: start;
  padding-block: var(--space-2);
  border-bottom: 1px solid var(--divider);
  font-size: var(--type-body-sm-size);
  line-height: var(--type-body-sm-line);
}

.rq-pline .rq-tick { padding-top: 0; min-height: var(--tap-min); align-items: center; }
.rq-pline__item { color: var(--ink-muted); padding-top: var(--space-3); overflow-wrap: anywhere; }
.rq-pline__words { min-width: 0; }
.rq-pline__facts {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding-top: var(--space-3);
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
}

.rq-tile__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  padding-top: var(--space-3);
}

.rq-tile__put { display: inline-flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
.rq-tile__put input[type="text"] { width: calc(var(--space-8) * 7); }

/* "Add 'X' as a new category?" asked in the review bar, in the page (C1a). */
.rq-ask {
  padding: var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface-sunken);
}

.rq-ask .section-title { margin: 0 0 var(--space-1); }

.rq-tile__text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.rq-tile__name {
  font-size: var(--type-body-size);
  line-height: var(--type-body-line);
  font-weight: 600;
  overflow-wrap: anywhere;
}

.rq-tile__meta {
  font-size: var(--type-meta-size);
  line-height: var(--type-meta-line);
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
}

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

/* "Merge into…": a small panel under the button, holding the house list. */
.rq-merge { position: relative; }
.rq-merge > summary { list-style: none; cursor: pointer; }
.rq-merge > summary::-webkit-details-marker { display: none; }

.rq-merge__form {
  position: absolute;
  top: calc(100% + var(--space-1));
  right: 0;
  z-index: 5;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface-card);
  box-shadow: var(--shadow-sheet);
}

.rq-merge__form select { width: auto; max-width: calc(var(--space-8) * 7); }

.rq-lines { padding: 0; overflow: hidden; }

.rq-lines__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-4) var(--space-3);
}

.rq-lines__head .section-title { margin: 0; }
.rq-lines__head .card-subtitle { margin: 2px 0 0; }

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

.rq-filters select { width: auto; }
.rq-filters input[type="text"] { width: calc(var(--space-8) * 7); }

.rq-table-wrap { border: 0; border-radius: 0; }

/* The decision columns stay on screen (C1a): the table is laid out fixed to
   its card's width, so a long parent line is cut inside the description
   column instead of pushing the AI's category and Scope off the right edge. */
@media (min-width: 641px) {
  table.app-table.rq-table { table-layout: fixed; width: 100%; }
  table.rq-table .rq-table__desc { overflow: hidden; }
  table.rq-table .rq-table__desc .row-label__parent { max-width: 100%; }
  table.rq-table th.num,
  table.rq-table td.rq-table__qty,
  table.rq-table td.rq-table__value { width: calc(var(--space-8) * 3); }
}

table.app-table.rq-table tbody td {
  height: auto;
  max-width: none;
  padding: var(--space-3);
  border-bottom: 0;
  border-top: 1px solid var(--divider);
  vertical-align: top;
  white-space: normal;
  overflow: visible;
  line-height: var(--type-body-sm-line);
}

table.app-table.rq-table th.num,
table.app-table.rq-table tbody td.num {
  text-align: right;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

.rq-table__tick { width: var(--tap-min); }
table.rq-table .rq-table__tick { padding: 0; }

/* The tick answers across the whole 44px cell, not just the 20px box. */
.rq-tick {
  display: flex;
  align-items: flex-start;
  justify-content: center;
  min-width: var(--tap-min);
  min-height: var(--tap-min);
  padding-top: var(--space-3);
  cursor: pointer;
}

.rq-table__tick input[type="checkbox"] {
  display: block;
  width: 20px;
  height: 20px;
  margin: 0;
  accent-color: var(--accent);
}

.rq-table__item { width: calc(var(--space-8) * 3); color: var(--ink-muted); }
.rq-table__ai { width: calc(var(--space-8) * 6); }
.rq-table__scope { width: calc(var(--space-8) * 4); }

.rq-row--on > td { background: var(--accent-tint); }

.rq-many {
  display: block;
  font-size: var(--type-caption-size);
  line-height: var(--type-caption-line);
  font-weight: 600;
  color: var(--ink);
}

/* A scope of work runs to a page: its own words take three lines here (its
   parent is the one line above, E2), the whole on hover and on "All lines".
   A phone has no hover, so there the words are never cut. */

@media (max-width: 640px) {
}

.rq-sub {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  margin-top: 2px;
  font-size: var(--type-caption-size);
  line-height: var(--type-caption-line);
  color: var(--ink-muted);
}

.pill-neutral { background: var(--surface-sunken); color: var(--ink-muted); }

.rq-unsure {
  margin-left: var(--space-1);
  font-size: var(--type-caption-size);
  color: var(--warning);
}

.rq-foot,
.rq-empty {
  margin: 0;
  padding: var(--space-3) var(--space-4);
  border-top: 1px solid var(--divider);
  font-size: var(--type-meta-size);
  line-height: var(--type-meta-line);
  color: var(--ink-muted);
}

.rq-bar__qty { font-weight: 400; }

.rq-bar__actions { flex-wrap: nowrap; }

.rq-bar__select,
.rq-bar__input {
  width: auto;
  max-width: calc(var(--space-8) * 5);
  min-height: var(--button);
  border: 1px solid var(--control-border);
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--surface-card);
}

.rq-bar__input::placeholder { color: var(--control-border); }

.rq-bar__select option {
  background: var(--surface-card);
  color: var(--ink);
}

.rq-spellings__act { width: 1%; text-align: right; }

@media (max-width: 640px) {
  .rq-switch { margin-bottom: var(--space-4); }
  .rq-page { gap: var(--space-5); }
  .rq-tile__top { flex-wrap: wrap; }
  .rq-tile__actions { width: 100%; flex-wrap: wrap; }
  .rq-pline {
    grid-template-columns: var(--tap-min) minmax(0, 1fr);
    grid-template-areas: "tick item" "tick words" "tick facts";
  }
  .rq-pline .rq-tick { grid-area: tick; }
  .rq-pline__item { grid-area: item; padding-top: var(--space-3); }
  .rq-pline__words { grid-area: words; }
  .rq-pline__facts { grid-area: facts; flex-direction: row; flex-wrap: wrap; gap: var(--space-3); padding-top: 0; }
  .rq-tile__put,
  .rq-tile__put input[type="text"] { width: 100%; font-size: var(--type-input-size); }
  .rq-merge__form { left: 0; right: auto; flex-wrap: wrap; }
  .rq-filters,
  .rq-filters select,
  .rq-filters input[type="text"] { width: 100%; }
  .rq-filters select,
  .rq-filters input[type="text"],
  .rq-bar__select,
  .rq-bar__input { font-size: var(--type-input-size); }

  /* The table becomes a list: one card-less row per wording, the tick on the
     left, the figures under the words. */
  table.rq-table,
  table.rq-table tbody { display: block; }
  table.rq-table thead { display: none; }

  table.rq-table tr.rq-row {
    display: grid;
    grid-template-columns: var(--tap-min) minmax(0, 1fr) auto;
    grid-template-areas:
      "tick item value"
      "tick desc desc"
      "tick qty qty"
      "tick ai scope";
    column-gap: var(--space-2);
    padding: var(--space-3) var(--space-4) var(--space-3) 0;
    border-top: 1px solid var(--divider);
  }

  table.app-table.rq-table tr.rq-row > td {
    display: block;
    width: auto;
    padding: 0;
    border: 0;
    background: none;
  }

  .rq-row--on { background: var(--accent-tint); }

  table.rq-table td.rq-table__tick { grid-area: tick; }
  .rq-tick { padding-top: 0; }

  table.rq-table td.rq-table__item { grid-area: item; }
  table.app-table.rq-table tbody td.rq-table__value { grid-area: value; font-weight: 600; color: var(--ink); }
  table.rq-table td.rq-table__desc { grid-area: desc; margin-top: var(--space-1); }
  table.app-table.rq-table tbody td.rq-table__qty { grid-area: qty; text-align: left; color: var(--ink-muted); }
  table.rq-table td.rq-table__ai { grid-area: ai; margin-top: var(--space-1); }
  table.rq-table td.rq-table__scope { grid-area: scope; margin-top: var(--space-1); text-align: right; }

  .rq-many { display: inline; margin-left: var(--space-1); }

  .rq-bar__actions { flex-wrap: wrap; }
  .rq-bar__select,
  .rq-bar__input { flex: 1 1 0; min-width: 0; max-width: none; }
  .rq-bar__actions .btn-primary { flex: 1 1 100%; }
}

/* ============================================================
   CHECK THE READING (ADR-0222)
   ============================================================
   One table of what a reading found, one row per fact, grouped by section:
   the field, what the document states with a one-line quote, its page chip,
   what the tender holds now, and the status with Accept / Leave (the .seg
   control above). The dark bar is .sel-tray. On a phone each row is a card. */

.cr-page {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
}

.cr-page > .card + .card { margin-top: 0; }

.cr-brief {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2) var(--space-4);
}

.cr-brief__tick {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex: 1 1 0;
  min-width: 0;
  min-height: var(--tap-min);
  cursor: pointer;
}

.cr-brief__tick input { width: 18px; height: 18px; flex: none; margin: 0; accent-color: var(--accent-fill); }

.cr-brief__text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }

.cr-brief__title {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--type-body-size);
  line-height: var(--type-body-line);
  font-weight: 600;
}

.cr-brief__meta {
  font-size: var(--type-meta-size);
  line-height: var(--type-meta-line);
  color: var(--ink-muted);
}

.cr-brief__preview > summary {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  list-style: none;
  cursor: pointer;
}

.cr-brief__preview > summary::-webkit-details-marker { display: none; }
.cr-brief__preview[open] { flex-basis: 100%; }
.cr-brief__preview[open] > summary { margin-left: auto; display: flex; justify-content: flex-end; }

.cr-brief__heading {
  margin: var(--space-4) 0 var(--space-1);
  font-size: var(--type-meta-size);
  line-height: var(--type-meta-line);
  font-weight: 600;
}

.cr-facts { padding: 0; overflow: hidden; }

.cr-table-wrap { border: 0; border-radius: 0; }

table.app-table.cr-table thead th { background: var(--surface-card); }

/* Values and clauses wrap here — a person reads them before accepting — and a
   chosen Accept is not a ticked row, so the table's selected tint is off. */
table.app-table.cr-table tbody td,
table.app-table.cr-table tbody th {
  height: auto;
  max-width: none;
  padding: var(--space-3);
  border-top: 1px solid var(--divider);
  border-bottom: 0;
  vertical-align: top;
  text-align: left;
  white-space: normal;
  overflow: visible;
}

table.app-table.cr-table tbody tr:has(input:checked) { background: none; }

.cr-table__field { width: calc(var(--space-8) * 5); }
.cr-table__source { width: calc(var(--space-8) * 4); }
.cr-table__now { width: calc(var(--space-8) * 7); }
.cr-table__status { width: calc(var(--space-8) * 6); }

table.app-table.cr-table tr.cr-group th {
  padding: var(--space-2) var(--space-3);
  background: var(--surface-sunken);
  font-size: var(--type-section-label-size);
  line-height: var(--type-section-label-line);
  font-weight: var(--type-section-label-weight);
  letter-spacing: var(--type-section-label-tracking);
  text-transform: uppercase;
  color: var(--ink-muted);
}

table.app-table.cr-table th.cr-field { font-weight: 500; color: var(--ink); }

.cr-row--differs > td,
.cr-row--differs > th,
.cr-row--pick > td,
.cr-row--pick > th { background: var(--warning-tint); }

.cr-value { display: block; overflow-wrap: anywhere; }

/* A short quote: one line, the whole of it on hover. */
.cr-quote {
  display: block;
  max-width: calc(var(--space-8) * 18);
  margin-top: 2px;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: var(--type-caption-size);
  line-height: var(--type-caption-line);
  font-style: italic;
  color: var(--ink-muted);
}

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

.cr-pick {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  min-height: var(--tap-min);
  cursor: pointer;
}

.cr-pick input { flex: none; margin-top: 3px; accent-color: var(--accent-fill); }
.cr-pick .page-chip,
.cr-pick .pill { margin-left: var(--space-1); }

.cr-forms { display: flex; flex-wrap: wrap; gap: var(--space-1); }
.cr-forms .pill { white-space: normal; }

.cr-source .page-chip + .page-chip,
.cr-source .page-chip + .pill { margin-left: var(--space-1); }

.cr-now__label { display: none; }
.cr-now__empty { color: var(--ink-muted); }

.cr-status .pill { margin-bottom: var(--space-2); }
.cr-status { white-space: nowrap; }
.cr-seg { display: flex; width: max-content; }

.cr-by {
  display: block;
  font-size: var(--type-caption-size);
  line-height: var(--type-caption-line);
  color: var(--ink-muted);
}

.cr-foot {
  padding: var(--space-3) var(--space-4) var(--space-4);
  border-top: 1px solid var(--divider);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.cr-foot__line {
  margin: 0;
  font-size: var(--type-meta-size);
  line-height: var(--type-meta-line);
  color: var(--ink-muted);
}

.cr-foot__pills { margin: 0; display: flex; flex-wrap: wrap; gap: var(--space-1); }

@media (max-width: 640px) {
  .cr-page { gap: var(--space-4); }

  /* The table becomes cards: the field and its status on top, then what was
     found, its page, what the tender holds, and Accept / Leave full width. */
  table.cr-table,
  table.cr-table tbody { display: block; }
  table.cr-table thead { display: none; }

  table.app-table.cr-table tr.cr-group { display: block; }
  table.app-table.cr-table tr.cr-group th { display: block; }

  table.app-table.cr-table tr.cr-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas:
      "field source"
      "found found"
      "now now"
      "status status";
    gap: var(--space-2);
    padding: var(--space-3) var(--space-4);
    border-top: 1px solid var(--divider);
  }

  table.app-table.cr-table tr.cr-row > td,
  table.app-table.cr-table tr.cr-row > th {
    display: block;
    width: auto;
    padding: 0;
    border: 0;
    background: none;
  }

  table.app-table.cr-table th.cr-field { grid-area: field; font-weight: 600; }
  table.app-table.cr-table td.cr-found { grid-area: found; }
  table.app-table.cr-table td.cr-source { grid-area: source; text-align: right; }
  table.app-table.cr-table td.cr-now { grid-area: now; }
  table.app-table.cr-table td.cr-status {
    grid-area: status;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    white-space: normal;
  }

  .cr-row--differs,
  .cr-row--pick { background: var(--warning-tint); }

  .cr-quote { max-width: 100%; }

  .cr-now__label {
    display: block;
    font-size: var(--type-caption-size);
    line-height: var(--type-caption-line);
    color: var(--ink-muted);
  }

  .cr-status .pill { margin-bottom: 0; }
  .cr-seg { flex: 1 1 100%; width: 100%; }
  .cr-seg label { flex: 1 1 0; justify-content: center; }

  .cr-bar { flex-wrap: wrap; gap: var(--space-2); }
  .cr-bar .sel-tray__actions { width: 100%; }
  .cr-bar .sel-tray__actions .btn-primary { flex: 1 1 0; }
}

/* ---------- What's next card (ADR-0234) ----------
   What a record needs now, at the top of its first tab: the stage's heading and
   its one filled button, the date box, and the open items as count rows, each
   opening where the work is. On a phone the date box comes first. */

.next-card { display: grid; gap: var(--space-4); }
.next-card > .section-label { margin: 0; }

.next-card__top {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 18rem);
  gap: var(--space-6);
  align-items: start;
}

.next-card__call { display: grid; gap: var(--space-3); min-width: 0; }

.next-card__title {
  margin: 0;
  font-size: var(--type-title-size);
  line-height: var(--type-title-line);
  font-weight: 600;
}

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

.next-card__actions form { margin: 0; }

.due-box {
  display: grid;
  gap: var(--space-1);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
  background: var(--surface-sunken);
}

.due-box__value {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--type-number-size);
  line-height: var(--type-number-line);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.due-box__value--none { color: var(--ink-muted); font-weight: 400; font-size: var(--type-body-size); }

.todo-list { margin: 0; padding: 0; list-style: none; border-top: 1px solid var(--divider); }
.todo-list > li { border-bottom: 1px solid var(--divider); }
.todo-list > li:last-child { border-bottom: 0; }

.todo-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: var(--row-2);
  padding-block: var(--space-2);
  color: var(--ink);
}

a.todo-row { text-decoration: none; }

.todo-row__body { flex: 1 1 auto; min-width: 0; display: grid; }
.todo-row__title { font-weight: 500; color: var(--ink); }

.todo-row__meta {
  display: block;
  color: var(--ink-muted);
  font-size: var(--type-meta-size);
  line-height: var(--type-meta-line);
}

.todo-row__count {
  flex: none;
  font-size: var(--type-heading-size);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.todo-row__chevron {
  flex: none;
  width: 16px;
  height: 16px;
  color: var(--ink-faint);
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* ---------- Group card (ADR-0234) ----------
   A card that opens and closes, whose closed line says what is inside it, so a
   long record reads as a list of its jobs. A <details>, so it needs no script. */

.group-columns {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
  gap: var(--space-6);
  align-items: start;
  margin-top: var(--space-6);
}

.group-column { display: grid; gap: var(--space-6); min-width: 0; }
.group-column > .card + .card { margin-top: 0; }

.group-card__summary {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-3);
  min-height: var(--tap-min);
  list-style: none;
  cursor: pointer;
}

.group-card__summary::-webkit-details-marker { display: none; }
.group-card__summary .section-title { margin: 0; }

.group-card__summary::after {
  content: "";
  width: var(--space-2);
  height: var(--space-2);
  margin: 0 var(--space-1) var(--space-1);
  border-right: 2px solid var(--ink-faint);
  border-bottom: 2px solid var(--ink-faint);
  transform: rotate(45deg);
}

.group-card[open] > .group-card__summary::after {
  margin-bottom: 0;
  transform: rotate(-135deg);
}

.group-card__head { display: grid; min-width: 0; }

.group-card__digest {
  color: var(--ink-muted);
  font-size: var(--type-meta-size);
  line-height: var(--type-meta-line);
  font-weight: 400;
  overflow-wrap: anywhere;
}

.group-card__body { display: grid; gap: var(--space-5); margin-top: var(--space-4); min-width: 0; }

/* A part of a group, its heading, and the one line that says what a term means. */
.sub-block { display: grid; gap: var(--space-2); min-width: 0; }

.sub-block__title {
  margin: 0;
  font-size: var(--type-body-size);
  line-height: var(--type-body-line);
  font-weight: 600;
}

.gloss {
  margin: 0;
  color: var(--ink-muted);
  font-size: var(--type-meta-size);
  line-height: var(--type-meta-line);
}

.fact-list { display: grid; gap: var(--space-3); margin: 0; }

.fact-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 2px var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--divider);
  font-size: var(--type-body-sm-size);
}

.fact-row dt { font-weight: 600; }
.fact-row dd { grid-column: 1; margin: 0; overflow-wrap: anywhere; }
.fact-row--quiet dd { color: var(--ink-muted); }
.fact-row .gloss { grid-column: 1 / -1; }
.fact-row > .page-chip,
.fact-row > .pill { grid-row: 1; grid-column: 2; justify-self: end; }

.tick-list {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0 var(--space-6);
  margin: 0;
  padding: 0;
  list-style: none;
}

.tick-list__row {
  display: flex;
  align-items: center;
  min-height: var(--tap-min);
  border-bottom: 1px solid var(--divider);
  font-size: var(--type-body-sm-size);
}

.tick-list__row form { margin: 0; }
.tick-list__label { display: flex; align-items: center; gap: var(--space-3); min-height: var(--tap-min); cursor: pointer; }
.tick-list__label input { width: var(--space-5); height: var(--space-5); accent-color: var(--accent-fill); }
.tick-list__row--done span { color: var(--ink-muted); text-decoration: line-through; }

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

.plain-rows > li {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-1) var(--space-3);
  min-height: var(--row-1);
  padding-block: var(--space-2);
  border-bottom: 1px solid var(--divider);
}

.plain-rows > li:last-child { border-bottom: 0; }
.plain-rows > li > span:first-child { display: grid; gap: 2px; min-width: 0; }

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

/* The Overview's lists, short until "See all" (ADR-0234). A phone counts its own. */
@media (min-width: 641px) {
  .ov-list--short > li:nth-child(n + 6) { display: none; }
  table.ov-qualify-table--short tbody tr:nth-child(n + 5) { display: none; }
}

/* Not bidding is closed, like lost and withdrawn: grey, never red. */
.pill-not-bidding { background: var(--surface-sunken); color: var(--ink-muted); }

.read-start__field { flex: 1 1 100%; margin: 0; }
.read-start__field input { width: 100%; }

/* The name in the tender's header. A <details> with nothing in it: on a phone it is
   clamped to one line, and tapping it opens it to show the whole name. */
.tender-name { min-width: 0; max-width: 100%; }
.tender-name > summary { list-style: none; }
.tender-name > summary::-webkit-details-marker { display: none; }
.tender-head__stage { display: none; }

@media (min-width: 641px) {
  .tender-name > summary { pointer-events: none; }
}

/* ---------- Documents: one file (ADR-0234) ---------- */

.upload-box {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--space-3) var(--space-4);
  align-items: center;
  margin-top: var(--space-4);
  padding: var(--space-4);
  border: 1px dashed var(--control-border);
  border-radius: var(--radius-md);
  background: var(--surface-sunken);
}

.upload-box .field { margin: 0; min-width: 0; }
.upload-box input[type="file"] { max-width: 100%; font-size: var(--type-input-size); }
.upload-box__hint { grid-column: 1 / -1; }

.file-row {
  display: grid;
  gap: var(--space-2);
  padding-block: var(--space-4);
  border-bottom: 1px solid var(--divider);
}

.file-row:last-child { border-bottom: 0; }

.file-row__head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--space-1) var(--space-4);
  align-items: start;
}

.file-row__name { font-weight: 600; overflow-wrap: anywhere; }

.file-row__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--space-2);
  grid-row: 1 / span 2;
  grid-column: 2;
}

.file-row__more-items {
  display: grid;
  gap: var(--space-2);
  width: min(20rem, 80vw);
  padding: var(--space-3);
}

.file-row__kind {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-1) var(--space-2);
  font-size: var(--type-body-sm-size);
}

.file-row__kind b { font-weight: 600; }

.file-row__kind .btn-link {
  min-height: var(--tap-min);
  display: inline-flex;
  align-items: center;
  font-size: var(--type-body-sm-size);
}

@media (max-width: 1023px) {
  .group-columns { grid-template-columns: minmax(0, 1fr); gap: var(--space-5); margin-top: var(--space-5); }
  .group-column { gap: var(--space-5); }
}

@media (max-width: 640px) {
  .next-card__top { grid-template-columns: minmax(0, 1fr); gap: var(--space-4); }
  .next-card__title { font-size: var(--type-heading-size); line-height: var(--type-heading-line); }
  .due-box { order: -1; }
  .tick-list { grid-template-columns: minmax(0, 1fr); }
  .file-row__head { grid-template-columns: minmax(0, 1fr); }
  .file-row__actions { grid-row: auto; grid-column: 1; justify-content: flex-start; order: 3; }
  .upload-box { grid-template-columns: minmax(0, 1fr); }
  .upload-box .btn { justify-self: start; }

  /* The tender's header on a phone: name on one line, the lifecycle in the meta
     line. The whole header stays inside the chrome budget. */
  .tender-head__track { display: none; }
  .tender-head__stage { display: inline; }
  .tender-name,
  .tender-head .tender-head__meta { flex: 0 0 100%; }
  .tender-name > summary { cursor: pointer; }
  .tender-name:not([open]) .rec-head__name {
    display: block;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
  }
  .tender-head .tender-head__meta {
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
  }
}

/* The two small folds on the page choice (ADR-0234): Change pages under a ticked
   schedule, and the numbers restored, in one line. Each answers across a tap. */
.option-row__more > summary,
.restored-rows > summary {
  display: flex;
  align-items: center;
  min-height: var(--tap-min);
  color: var(--accent);
  font-size: var(--type-body-sm-size);
  cursor: pointer;
}

/* ---------- Scope split (ADR-0234) ----------
   A category whose lines do not share one scope, as counts, biggest first. Not
   set is the only part to act on: amber, and it opens those lines. A row is a link
   already, so on a list the split is a line of its own under the row. */

.scope-split {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1) var(--space-2);
  color: var(--ink-muted);
  font-size: var(--type-meta-size);
  line-height: var(--type-meta-line);
  font-variant-numeric: tabular-nums;
}

/* The separator ends a line, so a wrapped split never starts with one. */
.scope-split > :not(:last-child)::after {
  content: " ·";
  color: var(--ink-muted);
  font-weight: 400;
}

.scope-split__unset {
  position: relative;
  color: var(--warning);
  font-weight: 600;
}

/* Drawn small, it answers taps across tap-min, as a page chip does. */
.scope-split__unset::after {
  content: "";
  position: absolute;
  inset: calc((100% - var(--tap-min)) / 2) 0;
}

.qt-item:has(> .scope-split) { flex-wrap: wrap; }
.qt-item:has(> .scope-split) > .qt-row { flex: 1 1 calc(100% - var(--tap-min) - 1px); }

.qt-item > .scope-split {
  flex: 0 0 100%;
  padding: 0 var(--space-3) var(--space-2) calc(var(--tap-min) + var(--space-3));
}

.scope-split__part { white-space: nowrap; }

@media (max-width: 640px) {
  .qt-item > .scope-split { padding: 0 0 var(--space-2); }
}

/* ============================================================
   ROW LABEL (ADR-0066 amendment, E2; README "Row label")
   ============================================================
   A BOQ row where it stands alone: its numbered parent's text on ONE line in
   ink-muted, ellipsised, then the client's own words in ink, wrapped in full —
   the part that tells two rows apart is often the end of them. The parent is
   a <details>: a tap (or a click) opens it whole, the tooltip gives it on
   hover. Its summary is drawn one line tall and answers taps across tap-min. */
.row-label__parent { display: block; min-width: 0; }

.row-label__parent > summary {
  display: block;
  padding-block: calc((var(--tap-min) - 1lh) / 2);
  margin-block: calc((1lh - var(--tap-min)) / 2);
  color: var(--ink-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  list-style: none;
  cursor: pointer;
}

.row-label__parent > summary::-webkit-details-marker { display: none; }
.row-label__parent[open] > summary { white-space: normal; overflow-wrap: anywhere; }

/* Two lines, not one (P9, the PO comparison's item cell): the parent chain in
   ink-muted and the client's own words in ink, each clamped to two lines and
   opened whole on a tap — the frozen item column is too narrow for one line
   to say anything. The whole is the tooltip too. The summary answers taps
   across tap-min; the clamp is on the text inside it, so a summary taller than
   two lines never shows part of a third. */
.row-label__parent--two > summary,
.row-label__more > summary {
  display: block;
  padding-block: 0;
  margin-block: 0;
  min-height: var(--tap-min);
  white-space: normal;
  overflow-wrap: anywhere;
  list-style: none;
  cursor: pointer;
}

.row-label__clamp {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}

.row-label__parent--two,
.row-label__more { display: block; min-width: 0; font-weight: 400; }
.row-label__more > summary::-webkit-details-marker { display: none; }
.row-label__parent--two[open] .row-label__clamp,
.row-label__more[open] .row-label__clamp { display: block; -webkit-line-clamp: none; }

/* A row's own words on every tender grid (V1, ADR-0250): two lines, the whole
   on a tap or a click (the summary opens it) and on hover. The BOQ tab's Tall
   rows draw every line whole. */
/* Its summary answers taps across tap-min without making a short row taller:
   the Row label parent line's padding and negative margin. */
.row-label__words > summary {
  min-height: 0;
  padding-block: calc((var(--tap-min) - 1lh) / 2);
  margin-block: calc((1lh - var(--tap-min)) / 2);
}
.row-label__words .row-label__text {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}
.row-label__words[open] .row-label__text,
.rows-tall .row-label__words .row-label__text {
  display: block;
  -webkit-line-clamp: none;
  overflow: visible;
}

table.app-table tbody td.row-label {
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
  overflow-wrap: anywhere;
  min-width: 14rem;
  max-width: 48rem;
}

/* A labelled row runs several lines: its other cells start at its top. */
table.app-table tbody tr:has(> td.row-label) > td { vertical-align: top; }

@media (max-width: 640px) {
  table.app-table tbody td.row-label { min-width: 12rem; max-width: 16rem; }
}

/* ============================================================
   FIGURE FOLD (E2; README "Figure fold")
   ============================================================
   A record header's figure strip as one line on a phone: the few figures that
   matter in `meta`, ink-muted, tabular, then a chevron; a ⚠ in danger first
   when the record has an exception. Drawn one meta line tall, it answers taps
   across tap-min (the page chip's way), so the header stays inside the phone's
   chrome budget. Open, it shows the meta line, the chips and the whole strip. */
.fig-fold { margin-top: 2px; min-width: 0; }

.fig-fold__line {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-1);
  min-width: 0;
  font-size: var(--type-meta-size);
  line-height: var(--type-meta-line);
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
  list-style: none;
  cursor: pointer;
}

.fig-fold__line::-webkit-details-marker { display: none; }

.fig-fold__line::after {
  content: "";
  position: absolute;
  inset: calc((var(--type-meta-line) - var(--tap-min)) / 2) 0;
}

.fig-fold__words {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.fig-fold__flag { flex: none; color: var(--danger); font-weight: 600; }

.fig-fold__chevron {
  flex: none;
  width: 16px;
  height: 16px;
  fill: none;
  stroke: var(--ink-faint);
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.fig-fold[open] .fig-fold__chevron { transform: rotate(90deg); }

.fig-fold__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding-top: var(--space-2);
}

.fig-fold__body .tender-head__meta { white-space: normal; }
.fig-fold__body .rec-head__figures { padding-inline: 0; margin: 0; overflow: visible; }

/* ---------- Enquiry header on a phone (E8b, E8c) ----------
   The top bar carries the title, so the header starts at the figure fold (the
   crumb and the meta line), then the chips on one row; the buttons are under
   ⋯ in the top bar, except "Hand to procurement" on "What it covers", full
   width under the tabs (.enq-handoff). The laptop is untouched. */

@media (max-width: 640px) {
  /* Flush with the top bar, as a record's frame is on a phone, so the tabs
     end inside the chrome budget. */
  .enq-head {
    margin: calc(var(--space-3) * -1) calc(var(--app-gutter) * -1) 0;
    padding: var(--space-2) var(--app-gutter) 0;
    border-radius: 0;
    border-bottom: 1px solid var(--border);
  }
  .enq-head .tabs { padding-inline: 0; }
  .enq-head__id { flex: 1 1 100%; min-width: 0; }
  .enq-head__chips {
    display: flex;
    gap: var(--space-1);
    min-width: 0;
    overflow: hidden;
  }
  .enq-head__chips > .row-chips { flex: none; flex-wrap: nowrap; }
  .enq-handoff .btn { width: 100%; min-height: var(--touch-min); }
}

/* ---------- Enquiry picker (E3) ----------
   The title and "Record" in one bar pinned to the top while the rows scroll
   (design system, "Form bar"); the rows not ticked behind "Add more rows",
   one closed line per client's section; a row that can never go on an
   enquiry greyed, its box disabled. */

.enq-bar {
  position: sticky;
  top: 0;
  z-index: 10;
  display: flex;
  align-items: flex-end;
  gap: var(--space-3);
  margin-top: var(--space-4);
  padding: var(--space-3) var(--space-4);
  background: var(--surface-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}

.enq-bar__field { flex: 1 1 auto; min-width: 0; margin: 0; }
.enq-bar__field input { width: 100%; }
.enq-bar .btn { flex: none; }

.enq-more__body { gap: var(--space-2); }

/* min-width: 0, or a grid item grows to its widest one-line name. */
.enq-section { min-width: 0; border-top: 1px solid var(--divider); }

.enq-section__summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  min-height: var(--tap-min);
  list-style: none;
  cursor: pointer;
}

.enq-section__summary::-webkit-details-marker { display: none; }

/* The group card's chevron, one size down. */
.enq-section__summary::after {
  content: "";
  flex: none;
  width: var(--space-2);
  height: var(--space-2);
  margin: 0 var(--space-1) var(--space-1);
  border-right: 2px solid var(--ink-faint);
  border-bottom: 2px solid var(--ink-faint);
  transform: rotate(45deg);
}

.enq-section[open] > .enq-section__summary::after {
  margin-bottom: 0;
  transform: rotate(-135deg);
}

.enq-section__name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.enq-section__count {
  flex: none;
  color: var(--ink-muted);
  font-size: var(--type-meta-size);
  font-variant-numeric: tabular-nums;
}

.enq-section[open] > .table-wrap { margin-bottom: var(--space-3); }

table.app-table tbody tr.row-left-out > td { color: var(--ink-muted); }

@media (max-width: 640px) {
  .enq-bar {
    top: calc(var(--phone-bar-h) + env(safe-area-inset-top));
    padding: var(--space-2) var(--space-3);
  }
}

.row-views { width: fit-content; max-width: 100%; margin-top: var(--space-4); }
.row-views .segmented__item { flex: 0 0 auto; }

/* Category spellings: mark a category as a service (E3) — the select and its
   button on one line where they fit. */
.cat-service-add { display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--space-3); }
.cat-service-add .field { flex: 1 1 16rem; margin: 0; }

/* ============================================================
   THE TENDER BOQ TAB (ADR-0246)
   ============================================================
   Built like the project's Lines tab: the table is a .table-wrap--bounded box
   (ADR-0131) whose column headings pin to its top, and on this tab the tender
   header does not stick, so the synced top bar above the box is never under
   it. A heading row spans the table and wraps; its Remove… opens its confirm
   in place, under its words. */
.boq-table td.boq-heading {
  white-space: normal;
  max-width: none;
  overflow: visible;
  background: var(--surface-sunken);
  font-weight: 600;
}

/* The heading's words, two lines at most — a BOQ heading can be a paragraph
   of terms; the whole is the tooltip, and the heading's side panel (V1b), where
   its Remove… now lives. A phone's open arrow sits beside the words. */
.boq-heading__band {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: start;
  gap: var(--space-1) var(--space-4);
}
.boq-heading__text {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}
.boq-heading__band > .boq-row__removed { grid-column: 1; }
/* .icon-btn draws itself; a laptop opens the panel by a click on the row. */
.boq-heading__open.phone-only { display: none; }

/* A row's tick: the box drawn at its usual size inside a 44px target, so a
   thumb hits it on a phone without the box itself growing (ADR-0185). */
.tick-target {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--tap-min);
  min-height: var(--tap-min);
  margin: calc(var(--space-3) * -1) 0;
  cursor: pointer;
}

/* When, who and why a row was removed, under its words in the Removed view. */
/* The Description column keeps its room (V1b): at least 360px at 1440. With
   a row's side panel open, Offered rate, Amount and Needs action step aside
   rather than crush the words — they are in the panel. Scope wraps to two
   short lines ("Supply + / installation") instead of taking a wide column. */
@media (min-width: 1024px) {
  .boq-table th[data-col="description"],
  .boq-table td[data-col="description"] { min-width: 22.5rem; }
  .boq-table th[data-col="scope"],
  .boq-table td[data-col="scope"] { width: 7rem; max-width: 7rem; white-space: normal; }
  .lines-layout--open .boq-table th[data-col="rate"],
  .lines-layout--open .boq-table td[data-col="rate"],
  .lines-layout--open .boq-table th[data-col="amount"],
  .lines-layout--open .boq-table td[data-col="amount"],
  .lines-layout--open .boq-table th[data-col="exception"],
  .lines-layout--open .boq-table td[data-col="exception"],
  /* The Sourcing view's extras step aside too (T4, ADR-0261); the panel says them. */
  .lines-layout--open .boq-table th[data-col="makes"],
  .lines-layout--open .boq-table td[data-col="makes"],
  .lines-layout--open .boq-table th[data-col="enquiry"],
  .lines-layout--open .boq-table td[data-col="enquiry"],
  .lines-layout--open .boq-table th[data-col="query"],
  .lines-layout--open .boq-table td[data-col="query"],
  .lines-layout--open .boq-table th[data-col="best"],
  .lines-layout--open .boq-table td[data-col="best"] { display: none; }
}

/* The Sourcing view fits a 1440 laptop without sideways scroll (W2): the
   parent label above a row's words is one ellipsised line inside the words'
   column instead of setting its width, and the narrow columns wrap — Unit,
   Enquiry (before the makes), the makes clamped to two lines, Query and
   Needs action. Measured on tender 6: 1,599px in a 1,166px box before,
   inside it after, Description at its 360px. */
@media (min-width: 1024px) {
  .boq-table .boq-row__parent {
    display: block;
    max-width: 21rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .boq-table th[data-col="uom"],
  .boq-table td[data-col="uom"] { width: 4rem; max-width: 4.5rem; white-space: normal; }
  .boq-table th[data-col="category"],
  .boq-table td[data-col="category"] { max-width: 9rem; white-space: normal; }
  .boq-table th[data-col="enquiry"],
  .boq-table td[data-col="enquiry"] { width: 7.5rem; max-width: 7.5rem; white-space: normal; }
  .boq-table th[data-col="makes"],
  .boq-table td[data-col="makes"] { width: 9rem; max-width: 9rem; white-space: normal; }
  .boq-table .boq-makes {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    overflow: hidden;
    white-space: normal;
    max-width: 9rem;
  }
  .boq-table th[data-col="query"],
  .boq-table td[data-col="query"] { width: 3.5rem; white-space: normal; }
  .boq-table th[data-col="exception"],
  .boq-table td[data-col="exception"] { width: 5.5rem; max-width: 6rem; white-space: normal; }
  /* A category's why opens in its cell's own width and wraps there (W2): the
     column is 9rem now, and a 16rem card was cut at its edge. */
  .boq-table .boq-why[open] { min-width: 0; }
  .boq-table .boq-why[open] .boq-why__body { width: auto; max-width: 100%; }
  .boq-table td[data-col="category"]:has(.boq-why[open]) { overflow: visible; text-overflow: clip; min-width: 12rem; max-width: 12rem; }
  .boq-table .boq-why__card,
  .boq-table .boq-why__card li { white-space: normal; overflow: visible; overflow-wrap: anywhere; }
  /* The flag wraps inside its cell rather than spill past the table's edge. */
  .boq-table td[data-col="exception"] .pill { white-space: normal; height: auto; text-align: left; overflow-wrap: anywhere; max-width: 100%; }
  .boq-table .boq-makes { overflow-wrap: anywhere; }
}

/* The phone BOQ's first row on the first screen (W2): the tender's name is the
   top bar's, the BOQ bar folds into the Filters panel, and that panel opens as
   the design system's bottom sheet above the tab bar — under the toolbar, at
   most 80% of the screen, radius-lg top corners, shadow-sheet, Apply pinned at
   its foot. Tapping Filters again closes it. */
@media (max-width: 640px) {
  body:has(.boq-bar) .tender-head .tender-name { display: none; }
  body:has(.boq-bar) .tender-head .rec-head__bar:not(:has(.fig-fold)) { padding-block: 0; }
  body:has(.boq-bar) .toolbar-card { margin-top: var(--space-2); }
  body:has(.boq-bar) .line-toolbar__filters > .menu:first-of-type > .menu__items--form {
    position: fixed;
    top: auto;
    left: 0;
    right: 0;
    bottom: calc(var(--tabbar) + env(safe-area-inset-bottom));
    z-index: 36;
    /* Under the toolbar, so Filters stays in sight and a tap on it closes the sheet. */
    max-height: min(80vh, calc(100dvh - var(--tabbar) - 12rem));
    overflow-y: auto;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    box-shadow: var(--shadow-sheet);
    background: var(--surface-card);
  }
  /* The sheet's actions pinned at its foot. */
  body:has(.boq-bar) .menu__items--form .filter-actions {
    position: sticky;
    bottom: calc(var(--space-3) * -1);
    background: var(--surface-card);
    padding-block: var(--space-3);
  }
  .boq-sheet-head { margin-bottom: var(--space-3); }
  .boq-sheet-head .boq-bar__facts { margin: 0 0 var(--space-2); }
}

.boq-row__removed {
  margin: var(--space-1) 0 0;
  font-size: var(--type-meta-size);
  font-weight: 400;
  color: var(--ink-muted);
  white-space: normal;
}

.boq-table tr.is-removed td[data-col="description"] {
  white-space: normal;
  overflow: visible;
}

.boq-row__put-back { margin-top: var(--space-2); }

/* THE PINNED SECTION BAND (V1, ADR-0250). Inside the bounded box each heading
   row sticks under the pinned column headings (36px) as it scrolls past, and
   the next one slides over it — so the section the rows on screen belong to
   is always the band at the top of the box. A laptop only: a phone's table is
   not a scroll box. */
@media (min-width: 641px) {
  .boq-table.table-wrap--bounded tr.line-heading > td {
    position: sticky;
    top: calc(var(--space-8) + var(--space-1));
    z-index: 1;
    background: var(--surface-sunken);
    box-shadow: 0 1px 0 var(--border);
  }
}

/* The finished "BOQ from the documents" card as one line (V1). */
.boq-docs-line {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  padding-block: var(--space-2);
  font-size: var(--type-meta-size);
  line-height: var(--type-meta-line);
  color: var(--ink-muted);
}
.boq-docs-line a,
.boq-docs-line .text-btn { min-height: var(--tap-min); display: inline-flex; align-items: center; }

.boq-card .card-header { align-items: flex-start; }
.boq-more { position: relative; }
.boq-more > summary { list-style: none; cursor: pointer; }
.boq-more > summary::-webkit-details-marker { display: none; }
.boq-more .menu__items { right: 0; left: auto; min-width: calc(var(--space-8) * 8); }

/* The BOQ tab's one bar over the toolbar (V1): its counts and where its rows
   came from on the left, Short / Tall and More on the right. No card. */
.boq-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2) var(--space-4);
}
.boq-bar__facts {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0 var(--space-2);
  margin: 0;
  font-size: var(--type-meta-size);
  line-height: var(--type-meta-line);
  color: var(--ink-muted);
}
.boq-bar__facts a { display: inline-flex; align-items: center; min-height: var(--tap-min); }
.boq-bar__tools { display: flex; align-items: center; gap: var(--space-2); }
.boq-heights { width: max-content; margin-bottom: 0; }
.boq-heights .segmented__item:not(.segmented__item--on) { background: none; }
.boq-heights .segmented__item {
  min-height: var(--tap-min);
  border: 0;
  font: inherit;
  cursor: pointer;
}

/* The row panel's chain and words (V1): the headings muted, the words whole. */
.boq-panel__chain { margin: 2px 0 0; overflow-wrap: anywhere; }
.boq-panel__words { overflow-wrap: anywhere; }
.boq-panel__facts { margin: 0; }
.boq-panel__remove > summary { list-style: none; }
.boq-panel__remove > summary::-webkit-details-marker { display: none; }
@media (max-width: 640px) {
  /* The item number opens the row's panel as a page on a phone only. */
  .boq-row__open.phone-only { display: inline-flex; align-items: center; min-height: var(--tap-min); font-weight: 600; }
  .boq-heights { display: none; }
  .boq-bar__tools { width: 100%; justify-content: flex-end; }
}

/* The figure's name inside a phone card ("Qty", "Offered rate"). */
.boq-row__key { color: var(--ink-muted); margin-right: var(--space-2); }

/* The phone: one card per row, as the rate grid draws its rows (E8). The same
   <table>, ticks and detail rows as the laptop's, drawn as blocks: the tick
   and the item number, the client's words in full, then a line each for the
   quantity, the scope, the money and the flag. A heading is a band between
   the cards. The column headings go — every figure carries its own name — and
   the page never scrolls sideways. */
@media (max-width: 640px) {
  .boq-table.table-wrap {
    overflow: visible;
    border: 0;
    border-radius: 0;
    background: none;
  }
  .boq-table table.app-table,
  .boq-table table.app-table tbody {
    display: block;
    width: auto;
    min-width: 0;
  }
  .boq-table table.app-table thead { display: none; }
  .boq-table table.app-table tbody tr.boq-row {
    display: grid;
    grid-template-columns: auto 1fr auto;
    gap: var(--space-1) var(--space-2);
    margin-top: var(--space-3);
    padding: var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface-card);
  }
  .boq-table table.app-table tbody tr.boq-row:has(input:checked) { background: var(--accent-tint); }
  .boq-table table.app-table tbody tr.boq-row > td {
    display: block;
    height: auto;
    max-width: none;
    padding: 0;
    border: 0;
    text-align: left;
    white-space: normal;
    overflow: visible;
    grid-column: 1 / -1;
  }
  .boq-table table.app-table tbody tr.boq-row > td.checkbox-cell { grid-column: 1; }
  .boq-table table.app-table tbody tr.boq-row > td[data-col="item"] { grid-column: 2; font-weight: 600; }
  .boq-table table.app-table tbody tr.boq-row > td[data-col="detail"] { grid-column: 3; grid-row: 1; }
  .boq-table table.app-table tbody tr.boq-row > td[data-col="qty"],
  .boq-table table.app-table tbody tr.boq-row > td[data-col="uom"] {
    grid-column: auto;
    display: inline;
  }
  .boq-table table.app-table tbody tr.boq-row > td[data-col="qty"] { grid-column: 1 / 3; }
  .boq-table table.app-table tbody tr.boq-row > td[data-col="uom"] { grid-column: 3; color: var(--ink-muted); }
  .boq-table table.app-table tbody tr.boq-row > td[data-col="scope"],
  .boq-table table.app-table tbody tr.boq-row > td[data-col="rate"],
  .boq-table table.app-table tbody tr.boq-row > td[data-col="amount"] {
    color: var(--ink-muted);
    font-size: var(--type-meta-size);
  }
  .boq-table table.app-table tbody tr.boq-row > td[data-col="exception"]:not(:has(*)) { display: none; }
  .boq-table table.app-table tbody tr.boq-row .tick-target { margin: calc(var(--space-2) * -1) 0 0 calc(var(--space-2) * -1); }
  .boq-heading__open.phone-only { display: inline-flex; margin: calc(var(--space-2) * -1) calc(var(--space-2) * -1) 0 0; }
  .boq-table table.app-table tbody tr.line-heading,
  .boq-table table.app-table tbody tr.list-detail:not([hidden]) { display: block; }
  .boq-table table.app-table tbody tr.line-heading > td { display: none; }
  .boq-table table.app-table tbody tr.line-heading > td.boq-heading {
    display: block;
    height: auto;
    margin-top: var(--space-4);
    padding: var(--space-2) var(--space-3);
    border: 0;
    border-radius: var(--radius-sm);
  }
  .boq-table table.app-table tbody tr.list-detail > td {
    display: block;
    border: 0;
    border-radius: 0 0 var(--radius-md) var(--radius-md);
  }
  .boq-remove input[type="text"] { font-size: var(--type-input-size); }
}


/* ============================================================
   THE CATEGORIES SCREEN (C1a, ADR-0249) — the house list as list rows, a
   search field and "Add a category" over it; the last merge or rename with
   its Undo; a category's kinds with their actions folded under each.
   ============================================================ */
.house-undo {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
  background: var(--accent-tint);
  font-size: var(--type-meta-size);
  line-height: var(--type-meta-line);
}

.house-tools {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-3);
}

.house-search { flex: 1 1 calc(var(--space-8) * 8); max-width: calc(var(--space-8) * 12); }

.house-add { position: relative; }
.house-add > summary { list-style: none; cursor: pointer; }
.house-add > summary::-webkit-details-marker { display: none; }
.house-add__form {
  position: absolute;
  right: 0;
  z-index: 5;
  width: calc(var(--space-8) * 12);
  margin-top: var(--space-2);
  box-shadow: var(--shadow-sheet);
}

.house-check {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-min);
  font-size: var(--type-body-sm-size);
}

/* A kind's row (V1b): its name and counts, and one ⋯ at the right holding
   Rename, Merge into… and See its lines. A form opens inside the menu. */
.list-row:has(> .house-kind__menu) { position: relative; padding-right: calc(var(--tap-min) + var(--space-4)); }
.house-kind__menu { position: absolute; top: var(--space-2); right: var(--space-2); }
.house-kind__items { min-width: 18rem; }
.house-kind__items .house-act__form { padding: 0 var(--space-3) var(--space-2); }
.house-act > summary { list-style: none; cursor: pointer; }
.house-act > summary::-webkit-details-marker { display: none; }
.house-act__form {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

.house-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--space-3);
}

.house-row .field { flex: 1 1 calc(var(--space-8) * 8); margin: 0; }
.house-facts { margin: 0; padding-left: var(--space-5); line-height: var(--type-body-line); }

@media (max-width: 640px) {
  .house-search { max-width: none; flex-basis: 100%; }
  .house-act__form input,
  .house-act__form select,
  .house-row input { font-size: var(--type-input-size); }
  .house-add { width: 100%; }
  .house-add__form { position: static; width: auto; box-shadow: none; }
}

/* Two levels and plant-type sets (C1b, ADR-0254). The family filter sits in
   the search form beside the search; a set's rows carry ↑ ↓ and Remove at the
   right, as a document set's files do. */
.house-search:has(> .house-family) {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: var(--space-2);
  max-width: calc(var(--space-8) * 18);
}
.house-search:has(> .house-family) > .search-field { flex: 1 1 0; min-width: 0; }
.house-family { flex: 0 1 calc(var(--space-8) * 6); min-width: 0; min-height: var(--tap-min); }
.set-row {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
}
.set-row__text { display: flex; flex-direction: column; min-width: 0; }

/* Learning from a finished tender (C2, ADR-0255): a sheet's five column
   pickers in a grid, and one row per desk's name on the review — its lines,
   samples, where it lands, and the choice under them. */
.learn-sheet__head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-2); }
.learn-cols {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(calc(var(--space-8) * 6), 1fr));
  gap: var(--space-3);
}
.learn-cols .field { margin: 0; }
.learn-counts { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-4); margin: 0; padding: 0; list-style: none; font-size: var(--type-meta-size); }
.learn-name { padding: var(--space-3) var(--space-4); border-top: 1px solid var(--divider); }
.learn-name:first-child { border-top: none; }
.learn-name__head { display: flex; justify-content: space-between; gap: var(--space-3); }
.learn-name__samples {
  margin: var(--space-1) 0 0;
  padding: 0;
  list-style: none;
  color: var(--ink-muted);
  font-size: var(--type-meta-size);
  line-height: var(--type-meta-line);
}
.learn-name__samples > li { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.learn-name__item { font-variant-numeric: tabular-nums; font-weight: 600; }
.learn-name__lands { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); margin: var(--space-2) 0 0; }
.learn-name__choice {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(calc(var(--space-8) * 6), 1fr));
  gap: var(--space-2) var(--space-3);
  margin-top: var(--space-2);
}
.learn-name__choice .field { margin: 0; }
.learn-change > summary { list-style: none; cursor: pointer; display: inline-flex; align-items: center; min-height: var(--tap-min); }
.learn-change > summary::-webkit-details-marker { display: none; }
.learn-sure__summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-1) var(--space-3);
  min-height: var(--tap-min);
}
.learn-sure__summary::-webkit-details-marker { display: none; }
.learn-sure[open] > .learn-sure__summary { margin-bottom: var(--space-2); }
.learn-actions {
  position: sticky;
  bottom: 0;
  display: flex;
  justify-content: flex-end;
  gap: var(--space-3);
  margin-top: var(--space-4);
  padding: var(--space-3) 0;
  background: var(--surface-page);
}

@media (max-width: 640px) {
  .learn-name { padding: var(--space-3); }
  .learn-cols select,
  .learn-cols input,
  .learn-name__choice select,
  .learn-name__choice input { font-size: var(--type-input-size); }
  .learn-actions { bottom: var(--tab-bar-h); }
}

/* A family's row on the quantum: a <details> whose summary is drawn as a row
   and opens to its categories, each indented under it. */
.qt-family { border-top: 1px solid var(--divider); }
.qt-family > summary { list-style: none; cursor: pointer; border-top: none; }
.qt-family > summary::-webkit-details-marker { display: none; }
.qt-family__count { display: block; font-weight: 400; }
.qt-family > summary:hover .qt-row { background: var(--surface-sunken); }
.qt-family__chevron { transform: rotate(90deg); }
.qt-family[open] .qt-family__chevron { transform: rotate(-90deg); }
.qt-family__body > .qt-item { border-top: 1px solid var(--divider); }
.qt-family__body .qt-row:not(.qt-row--sub) { padding-left: calc(var(--space-3) + var(--space-4)); }
.qt-family__body .qt-row:not(.qt-row--sub) .qt-row__name { font-weight: 500; }
.qt-family__body .qt-row--sub { padding-left: calc(var(--space-3) + var(--space-4) + var(--space-5)); }

@media (max-width: 640px) {
  .house-family,
  .house-row select { font-size: var(--type-input-size); }
  .house-search:has(> .house-family) > .house-family { flex: 0 0 42%; }
  .qt-family > summary .qt-row { padding: var(--space-3) 0; }
  .qt-family > summary .qt-row__chevron { display: inline-block; flex: 0 0 auto; }
  .qt-row--family .qt-row__detail { flex: 1 1 calc(100% - var(--space-8)); }
  .qt-row--family .qt-row__qty { margin-left: auto; }
  .qt-family__body .qt-row:not(.qt-row--sub) { padding-left: var(--space-4); }
  .qt-family__body .qt-row--sub { padding-left: calc(var(--space-4) + var(--space-5)); }
}


/* ============================================================
   LONG TENDER SCREENS, FOLDED (V1, ADR-0250) — red flags in folded groups
   paged 25 at a time, the offer's sections folding under their headings, and
   the enquiries tab's enquiries as folded cards.
   ============================================================ */
.flag-group > .group-card__summary { padding: var(--space-3) 0; }
.flag-pager {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  padding-top: var(--space-3);
}
.flag-pager__where { font-size: var(--type-meta-size); color: var(--ink-muted); }
.flag-row__words { min-width: 0; }

tbody.fold--closed > tr:not(.line-heading) { display: none; }
.fold-toggle {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  padding-inline: var(--space-2);
  border: 0;
  background: none;
  color: var(--accent);
  font: inherit;
  font-weight: 400;
  cursor: pointer;
}
tbody.fold--closed .fold-toggle__open,
tbody:not(.fold--closed) .fold-toggle__shut { display: none; }

.enq-cards { display: grid; gap: var(--space-2); }
.enq-card > .group-card__summary { padding: var(--space-3) var(--space-4); }
.enq-card__body {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  padding: 0 var(--space-4) var(--space-3);
  font-size: var(--type-meta-size);
  color: var(--ink-muted);
}

/* ---------- Document sets, the page index and the page viewer (D2a, ADR-0252) ---------- */

/* A set on the Documents tab: its title and counts, its two links, then its
   files in order, each with ↑ ↓ and Move to… for the desk. */
.doc-set + .doc-set { margin-top: var(--space-5); }

.doc-set__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-2);
}

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

.doc-set__files > li { align-items: center; }

.doc-set__file { display: grid; gap: var(--space-1); min-width: 0; }

.doc-set__move { display: flex; align-items: center; gap: var(--space-1); margin: 0; }

.doc-set__move .icon-btn:disabled { opacity: 0.35; }

.doc-files__summary {
  display: flex;
  align-items: center;
  min-height: var(--tap-min);
  color: var(--accent);
  cursor: pointer;
}

/* The page index: the filters in one row, then the runs, each folding open
   to its pages. */
.page-index__filters {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}

.page-index__category select { min-height: var(--tap-min); font-size: var(--type-input-size); }

.page-runs > li { display: block; padding: 0; }

.page-run__summary {
  display: flex;
  align-items: center;
  min-height: var(--tap-min);
  padding: var(--space-2) 0;
  list-style: none;
  cursor: pointer;
}

.page-run__summary::-webkit-details-marker { display: none; }

.page-run__main { display: grid; gap: var(--space-1); min-width: 0; }

.page-run__name { font-weight: 600; }

.page-run__body { padding: 0 0 var(--space-3) var(--space-4); }

.page-run__links { display: flex; flex-wrap: wrap; gap: var(--space-4); margin: 0 0 var(--space-2); }

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

.page-run__pages a {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-min);
  color: var(--ink);
}

/* The viewer: the page as wide as the column, the strip of pages under it in
   its own sideways box, the download form last. One filled button. */
.viewer { display: grid; gap: var(--space-3); min-width: 0; }

.viewer__title { margin: 0; font-size: var(--type-heading-size); line-height: var(--type-heading-line); }

.viewer__where { margin: 0; font-size: var(--type-meta-size); color: var(--ink-muted); }

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

.viewer__jump { flex: 1 1 12rem; margin: 0; }

.viewer__jump input {
  width: 100%;
  min-height: var(--tap-min);
  font-size: var(--type-input-size);
}

.viewer__note { margin: 0; font-size: var(--type-body-sm-size); line-height: var(--type-body-sm-line); }

.viewer__page {
  min-width: 0;
  overflow: hidden;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface-card);
}

.viewer__frame { display: block; width: 100%; height: 75vh; border: 0; }

.viewer__strip {
  display: flex;
  gap: var(--space-2);
  overflow-x: auto;
  padding-bottom: var(--space-2);
  scroll-snap-type: x proximity;
}

.viewer__thumb {
  position: relative;
  flex: none;
  width: 6rem;
  scroll-snap-align: center;
}

.viewer__thumb-link {
  display: grid;
  gap: var(--space-1);
  min-height: var(--tap-min);
  padding: var(--space-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-card);
  color: var(--ink);
  font-size: var(--type-caption-size);
}

.viewer__thumb-art {
  display: flex;
  align-items: flex-end;
  height: 5.5rem;
  overflow: hidden;
  color: var(--ink-muted);
}

/* A drawn thumbnail (D2c) shows the top of its page, so every tile is one height. */
.viewer__thumb-link[data-drawn] .viewer__thumb-art { align-items: flex-start; }

.viewer__thumb--current .viewer__thumb-link { border-color: var(--accent); background: var(--accent-tint); }

.viewer__tick {
  position: absolute;
  top: 0;
  right: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--tap-min);
  height: var(--tap-min);
}

.viewer__extract { display: grid; gap: var(--space-2); justify-items: start; }

.viewer__extract .field { width: 100%; }

.viewer__extract input[type="text"] { width: 100%; min-height: var(--tap-min); font-size: var(--type-input-size); }

@media (max-width: 767px) {
  /* A phone shows one page at a time, swiped; the strip stays under it. */
  .viewer__frame { height: 70vh; }
}

/* ---------- Pages on an enquiry (D2b, ADR-0253) ---------- */

.enq-docs__head { flex-wrap: wrap; gap: var(--space-3); }

.enq-docs > li { align-items: center; }

.enq-docs__item { display: grid; gap: var(--space-1); min-width: 0; }

.enq-docs__actions { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: var(--space-3); }

.enq-docs__actions form { margin: 0; }

.enq-docs__actions .text-btn,
.enq-docs__actions .btn-link { display: inline-flex; align-items: center; min-height: var(--tap-min); }

.enq-docs__form { display: grid; gap: var(--space-3); max-width: 32rem; padding-top: var(--space-2); }

.enq-docs__form select,
.enq-docs__form input[type="text"] { width: 100%; min-height: var(--tap-min); font-size: var(--type-input-size); }

.viewer__to-enquiry { position: relative; }

.viewer__to-enquiry-form { display: grid; gap: var(--space-3); min-width: 18rem; padding: var(--space-3); }

.viewer__to-enquiry-form select,
.viewer__to-enquiry-form input { width: 100%; min-height: var(--tap-min); font-size: var(--type-input-size); }

.page-index__to-enquiry { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); margin-bottom: var(--space-3); }

.page-index__to-enquiry select { min-height: var(--tap-min); font-size: var(--type-input-size); }

.page-runs > li.page-runs__ticked { display: grid; grid-template-columns: var(--tap-min) minmax(0, 1fr); align-items: start; }

.page-run__tick { display: flex; align-items: center; justify-content: center; width: var(--tap-min); height: var(--tap-min); }

/* The page drawn by pdf.js (D2c): as wide as its box, pinch to zoom on a phone. */
.viewer__canvas { display: block; width: 100%; height: auto; }

.viewer__thumb-art .viewer__canvas { border-radius: var(--radius-sm); }

/* ---------- Approved makes → vendors (M1, ADR-0256) ----------
   An item on the Makes page: its name and makes with the page chip, then the
   categories it covers and who sells its makes in meta, or a "No vendor yet"
   pill; "Change…" opens one item's editor in place. */
.make-items > li.make-item { display: block; }
.make-item__head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  gap: var(--space-3);
}
.make-item__covers {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0 var(--space-3);
}
.make-item__covers,
.make-item__none {
  margin: var(--space-1) 0 0;
  font-size: var(--type-meta-size);
  line-height: var(--type-meta-line);
  color: var(--ink-muted);
}
.make-item__covers b { color: var(--ink); font-weight: 500; }
.make-item__vendors {
  margin: var(--space-1) 0 0;
  padding: 0;
  list-style: none;
  font-size: var(--type-meta-size);
  line-height: var(--type-meta-line);
  color: var(--ink-muted);
}
.make-item__vendors b { color: var(--ink); font-weight: 500; }
.make-item__change { display: inline-flex; align-items: center; min-height: var(--tap-min); font-size: var(--type-body-sm-size); }
.make-edit {
  margin-top: var(--space-2);
  padding: var(--space-3);
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}
.make-edit__part { margin: 0 0 var(--space-3); padding: 0; border: 0; min-width: 0; }
.make-edit__actions { margin: var(--space-2) 0 0; }
.make-edit__submit { display: flex; align-items: flex-end; }
.make-edit__links > li { align-items: center; }

/* Vendors for this enquiry: suggestions as tickable card rows. */
.enq-vendors__list > li { display: block; padding: var(--space-1) 0; border-bottom: none; }
.enq-vendors__list { border-top: none; display: flex; flex-direction: column; gap: var(--space-2); }
.enq-vendors__on { cursor: default; }
.enq-vendors__actions { margin: var(--space-3) 0 0; }
.enq-vendors__pick { flex-wrap: wrap; margin-top: var(--space-4); }
.enq-vendors__pick input[type="text"] { flex: 1 1 14rem; min-height: var(--tap-min); }
.enq-vendors__pick .btn-link { display: inline-flex; align-items: center; min-height: var(--tap-min); }

@media (max-width: 640px) {
  .make-edit input,
  .make-edit select,
  .enq-vendors__pick input,
  #makes input,
  #makes select { font-size: var(--type-input-size); }
}

/* ---------- Rates from the vendor's Excel (R1, ADR-0257) ----------
   The preview: counts and totals in meta over a table of the rows read, each
   with its item's words, why it is or is not written in ink-muted under them,
   and what saving does as a pill or a tick. Rows not written are quieter. */
.rate-up__figures { margin-top: var(--space-3); }
.rate-up__figures p { margin: 0 0 var(--space-1); }
.rate-up__meta { font-size: var(--type-meta-size); line-height: var(--type-meta-line); color: var(--ink-muted); }
.rate-up__item { min-width: 16rem; white-space: normal; }
.rate-up__table .rate-up__why { white-space: normal; overflow: visible; text-overflow: clip; }
.rate-up__desc { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.rate-up__why { display: block; font-size: var(--type-meta-size); line-height: var(--type-meta-line); color: var(--ink-muted); }
.rate-up__skip td { color: var(--ink-muted); }
.rate-up__typed { font-size: var(--type-meta-size); color: var(--ink-muted); }
.rate-up__tick { display: inline-flex; align-items: center; gap: var(--space-2); min-height: var(--tap-min); white-space: nowrap; }
/* A vendor's own layout or PDF (R2): the line a row went to and how, in meta;
   the box to pick another from the enquiry's list (16px, tap-min tall); the
   pick form's line and button over the table; the lines no row prices. */
.rate-up__match { display: block; margin-top: var(--space-1); font-size: var(--type-meta-size); line-height: var(--type-meta-line); color: var(--ink); white-space: normal; }
.rate-up__line { width: 100%; min-height: var(--tap-min); margin-top: var(--space-1); font-size: var(--type-input-size); }
.rate-up__pick { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-3); }
.rate-up__pick p { margin: 0; }
.rate-up__unquoted { margin-top: var(--space-2); }
.rate-up__unquoted > summary { list-style: none; cursor: pointer; }
.rate-up__unquoted > summary::-webkit-details-marker { display: none; }
.rate-up__tick input { width: 20px; height: 20px; accent-color: var(--accent); }

/* ---------- Pre-bid queries (Q1, ADR-0258) ----------
   A query row: a tick for the sheet (the desk's), its number and words
   clamped to two lines, where it came from and MESPL's suggestion in meta,
   the client's reply under them, and its state as a pill at the right. */
.qry-list > li.qry-row { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: start; gap: var(--space-3); text-align: left; }
.qry-list > li.qry-row--read { grid-template-columns: minmax(0, 1fr) auto; }
.qry-row__tick { display: inline-flex; align-items: center; justify-content: center; min-width: var(--tap-min); min-height: var(--tap-min); margin: calc(var(--space-3) * -1) 0; }
.qry-row__tick input { width: 20px; height: 20px; accent-color: var(--accent); }
.qry-row__body { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }
.qry-row__title { color: var(--ink); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.qry-row__meta { font-size: var(--type-meta-size); line-height: var(--type-meta-line); color: var(--ink-muted); }
.qry-row__reply { font-size: var(--type-body-sm-size); line-height: var(--type-body-sm-line); color: var(--ink); }
.qry-states { flex-wrap: wrap; }
.qry-sheet { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-2) var(--space-3); margin-top: var(--space-3); }
.qry-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-3); margin: var(--space-3) 0 0; }
.qry-form textarea { width: 100%; }
.boq-panel__query { margin: var(--space-3) 0 0; }
.flag-row__ask { display: inline-flex; align-items: center; min-height: var(--tap-min); color: var(--accent); }

@media (max-width: 640px) {
  .qry-form input,
  .qry-form textarea,
  .qry-tools input { font-size: var(--type-input-size); }
}

/* ---------- The comparison grids fit three or four vendors (P9b) ----------
   At 1440 the grid's box is about 1150px. A vendor column is as wide as its
   money figures and inputs, never its words: terms, a contact and a vendor's
   name wrap inside it (the name in two lines, whole on hover); the item column
   is 16rem, 13rem with four vendors, its words already two lines and whole on
   a tap. Five and more scroll between the pinned item column and (on the
   tender grid) Costed from. Laptop only: the phone keeps its own layouts. */
@media (min-width: 641px) {
  table.app-table--grid thead th { min-width: 0; }
  table.app-table--grid:not(.cmp-grid--many) { width: 100%; }
  table.app-table--grid th.cmp-rowhead { min-width: 12rem; max-width: 16rem; }
  table.app-table--grid.cmp-grid--four th.cmp-rowhead,
  table.app-table--grid.cmp-grid--many th.cmp-rowhead { min-width: 10rem; max-width: 12rem; }
  table.app-table--grid tbody td,
  table.app-table--grid tfoot td,
  table.app-table--grid thead th:not(.cmp-rowhead) { padding-left: var(--space-2); padding-right: var(--space-2); }
  table.app-table--grid td.num,
  table.app-table--grid th.num { white-space: nowrap; }
  .cmp-vendor { min-width: 8rem; max-width: 9rem; }
  .cmp-vendor__name {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    overflow-wrap: anywhere;
  }
  .cmp-term { max-width: 8rem; }
  table.app-table--grid td .cmp-amount { white-space: normal; overflow-wrap: anywhere; max-width: 8rem; }
  table.app-table--grid .rate-cell-input--term { width: 8rem; }
  table.app-table--grid .rate-cell-input--text { width: 8rem; }
  table.app-table--grid input[type="file"] { width: 8rem; font-size: var(--type-caption-size); }
  /* Costed from is never cut (P0 #29): the select keeps its chosen vendor's
     whole name and rate, and fills its cell when the cell is wider. */
  .cmp-scroll--rates table.app-table--grid .cmp-basis { min-width: 10rem; }
  .cmp-scroll--rates table.app-table--grid .cmp-basis__select { width: auto; min-width: 100%; }
  .cmp-scroll--rates table.app-table--grid .cmp-basis { overflow-x: clip; }
  .cmp-scroll--rates table.app-table--grid .cmp-basis__note { white-space: normal; }
}

/* ---------- Long text on the record screens (L1) ----------
   A record header's actions keep their own width on a laptop: a long title
   (a vendor's invoice number can be a sentence) clamps to its two lines and
   the buttons stay one line each beside it, rather than the title squeezing
   them into a stack. A vendor invoice's lines show the vendor's part number
   and description in two lines, whole on a tap (Row label), where they were
   cut to one line. */
@media (min-width: 641px) {
  .rec-head__actions { flex: none; }
  .rec-head__actions .btn { white-space: nowrap; }
}
table.app-table td.vi-line__part,
table.app-table td.vi-line__desc {
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
  overflow-wrap: anywhere;
}
table.app-table td.vi-line__part { min-width: 8rem; max-width: 12rem; }
table.app-table td.vi-line__desc { min-width: 16rem; max-width: 24rem; }

/* ---------- The plant type, out of the Scope card (F1) ----------
   On the header's meta line it is a text button in accent that answers taps
   across `tap-min`, like the page chip, and opens the form sheet (ADR-0247).
   With none set, the What's next card's open item opens its picker in place:
   the count row as a <details> summary, the select and an outlined Save
   under it, in the house-row layout. */
.tender-head__plant {
  position: relative;
  z-index: 1;
  color: var(--accent);
  font-weight: 500;
  text-decoration: none;
}

.tender-head__plant::after {
  content: "";
  position: absolute;
  inset: calc(50% - var(--tap-min) / 2) calc(-1 * var(--space-1));
}

.tender-head__plant:hover { text-decoration: underline; }

/* On a phone the meta line is clipped to one line, which would clip the tap
   area too: the line is padded out to `tap-min` and pulled back by the same
   amount, so the header is no taller, and passes taps through to the name and
   tabs it overlaps — only the plant type answers them. */
@media (max-width: 640px) {
  .tender-head__meta:has(.tender-head__plant) {
    padding-block: calc((var(--tap-min) - var(--type-meta-line)) / 2);
    margin-block: calc((var(--tap-min) - var(--type-meta-line)) / -2 + 2px)
      calc((var(--tap-min) - var(--type-meta-line)) / -2);
    pointer-events: none;
  }
  .tender-head__plant { pointer-events: auto; }
}

.todo-pick > summary { list-style: none; cursor: pointer; }
.todo-pick > summary::-webkit-details-marker { display: none; }
.todo-pick[open] .todo-row__chevron { transform: rotate(90deg); }

.todo-pick__form {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--space-3);
  padding-bottom: var(--space-3);
}

.todo-pick__form .field { flex: 1 1 calc(var(--space-8) * 8); margin: 0; }

/* Why a line stays on its draft (P11, ADR-0260): meta, ink-muted, wrapping in
   its cell rather than cut off, in place of the Remove button. */
.draft-lines__why {
  display: block;
  min-width: 14rem;
  white-space: normal;
  font-size: var(--type-meta-size);
  line-height: var(--type-meta-line);
  color: var(--ink-muted);
}

/* ---------- The BOQ as the desk's working sheet (T4, ADR-0261) ----------
   The row's parent heading muted above its words; the category with its
   source tag, whose why opens in the cell (a <details>, fetched when opened —
   in the flow, so the table's own scroll box never clips it); the makes,
   enquiry and best rate's vendor in meta. */
.boq-row__parent {
  display: block;
  color: var(--ink-muted);
  font-size: var(--type-meta-size);
  line-height: var(--type-meta-line);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 48rem;
}

.boq-cat__name { display: block; }
.boq-why { display: block; margin-top: var(--space-1); }
/* The tag answers taps across tap-min, like the page chip. */
.boq-why > summary {
  list-style: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
}
.boq-why > summary::-webkit-details-marker { display: none; }
.boq-why[open] { min-width: 16rem; }
.boq-why[open] .boq-why__body { width: 16rem; }
.boq-why__body,
.boq-why__card {
  margin-top: var(--space-2);
  max-width: 18rem;
  white-space: normal;
  font-size: var(--type-meta-size);
  line-height: var(--type-meta-line);
  color: var(--ink);
}
.boq-why__title { margin: 0 0 var(--space-1); }
.boq-why__lines { margin: 0; padding-left: var(--space-4); }

.boq-makes,
.boq-enquiry,
.boq-best__vendor {
  display: block;
  font-size: var(--type-meta-size);
  line-height: var(--type-meta-line);
  white-space: normal;
  max-width: 14rem;
}
.boq-best__vendor { color: var(--ink-muted); }
/* One line, ellipsised: the whole list is the tooltip and the row panel's. */
.boq-makes { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* The BOQ's bulk bar has five actions (T4): on a phone they scroll inside the
   bar, as the column views do (ADR-0189), rather than run off its edge. */
@media (max-width: 640px) {
  .sel-tray--boq .sel-tray__actions {
    flex: 1 1 auto;
    min-width: 0;
    overflow-x: auto;
    scrollbar-width: none;
  }
}
