/* ──────────────────────────────────────────────────────────────────────────
 * books.css — the neuralbooks app (invoices.html / quotes.html / customers.html).
 *
 * Lifted verbatim from the inline <style> blocks that were duplicated across
 * invoices.html and quotes.html (the invoices block is a superset), plus the
 * shared neuralbooks app bar and the customers view. Palette mirrors the
 * shell's --nds-* dark tokens.
 *
 * NOTE: pre-token debt, MOVED not created — these rules predate the token
 * system and are excluded from the stylelint drift gate (see .stylelintrc.json
 * ignoreFiles), exactly as they were while inline. Tokenizing this file is
 * part of the CSS-drift burn-down, not the neuralbooks extraction.
 * ────────────────────────────────────────────────────────────────────────── */
:root {
  /* Surface/text/accent/border/danger flow from the shared --app-* palette in
     tokens.css (identical values, both themes). Only accent-hover, link-hover
     and the card shadow stay book-local. See tokens.css "Shared APP surface". */
  --inv-bg: var(--app-bg); --inv-surface: var(--app-surface); --inv-card: var(--app-card); --inv-card-hover: var(--app-card-hover);
  --inv-text: var(--app-text); --inv-text-2: var(--app-text-2); --inv-muted: var(--app-muted);
  --inv-border: var(--app-border); --inv-border-2: var(--app-border-2);
  --inv-input-bg: var(--app-sunken); --inv-accent: var(--app-accent); --inv-accent-h: #1493e6; --inv-accent-fg: var(--app-accent-fg);
  --inv-link: var(--app-accent); --inv-link-h: #66c6ff; --inv-danger: var(--app-danger);
  --inv-card-shadow: 0 2px 12px rgba(0,0,0,.35);
  /* Numeric face — Inter (already loaded) carries real tabular figures so money
     columns align; Sora (body) does not. Applied to money via .num-fig below. */
  --num-font: 'Inter', system-ui, -apple-system, sans-serif;
  /* Semantic status tokens — solid forms. (The rgba() alpha tints on banners/
     chips are tokenised in the Track B tokens.css burn-down, not here.) */
  --ok: #22c55e; --ok-soft: #4ade80; --warn: #f59e0b; --warn-soft: #fbbf24;
}
/* Money & date figures: fixed-width digits so decimals stack down a column. */
.row-total, .row-date, .stat-val, .ov-big, .ed-totals, table.ed-items td.num,
.cat-price, .cat-pick-price, .cust-owing, .cat-imp-sell, .cat-imp-num {
  font-family: var(--num-font); font-variant-numeric: tabular-nums;
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--inv-bg); font-family: 'Sora', system-ui, -apple-system, sans-serif; color: var(--inv-text); }
.topbar { display: flex; justify-content: space-between; align-items: center; padding: 16px 28px; background: var(--inv-surface); border-bottom: 1px solid var(--inv-border); }
.topbar h1 { font-size: 18px; margin: 0; letter-spacing: -.01em; }
.topbar a { color: var(--inv-text-2); text-decoration: none; font-size: 14px; }
.wrap { max-width: 920px; margin: 26px auto 80px; padding: 0 16px; }
.muted { color: var(--inv-muted); }
.btn-primary { background: var(--inv-accent); color: var(--inv-accent-fg); border: 0; padding: 10px 18px; border-radius: 8px; font-weight: 600; font-size: 14px; cursor: pointer; box-shadow: 0 2px 10px rgba(51,177,255,.30); transition: background .15s ease; }
.btn-primary:hover { background: var(--inv-accent-h); }
.btn-ghost { background: var(--inv-card-hover); color: var(--inv-text); border: 0; padding: 10px 18px; border-radius: 8px; font-weight: 600; font-size: 14px; cursor: pointer; text-decoration: none; display: inline-block; transition: background .15s ease; }
.btn-ghost:hover { background: #2d374e; }
.btn-danger-ghost { background: rgba(239,68,68,.12); color: var(--inv-danger); border: 0; padding: 10px 18px; border-radius: 8px; font-weight: 600; font-size: 14px; cursor: pointer; transition: background .15s ease; }
.btn-danger-ghost:hover { background: rgba(239,68,68,.22); }
.link { background: none; border: 0; color: var(--inv-link); font-weight: 600; cursor: pointer; font-size: 14px; padding: 0; }
.link:hover { color: var(--inv-link-h); }
.badge { padding: 3px 10px; border-radius: 14px; font-size: 12px; font-weight: 600; text-transform: capitalize; }
.sb { display: flex; align-items: center; justify-content: space-between; gap: 14px; padding: 12px 16px; border-radius: 10px; font-size: 14px; margin-bottom: 14px; }
.sb-ok { background: rgba(34,197,94,.12); border: 1px solid rgba(34,197,94,.30); color: #4ade80; }
.sb-warn { background: rgba(245,158,11,.12); border: 1px solid rgba(245,158,11,.30); color: #fbbf24; }
/* Brand-tinted "connect" invite banners — the initial CTA wears the vendor's
   colour (indigo/blue) with a 3px brand accent bar. "Needs attention" states
   stay amber (.sb-warn) and success stays green (.sb-ok) so those signals read. */
.sb-stripe { background: rgba(99,91,255,.10); border: 1px solid rgba(99,91,255,.28); border-left: 3px solid #635bff; color: #b6b1fb; }
.sb-xero { background: rgba(19,181,234,.10); border: 1px solid rgba(19,181,234,.26); border-left: 3px solid #13b5ea; color: #86d4f1; }
.sb-square { background: rgba(0,106,255,.10); border: 1px solid rgba(0,106,255,.26); border-left: 3px solid #006aff; color: #8ab8f5; }
.sb-stripe strong, .sb-xero strong, .sb-square strong { color: #fff; }
/* Leading brand badge — a white "app-icon" chip carrying the vendor logo,
   so every Stripe/Xero banner is identifiable at a glance. */
.sb-lead { display: flex; align-items: center; gap: 12px; min-width: 0; }
.sb-badge { flex: none; display: inline-flex; align-items: center; justify-content: center; background: #fff; border-radius: 7px; padding: 6px 8px; box-shadow: 0 1px 3px rgba(0,0,0,.25); }
.sb-badge svg { display: block; }
.sb .btn-primary, .sb .btn-brand { white-space: nowrap; }

/* ── Integration banner row ─────────────────────────────────────
   Full-size banners (connect CTAs / "needs attention" states) span
   the whole row; "all set up" success states collapse to compact
   pills that sit side-by-side, each expanding a full-width detail
   panel below the row on click. */
.int-banners { display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-start; }
.int-banners:has(> div:not(:empty)) { margin-bottom: 14px; }
.int-banners > div:empty { display: none; }
.int-banners > div { flex: 1 1 100%; }        /* full banner → own row */
.int-banners .sb { margin-bottom: 0; }
/* In pill mode the container & wrap dissolve so the pill button and its
   detail panel become direct flex items of the row: pills flow together
   on one line, an opened panel wraps onto its own full-width line below. */
.int-banners > div.is-chip, .int-banners .sb-chip-wrap { display: contents; }

/* Collapsed success pill. The brand badge already carries the vendor
   wordmark, so the pill is just badge + tick + chevron. */
.sb-chip { order: 0; flex: 0 1 auto; box-sizing: border-box; min-height: 32px; display: inline-flex; align-items: center; gap: 8px; font-family: inherit; font-size: 13px; font-weight: 600; color: #4ade80; background: rgba(34,197,94,.12); border: 1px solid rgba(34,197,94,.30); border-radius: 999px; padding: 5px 12px 5px 6px; cursor: pointer; transition: background .15s ease, border-color .15s ease; }
.sb-chip:hover { background: rgba(34,197,94,.20); border-color: rgba(34,197,94,.45); }
/* Normalise the two vendor badges to one height so the pills match. */
.sb-chip .sb-badge { padding: 3px 6px; border-radius: 5px; }
.sb-chip .sb-badge svg { height: 13px; width: auto; }
.sb-chip-tick { font-size: 14px; line-height: 1; }
.sb-chip-chev { margin-left: 1px; opacity: .7; transition: transform .25s ease; }
.sb-chip-wrap.open .sb-chip-chev { transform: rotate(180deg); }
/* Detail panel: collapsed → zero-width, invisible item riding the pill row;
   open → full-width flex item that wraps to its own line below the pills.
   grid 0fr→1fr animates the height without measuring content. Border/bg
   live on the open state only so nothing shows while collapsed. */
.sb-chip-body { order: 1; flex: 0 0 0; width: 0; display: grid; grid-template-rows: 0fr; opacity: 0; visibility: hidden; overflow: hidden; transition: grid-template-rows .25s ease, opacity .2s ease; }
.sb-chip-wrap.open .sb-chip-body { flex: 1 1 100%; width: auto; grid-template-rows: 1fr; opacity: 1; visibility: visible; }
.sb-chip-body-inner { overflow: hidden; display: flex; align-items: center; gap: 14px; font-size: 13px; color: var(--ok-soft); border-radius: 10px; }
.sb-chip-wrap.open .sb-chip-body-inner { padding: 10px 14px; background: rgba(34,197,94,.08); border: 1px solid rgba(34,197,94,.22); }
.sb-chip-body-inner .btn-ghost { white-space: nowrap; flex: none; }

/* Stripe "money in transit" pill — sits inline next to the connected Stripe
   pill (the banner row is display:contents, so this span is a flex item too).
   Informational only: no button, Stripe-indigo tint to read as its own thing. */
.sb-balance-pill { order: 0; flex: 0 1 auto; box-sizing: border-box; min-height: 32px; display: inline-flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 600; color: #a5a0ff; background: rgba(99,91,255,.12); border: 1px solid rgba(99,91,255,.32); border-radius: 999px; padding: 5px 12px; white-space: nowrap; }
.sb-balance-pill i { font-size: 15px; opacity: .85; }
/* Inner structure emitted by stripeBalancePill(). Classic pill keeps its inline
   look; the v2 rules below re-lay these out as a ledger row. */
.sb-balance-pill .sbp-item { display: inline-flex; align-items: center; gap: 6px; }
.sb-balance-pill .sbp-item + .sbp-item { margin-left: 10px; }
.sb-balance-pill .sbp-val { font-variant-numeric: tabular-nums; }
:root[data-theme="light"] .sb-balance-pill { color: #4f46e5; background: rgba(99,91,255,.08); border-color: rgba(99,91,255,.22); }

/* ── Integration brand buttons (Stripe / Xero) ──────────────────
   Brand-coloured CTAs so each integration is instantly recognisable
   instead of a generic blue button. Colours are the vendors' brand
   values: Stripe #635bff, Xero #13b5ea. */
.btn-brand { display: inline-flex; align-items: center; gap: 9px; border: 0; padding: 10px 16px; border-radius: 8px; font-weight: 600; font-size: 14px; line-height: 1; color: #fff; cursor: pointer; transition: background .15s ease, box-shadow .15s ease, transform .06s ease; }
.btn-brand:hover { transform: translateY(-1px); }
.btn-brand:active { transform: translateY(0); }
.btn-brand:disabled { opacity: .7; cursor: default; transform: none; }
.btn-brand svg { display: block; }
.brand-logo { height: 15px; width: auto; }
.btn-stripe { background: #635bff; box-shadow: 0 2px 10px rgba(99,91,255,.35); }
.btn-stripe:hover { background: #554df0; }
.btn-xero { background: #13b5ea; box-shadow: 0 2px 10px rgba(19,181,234,.32); }
.btn-xero:hover { background: #0f9fce; }
.btn-square { background: #1a1a1a; box-shadow: 0 2px 10px rgba(0,0,0,.35); }
.btn-square:hover { background: #000; }
/* Xero controls in already-connected / per-invoice contexts (dark bg). */
.btn-xero-ghost { display: inline-flex; align-items: center; gap: 7px; background: rgba(19,181,234,.12); color: #5ccbef; border: 1px solid rgba(19,181,234,.30); padding: 9px 14px; border-radius: 8px; font-weight: 600; font-size: 14px; line-height: 1; cursor: pointer; transition: background .15s ease; }
.btn-xero-ghost:hover { background: rgba(19,181,234,.20); }
.xero-synced { display: inline-flex; align-items: center; gap: 7px; padding: 9px 14px; border-radius: 8px; font-weight: 600; font-size: 14px; line-height: 1; color: #4ade80; background: rgba(34,197,94,.10); border: 1px solid rgba(34,197,94,.25); cursor: default; }

/* list */
.inv-summary { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 18px; }
.inv-summary:empty { display: none; }
.stat { background: var(--inv-card); border-radius: 12px; box-shadow: var(--inv-card-shadow); padding: 16px 18px; text-align: left; font-family: inherit; color: var(--inv-text); border: 1px solid transparent; cursor: pointer; transition: background .15s ease, border-color .15s ease, transform .15s ease, box-shadow .15s ease; }
.stat:hover { background: var(--inv-card-hover); transform: translateY(-2px); box-shadow: 0 6px 18px rgba(0,0,0,.4); }
.stat:active { transform: translateY(0); }
.stat-active { border-color: var(--inv-accent); background: var(--inv-card-hover); }
.stat-val { font-size: 22px; font-weight: 700; letter-spacing: -.01em; transition: color .15s ease; }
.stat-lbl { font-size: 12px; color: var(--inv-muted); margin-top: 4px; text-transform: uppercase; letter-spacing: .04em; }
.stat-danger .stat-val { color: var(--inv-danger); }
.stat-ok .stat-val { color: var(--ok-soft); }
@media (max-width: 640px) { .inv-summary { grid-template-columns: repeat(2, 1fr); } }
.list-head { display: flex; justify-content: space-between; align-items: center; gap: 14px; margin-bottom: 14px; flex-wrap: wrap; }
.list-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.inv-search { width: 220px; padding: 9px 12px; border: 1px solid var(--inv-border-2); border-radius: 8px; background: var(--inv-input-bg); color: var(--inv-text); font-size: 14px; }
.inv-search::placeholder { color: var(--inv-muted); }
.inv-voidtoggle { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; color: var(--inv-muted); white-space: nowrap; cursor: pointer; }
.inv-voidtoggle input { margin: 0; cursor: pointer; }
.inv-settings { margin-bottom: 18px; }
.settings-card { background: var(--inv-card); border-radius: 12px; box-shadow: var(--inv-card-shadow); padding: 22px; }
.settings-head { display: flex; justify-content: space-between; align-items: center; }
.settings-head h3 { margin: 0; font-size: 16px; }
.settings-note { font-size: 12px; color: var(--inv-muted); margin: 6px 0 14px; }
.settings-note a { color: var(--inv-link); }
.settings-sec { font-size: 12px; font-weight: 600; color: var(--inv-text-2); text-transform: uppercase; letter-spacing: .04em; margin: 16px 0 8px; }
.settings-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.settings-grid .lbl { margin-top: 0; }
.settings-actions { margin-top: 18px; }
@media (max-width: 640px) { .settings-grid { grid-template-columns: 1fr; } }
.list-head h2 { margin: 0; font-size: 22px; }
.card { background: var(--inv-card); border-radius: 12px; box-shadow: var(--inv-card-shadow); overflow: hidden; }
.row { display: flex; align-items: center; gap: 14px; padding: 16px 20px; border-bottom: 1px solid var(--inv-border); cursor: pointer; transition: background .15s ease; }
/* The row is keyboard-activatable (doclist.js gives it role=button), so it needs
   a visible focus ring. Inset, because a row is flush with the card's edges. */
.row:focus-visible { outline: 2px solid var(--inv-accent); outline-offset: -2px; }
.row:hover { background: var(--inv-card-hover); }
.row-num { font-weight: 600; }
.row-cust { font-size: 13px; color: var(--inv-text-2); }
/* Linked job name — a quiet third line, deliberately lighter than the customer
   so it reads as provenance ("what this is for") rather than a second subject. */
.row-job { display: flex; align-items: center; gap: 5px; margin-top: 2px; font-size: 12px; color: var(--inv-text-2); opacity: .82; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.row-job i { font-size: 13px; flex: none; opacity: .8; }
/* Fixed-track grid so amount / status / date line up down a column across every
   row (each .row is its own container, so the tracks must be fixed widths). */
.row-meta { display: grid; grid-template-columns: 104px 140px 116px; align-items: center; gap: 14px; margin-left: auto; }
.row-total { font-weight: 600; text-align: right; }
.row-badges { display: flex; align-items: center; gap: 8px; min-width: 0; }
.row-date { font-size: 12px; color: var(--inv-muted); min-width: 90px; text-align: right; }
.row-check { display: inline-flex; align-items: center; flex: none; padding: 2px; }
.row-check input { width: 16px; height: 16px; cursor: pointer; accent-color: var(--inv-accent); }

/* Per-row action buttons (ndDocList `actions` slot). A fixed-width column to
   the right of the meta grid so it never disturbs the amount/status/date
   alignment — the cell is reserved even on rows with no action. */
.row-actions { flex: none; width: 148px; display: flex; align-items: center; justify-content: flex-end; gap: 6px; }
.btn-rowact { font: inherit; font-size: 12px; font-weight: 600; line-height: 1; white-space: nowrap; padding: 6px 11px; border-radius: 8px; border: 1px solid var(--inv-border-2); background: var(--inv-card); color: var(--inv-text); cursor: pointer; transition: background .12s ease, border-color .12s ease; }
.btn-rowact:hover { background: var(--inv-card-hover); border-color: var(--inv-accent); }
.btn-rowact.primary { border-color: var(--inv-accent); color: var(--inv-accent); }
.btn-rowact.primary:hover { background: var(--inv-card-hover); }

/* Thin partial-payment progress bar tucked under a row's amount
   (ndDocList `belowTotal` slot). Right-aligned to sit beneath the total. */
.row-paybar { width: 84px; height: 4px; margin: 5px 0 0 auto; border-radius: 999px; background: var(--inv-border); overflow: hidden; }
.row-paybar > span { display: block; height: 100%; background: var(--inv-accent); border-radius: 999px; }

/* List rows on mobile — the amount, status badge and date can't all fit
   beside the number/customer on a phone, so the fixed-width right-aligned
   .row-date ("Due 23 Jul" / "Valid to 23 Jul") was clipped by the card's
   overflow:hidden. Drop the meta group onto its own line: number + customer
   on top, then total + status on the left with the date pushed to the right. */
@media (max-width: 560px) {
  .row { flex-wrap: wrap; row-gap: 8px; padding: 14px 16px; }
  .row-main { flex: 1 1 auto; min-width: 0; }
  .row-num, .row-cust { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .row-meta { display: flex; flex: 1 1 100%; order: 2; margin-left: 0; gap: 10px; flex-wrap: wrap; }
  .row-date { min-width: 0; margin-left: auto; }
  /* Actions drop to their own full-width line, right-aligned under the meta. */
  .row-actions { width: auto; flex: 1 1 100%; order: 3; }
}

/* Bulk-action bar — floats while rows are selected. */
.bulk-bar { position: fixed; left: 50%; bottom: 22px; transform: translateX(-50%); display: flex; align-items: center; gap: 8px; padding: 8px 10px 8px 16px; background: var(--inv-surface); border: 1px solid var(--inv-border-2); border-radius: 12px; box-shadow: 0 8px 30px rgba(0,0,0,.4); z-index: 60; animation: inv-fade-up .2s ease both; }
.bulk-n { font-size: 13px; font-weight: 600; color: var(--inv-text); margin-right: 4px; white-space: nowrap; }
.bulk-bar .btn-ghost { padding: 7px 12px; font-size: 13px; }
@media (max-width: 560px) { .bulk-bar { left: 12px; right: 12px; transform: none; flex-wrap: wrap; justify-content: center; } }

/* List loading skeleton (shimmer rows) — replaces the plain "Loading…" text. */
.sk-row { display: flex; align-items: center; justify-content: space-between; padding: 17px 20px; border-bottom: 1px solid var(--inv-border); }
.sk-bar { height: 12px; border-radius: 6px; background: linear-gradient(90deg, var(--inv-input-bg) 25%, var(--inv-card-hover) 37%, var(--inv-input-bg) 63%); background-size: 400% 100%; animation: sk-shimmer 1.4s ease infinite; }
@keyframes sk-shimmer { from { background-position: 100% 0; } to { background-position: 0 0; } }
@keyframes spin { to { transform: rotate(360deg); } }

/* Inline button spinner for core/feedback.js withBusy(). The books pages shipped
   the `spin` keyframes but never the ring, so a busy button changed its label
   and nothing else — on a slow call (a Xero push) that reads as a dead control.
   currentColor keeps it legible on ghost and primary buttons alike. */
.spinner {
  display: inline-block; width: 13px; height: 13px; vertical-align: -2px;
  border: 2px solid color-mix(in srgb, currentColor 25%, transparent);
  border-top-color: currentColor; border-radius: 50%;
  animation: spin .7s linear infinite;
}

/* editor */
.ed-top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 18px; }
.ed-status { display: flex; align-items: center; gap: 8px; }
.warn { background: rgba(245,158,11,.12); border: 1px solid rgba(245,158,11,.30); color: #fbbf24; padding: 10px 14px; border-radius: 8px; font-size: 13px; margin-bottom: 16px; }
.ed-dates { font-size: 13px; color: var(--inv-muted); font-weight: 400; }
.ed-from { background: var(--inv-card); padding: 16px 18px; border-radius: 12px; box-shadow: var(--inv-card-shadow); margin-bottom: 16px; }
.ed-from .lbl { margin: 0 0 4px; }
.ed-from .from-name { font-weight: 600; color: var(--inv-text); }
.ed-from .from-line { font-size: 13px; color: var(--inv-text-2); margin-top: 2px; }
/* Standing terms preview under the Notes field — read-only, so it reads as a
   statement of what will print rather than another editable field. */
.doc-terms-ed { margin-top: 12px; padding: 10px 12px; border: 1px solid var(--inv-border); border-radius: 8px; background: var(--inv-input-bg); }
.doc-terms-ed .lbl { margin: 0 0 4px; }
.doc-terms-ed .hint { font-weight: 400; text-transform: none; letter-spacing: 0; }
.doc-terms-ed-body { font-size: 12.5px; line-height: 1.55; color: var(--inv-text-2); white-space: pre-wrap; max-height: 140px; overflow-y: auto; }
.doc-terms-ed.empty { font-size: 12.5px; line-height: 1.55; color: var(--inv-muted); }
.doc-terms-ed a { color: var(--inv-accent); font-weight: 600; }
.ed-payments { margin: 0 0 4px auto; width: 340px; }
.ed-payments .lbl { margin: 0 0 6px; }
.ed-payments .pay-row { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; padding: 5px 0; font-size: 13px; color: var(--inv-text-2); border-top: 1px solid var(--inv-border); }
.ed-payments .pay-when { color: var(--inv-muted); white-space: nowrap; }
.ed-payments .pay-method { flex: 1; }
.ed-payments .pay-amt { font-weight: 600; color: var(--inv-text); white-space: nowrap; }
.ed-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 22px; background: var(--inv-card); padding: 22px; border-radius: 12px; box-shadow: var(--inv-card-shadow); }
.ed-col { display: flex; flex-direction: column; }
.lbl { font-size: 12px; font-weight: 600; color: var(--inv-text-2); margin: 12px 0 4px; }
.ed-col .lbl:first-child { margin-top: 0; }
.hint { font-weight: 400; color: var(--inv-muted); }
/* Inline action inside a field label — for the one thing still editable on an
   otherwise locked field (a finalized quote's expiry). Sized off the label, so
   it reads as part of it rather than as another button in the form. */
.lbl .lnk {
  font: inherit; font-weight: 600; color: var(--inv-accent, var(--app-accent));
  background: none; border: 0; padding: 0; margin-left: var(--sp-1_5);
  cursor: pointer; text-decoration: underline; text-underline-offset: 2px;
}
.lbl .lnk:hover { filter: brightness(1.15); }
/* "Is this a saved customer?" — the line under the doc editor's name field
   (feature_doc_save_customer). Sized off the hint text rather than the label, so
   it reads as an answer about the field above it and not as another control in
   the form. Two states, one row: linked (who, and a way into their record) or
   unlinked (an offer to save what was just typed). */
.cust-link { display: flex; align-items: center; gap: var(--sp-1_5); margin: 5px 0 0; font-size: 12px; color: var(--inv-muted); }
.cust-link i { font-size: 14px; opacity: .8; flex: none; }
.cust-link a { color: var(--inv-accent, var(--app-accent)); font-weight: 600; text-decoration: none; }
.cust-link a:hover { text-decoration: underline; }
.cust-link .lnk {
  font: inherit; font-weight: 600; color: var(--inv-accent, var(--app-accent));
  background: none; border: 0; padding: 0; width: auto; text-align: left;
  cursor: pointer; text-decoration: underline; text-underline-offset: 2px;
}
.cust-link .lnk:hover { filter: brightness(1.15); }
.cust-link .lnk:disabled { opacity: .6; cursor: default; text-decoration: none; }
input, textarea, select { font-family: inherit; font-size: 14px; padding: 9px 11px; border: 1px solid var(--inv-border-2); border-radius: 7px; width: 100%; background: var(--inv-input-bg); color: var(--inv-text); }
input::placeholder, textarea::placeholder { color: var(--inv-muted); }
input:disabled, textarea:disabled { background: var(--inv-bg); color: var(--inv-muted); }
.items-head { display: flex; justify-content: space-between; align-items: center; margin: 26px 0 8px; }
.items-head h3 { margin: 0; font-size: 16px; }
table.ed-items { width: 100%; border-collapse: collapse; background: var(--inv-card); border-radius: 12px; overflow: hidden; box-shadow: var(--inv-card-shadow); }
table.ed-items th { text-align: left; font-size: 12px; color: var(--inv-text-2); padding: 10px; border-bottom: 2px solid var(--inv-border); }
table.ed-items td { padding: 6px 10px; border-bottom: 1px solid var(--inv-border); vertical-align: middle; }
table.ed-items .num { text-align: right; white-space: nowrap; }
/* Line-item description: a one-line-tall textarea that grows downward as the
   text wraps (auto-sized by ndLineItems.grow / .autosize). */
table.ed-items td.it-desc { width: 100%; }
table.ed-items textarea.it-desc-input {
  width: 100%; min-height: 0; resize: none; overflow: hidden;
  line-height: 1.35; display: block; field-sizing: content;
}
.w-qty { width: 70px; } .w-unit { width: 60px; } .w-price { width: 100px; }
.gst-cell { display: flex; align-items: center; gap: 5px; font-size: 13px; }
.gst-cell input { width: auto; }
.x { background: none; border: 0; color: var(--inv-muted); font-size: 20px; cursor: pointer; line-height: 1; }
.x:hover { color: var(--inv-danger); }
.ed-totals { margin: 18px 0 18px auto; width: 280px; }
.ed-totals > div { display: flex; justify-content: space-between; padding: 4px 0; font-size: 14px; color: var(--inv-text-2); }
.ed-totals span { color: var(--inv-muted); }
.ed-totals .grand { border-top: 1px solid var(--inv-border); margin-top: 4px; padding-top: 8px; font-size: 17px; font-weight: 700; color: var(--inv-text); }
.ed-totals .grand span { color: var(--inv-text); }
.ed-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 20px; }

/* ── Parts to bring — invoices/doc-parts.js ──────────────────────────────────
   feature_quote_parts_list on a quote, feature_invoice_parts_list on an
   invoice; one block of styles, because it is one panel either way.
   The internal working behind a document priced as one line. Styled as a quieter
   companion to the line-item table, NOT a second copy of it: dashed border,
   sunken fill, no card shadow. That difference is doing a job — nothing in
   this block reaches the customer, and it should not read like the document. */
.qp-wrap {
  margin: 22px 0 0; padding: 0; border: 1px dashed var(--inv-border-2);
  border-radius: 12px; background: var(--inv-input-bg);
}
.qp-head { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; padding: 12px 14px; }
.qp-fold { display: flex; align-items: center; gap: 8px; background: none; border: 0; padding: 0; cursor: pointer; color: var(--inv-text); }
.qp-fold h3 { margin: 0; font-size: 15px; }
.qp-caret { font-size: 16px; opacity: .6; transition: transform .15s ease; }
.qp-wrap.is-folded .qp-caret { transform: rotate(-90deg); }
.qp-wrap.is-folded .qp-body { display: none; }
.qp-count {
  min-width: 20px; padding: 1px 7px; border-radius: 999px; font-size: 11px; font-weight: 700;
  background: var(--inv-border-2); color: var(--inv-text-2);
}
.qp-count:empty { display: none; }
.qp-headnote { font-size: 11.5px; color: var(--inv-muted); }
.qp-actions { margin-left: auto; display: flex; gap: 14px; align-items: center; flex-wrap: wrap; }
.qp-body { padding: 0 14px 12px; }
.qp-empty { padding: 10px 2px 12px; font-size: 13px; color: var(--inv-muted); line-height: 1.5; }

.qp-row { display: flex; align-items: center; gap: 8px; padding: 5px 0; border-bottom: 1px solid var(--inv-border); }
.qp-row:last-child { border-bottom: 0; }
.qp-row input { background: var(--inv-card); border: 1px solid var(--inv-border); border-radius: 7px; padding: 7px 8px; font-size: 13px; color: var(--inv-text); }
.qp-row .qp-q { width: 66px; flex: none; text-align: right; }
.qp-row .qp-u { width: 52px; flex: none; }
.qp-n { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.qp-n input.qp-name { width: 100%; }
.qp-meta { font-size: 11px; color: var(--inv-muted); }
.qp-cost { flex: none; display: flex; align-items: center; gap: 8px; }
.qp-cost input.qp-c { width: 92px; text-align: right; }
.qp-line { min-width: 76px; text-align: right; font-size: 13px; font-variant-numeric: tabular-nums; color: var(--inv-text-2); }
.qp-nocost { color: var(--inv-muted); }
.qp-x { background: none; border: 0; color: var(--inv-muted); cursor: pointer; font-size: 15px; line-height: 1; padding: 4px; }
.qp-x:hover { color: var(--inv-danger); }
/* Read-only (void / already converted): the same rows without the inputs. */
.qp-row.is-ro { gap: 12px; font-size: 13px; }
.qp-row.is-ro .qp-q { width: 66px; text-align: right; color: var(--inv-text-2); font-variant-numeric: tabular-nums; }
.qp-row.is-ro .qp-c { margin-left: auto; font-variant-numeric: tabular-nums; }

.qp-foot { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; padding: 10px 0 2px; border-top: 1px solid var(--inv-border); margin-top: 4px; }
.qp-foot:empty { display: none; }
.qp-sums { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; font-size: 13px; color: var(--inv-text-2); }
.qp-vs { color: var(--inv-muted); }
.qp-margin { padding: 2px 9px; border-radius: 999px; font-size: 12px; font-weight: 700; }
/* Three tones, not a pass/fail: labour still has to come out of what's left,
   so a thin margin is worth a look rather than an error. */
.qp-margin.is-good { background: rgba(34,197,94,.14); color: #22c55e; }
.qp-margin.is-ok   { background: rgba(245,158,11,.14); color: #f59e0b; }
.qp-margin.is-thin { background: rgba(239,68,68,.14);  color: #ef4444; }
.qp-warn { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; color: var(--inv-muted); }
.qp-savekit { margin-left: auto; }

/* Kit picker (the modal body lives in a .bk-modal). */
.qp-kits { display: flex; flex-direction: column; gap: 6px; max-height: 46vh; overflow-y: auto; }
.qp-kit { display: flex; align-items: stretch; gap: 4px; }
.qp-kit-main {
  flex: 1; min-width: 0; text-align: left; cursor: pointer;
  background: var(--inv-input-bg); border: 1px solid var(--inv-border); border-radius: 9px;
  padding: 10px 12px; display: flex; flex-direction: column; gap: 2px; color: var(--inv-text);
}
.qp-kit-main:hover { border-color: var(--inv-accent); }
.qp-kit-name { font-weight: 600; font-size: 14px; }
.qp-kit-sub { font-size: 12px; color: var(--inv-muted); }
.qp-kit-x { background: none; border: 0; color: var(--inv-muted); cursor: pointer; padding: 0 8px; font-size: 15px; }
.qp-kit-x:hover { color: var(--inv-danger); }

/* On a phone the row wraps to two lines — name across the top, then qty / unit
   / cost beneath it — rather than shrinking five controls into 320px. Same
   16px-minimum inputs as the line-item cards above, so iOS doesn't zoom. */
@media (max-width: 560px) {
  .qp-row { flex-wrap: wrap; padding: 9px 0; }
  .qp-row .qp-n { order: -1; flex: 1 0 100%; }
  .qp-row input { font-size: 16px; padding: 9px 10px; }
  .qp-row .qp-q { width: 74px; }
  .qp-row .qp-u { width: 60px; }
  .qp-cost { margin-left: auto; }
  .qp-cost input.qp-c { width: 84px; }
  .qp-line { min-width: 0; }
  .qp-headnote { flex: 1 0 100%; }
  .qp-actions { margin-left: 0; gap: 12px; }
}

/* Overflow menus (Email ▾ / More ▾) in the editor action bar — core/ui/docactions.js.
   Opens UPWARD so it clears the sticky bottom action bar on mobile. */
.ui-actmenu { position: relative; display: inline-block; }
.ui-actmenu__trigger { display: inline-flex; align-items: center; gap: 6px; }
.ui-actmenu__caret { font-size: 15px; opacity: .7; }
/* max-width is viewport-aware, and a long label WRAPS instead of running out
   past the panel's own edge. With nowrap + a fixed 300px cap, "Send payment
   reminder (auto-reminders off)" drew its tail outside the rounded card and
   off the right of a phone screen — taking the page's horizontal scroll with
   it. A tall menu scrolls inside itself rather than growing off the top. */
.ui-actmenu__panel {
  position: absolute; right: 0; bottom: calc(100% + 6px); z-index: 200;
  /* width: max-content is load-bearing. An absolutely positioned box with
     right: 0 shrink-to-fits against the space from its ANCHOR (a ~100px
     trigger) leftward, so it collapsed to min-content once the labels were
     allowed to wrap. Size to the content, capped by max-width. */
  width: max-content; min-width: 220px;
  max-width: min(320px, calc(100vw - 24px)); padding: 6px;
  box-sizing: border-box;
  max-height: min(58vh, 480px); overflow-y: auto; overscroll-behavior: contain;
  background: var(--inv-card); border: 1px solid var(--inv-border-2);
  border-radius: 12px; box-shadow: var(--inv-card-shadow), 0 12px 32px rgba(0,0,0,.3);
  display: flex; flex-direction: column;
}
.ui-actmenu__panel[hidden] { display: none; }
/* In a TOP toolbar the upward panel opens off the top of the page, so flip it.
   Scoped to .list-actions: the editor's bottom action bar still opens upward. */
.list-actions .ui-actmenu__panel { bottom: auto; top: calc(100% + 6px); }
.list-actions .ui-actmenu__caret { transform: rotate(180deg); }
.list-actions .ui-actmenu__trigger[aria-expanded="true"] .ui-actmenu__caret { transform: none; }
.ui-actmenu__item {
  display: flex; align-items: center; gap: 10px;
  /* No width: 100% — the panel is a flex column, so rows already stretch, and
     a percentage width makes the items contribute nothing to the panel's
     intrinsic width, which collapsed it to its min-width instead of sizing to
     the longest label. */
  padding: 9px 11px; border: 0; border-radius: 8px;
  box-sizing: border-box;
  background: transparent; color: var(--inv-text); text-align: left;
  font-size: 14px; font-weight: 500; cursor: pointer; text-decoration: none;
}
.ui-actmenu__item:hover { background: var(--inv-card-hover); }
.ui-actmenu__item i { font-size: 17px; color: var(--inv-muted); flex: 0 0 auto; }
.ui-actmenu__item--danger { color: var(--inv-danger); }
.ui-actmenu__item--danger i { color: var(--inv-danger); }
.ed-actions .ui-actmenu { flex: 0 0 auto; }
@media (max-width: 640px) {
  .ed-actions .ui-actmenu { flex: 1 1 auto; }
  .ui-actmenu__trigger { width: 100%; justify-content: center; }
  .ui-actmenu__item { padding: 11px 12px; }   /* 44px touch rows */
}
/* On a phone the editor's action bar is a pinned footer whose buttons wrap
   onto two or three rows, so a panel anchored to its own ~100px trigger
   floated somewhere mid-bar and covered the OTHER actions — and the leftmost
   trigger pushed it off the left edge. Neutralising .ui-actmenu's own
   positioning hands the panel to the bar (sticky, so it is the positioned
   ancestor here): one full-width sheet above the whole footer, wherever the
   trigger happens to have wrapped to. Scoped to ≤560px, which is exactly
   where .ed-actions is pinned. */
@media (max-width: 560px) {
  .ed-actions .ui-actmenu { position: static; }
  /* 16px == .ed-actions' own padding: the bar is full-bleed here (negative
     margins), so its padding box is the whole viewport and left/right: 0
     would hang the panel off both edges. */
  .ed-actions .ui-actmenu__panel {
    left: 16px; right: 16px; bottom: calc(100% + 10px);
    width: auto; min-width: 0; max-width: none; max-height: min(52vh, 420px);
  }
}

#flash { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); background: #0b0f17; color: #fff; padding: 10px 20px; border-radius: 8px; font-size: 14px; opacity: 0; pointer-events: none; border: 1px solid var(--inv-border-2); }
/* showToast() (core/toast.js) had NO styles on books pages, so "Copied to
   clipboard" / celebrate labels / omnisearch notices rendered as bare text at
   the document bottom. Dress toasts as the same bottom-centre dark pill as
   #flash so every notification on a books page speaks one language. */
.toast-container { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); display: flex; flex-direction: column; align-items: center; gap: 8px; z-index: var(--z-toast, 1100); pointer-events: none; }
.toast { background: #0b0f17; color: #fff; padding: 10px 20px; border-radius: 8px; font-size: 14px; border: 1px solid var(--inv-border-2); display: flex; align-items: center; gap: 9px; max-width: min(480px, calc(100vw - 32px)); pointer-events: auto; cursor: pointer; opacity: 0; transition: opacity .3s ease, transform .3s ease; }
.toast.visible { opacity: 1; transform: translateY(-6px); }
.toast.exiting { opacity: 0; transform: none; }
.toast i { font-size: 17px; flex-shrink: 0; }
.toast-success i { color: #4ade80; }
.toast-error i   { color: #f87171; }
.toast-warning i { color: #fbbf24; }
.toast-info i    { color: var(--inv-accent); }
.toast-msg { flex: 1; min-width: 0; }
.toast-actions { display: flex; gap: 6px; margin-left: 4px; }
.toast-action { font: inherit; font-size: 13px; font-weight: 600; color: var(--inv-accent); background: none; border: 0; padding: 0 2px; cursor: pointer; white-space: nowrap; }
.toast-action:hover { text-decoration: underline; }
/* z-index above .bk-overlay (60) — a busy state started while a modal is open
   (e.g. the supplier-import write) must show over it, not behind it. */
#busy { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.55); align-items: center; justify-content: center; color: #fff; font-weight: 600; z-index: 70; animation: inv-fade-in .15s ease; }

/* ── neuralbooks app bar ─────────────────────────────────────── */
.books-bar { justify-content: flex-start; gap: 22px; }
.books-back { display: inline-flex; align-items: center; justify-content: center; width: 34px; height: 34px; border-radius: 8px; color: var(--inv-text-2); text-decoration: none; font-size: 18px; margin-right: -8px; transition: background .15s ease, color .15s ease; }
.books-back:hover { background: var(--inv-card-hover); color: var(--inv-text); }
.books-brand { display: inline-flex; align-items: center; gap: 9px; font-size: 17px; letter-spacing: -.02em; color: var(--inv-text); white-space: nowrap; }
.books-brand b { font-weight: 800; }
.books-brand img { display: block; border-radius: 6px; }
.books-tabs { display: flex; align-items: center; gap: 4px; }
.books-tab { color: var(--inv-text-2); text-decoration: none; font-size: 14px; font-weight: 600; padding: 7px 14px; border-radius: 8px; border: 1px solid transparent; transition: background .15s ease, color .15s ease; }
.books-tab:hover { background: var(--inv-card-hover); color: var(--inv-text); }
.books-tab.is-active { background: var(--inv-card); color: var(--inv-text); border-color: var(--inv-border-2); }
@media (max-width: 560px) {
  /* App-style mobile chrome. The top bar is a single row (brand + search + bell +
     theme); the tabs move to a fixed BOTTOM NAV (see the block after the bell
     section). Content is padded so it clears the bottom nav. */
  .books-bar { padding: 9px 12px; gap: 8px; flex-wrap: wrap; align-items: center; }
  .books-back { display: none; }                 /* PWA: reclaim the space */
  .books-brand { order: 1; }
  .books-brand span { display: none; }
  .books-search { order: 2; flex: 1 1 auto; min-width: 0; margin-left: 0; }
  .books-search input, .books-search input:focus { width: 100%; padding-right: 12px; }
  .books-install { order: 3; }
  .books-bell-wrap { order: 4; }
  .books-theme { order: 5; margin-left: 0; }
  .books-signout { order: 6; margin-left: 0; }
  .books-kbd { display: none; }
  /* NB: the bottom-nav, bell-panel and search-results mobile overrides live AFTER
     their base rules (below the bell section) so source order lets them win. */
}
/* Tab icons only appear in the mobile bottom nav. */
.books-tab i.ti { display: none; }

/* Tab badges (unpaid invoices / quotes needing attention). */
.books-badge { display: inline-block; margin-left: 7px; padding: 1px 7px; border-radius: 999px; font-size: 11px; font-weight: 700; background: var(--inv-accent); color: var(--inv-accent-fg); vertical-align: 1px; }

/* Global search (injected by books-bar.js; sits at the right of the bar). */
.books-search { position: relative; margin-left: auto; display: flex; align-items: center; }
.books-search input { width: 200px; padding: 8px 54px 8px 12px; border: 1px solid var(--inv-border-2); border-radius: 8px; background: var(--inv-input-bg); color: var(--inv-text); font-size: 13px; transition: width .15s ease; }
.books-search input:focus { width: 280px; outline: none; border-color: var(--inv-accent); }
.books-search input::placeholder { color: var(--inv-muted); }
.books-kbd { position: absolute; right: 10px; font-family: inherit; font-size: 10px; color: var(--inv-muted); border: 1px solid var(--inv-border-2); border-radius: 4px; padding: 1px 5px; pointer-events: none; white-space: nowrap; }
/* Off Mac the badge reads "Ctrl K" (core/hotkey.js) — wider than ⌘K, so the
   field reserves more room or the typed text runs under it. Scoped above the
   560px break because down there the badge is hidden and the field is flush. */
@media (min-width: 561px) { .nd-notmac .books-search input { padding-right: 68px; } }
.books-search-results { position: absolute; top: calc(100% + 6px); right: 0; width: 340px; max-height: 420px; overflow-y: auto; background: var(--inv-surface); border: 1px solid var(--inv-border-2); border-radius: 10px; box-shadow: 0 10px 30px rgba(0,0,0,.45); z-index: 70; padding: 6px; }
.bsr-label { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: var(--inv-muted); padding: 8px 10px 4px; }
.bsr-row { display: flex; align-items: center; gap: 9px; padding: 8px 10px; border-radius: 7px; text-decoration: none; color: var(--inv-text); font-size: 13px; }
.bsr-row:hover { background: var(--inv-card-hover); }
.bsr-row i { color: var(--inv-muted); font-size: 15px; }
.bsr-row span { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
.bsr-row em { font-style: normal; color: var(--inv-muted); font-size: 12px; white-space: nowrap; }
.bsr-empty { padding: 14px; font-size: 13px; color: var(--inv-muted); text-align: center; }

/* Theme toggle button. */
.books-theme { flex: none; width: 34px; height: 34px; display: inline-flex; align-items: center; justify-content: center; background: none; border: 1px solid transparent; border-radius: 8px; color: var(--inv-text-2); font-size: 17px; cursor: pointer; transition: background .15s ease, color .15s ease; }
.books-theme:hover { background: var(--inv-card-hover); color: var(--inv-text); }

/* Sign-out button (injected by books-bar.js; sits at the far right of the bar). */
.books-signout { flex: none; width: 34px; height: 34px; display: inline-flex; align-items: center; justify-content: center; background: none; border: 1px solid transparent; border-radius: 8px; color: var(--inv-text-2); font-size: 17px; cursor: pointer; transition: background .15s ease, color .15s ease; }
.books-signout:hover { background: rgba(239,68,68,0.12); color: #ef4444; }

/* ── Install button (PWA; shown only when installable) ─────────── */
.books-install { flex: none; display: inline-flex; align-items: center; gap: 5px; height: 34px; padding: 0 12px; border-radius: 8px;
  background: var(--inv-card-hover); color: var(--inv-text); border: 1px solid var(--inv-border-2); font: inherit; font-size: 13px; font-weight: 600; cursor: pointer; transition: background .15s ease; }
.books-install:hover { background: var(--inv-accent); color: var(--inv-accent-fg); border-color: transparent; }
.books-install i { font-size: 15px; }

/* ── Notification bell + activity panel ────────────────────────── */
.books-bell-wrap { position: relative; flex: none; }
.books-bell { position: relative; width: 34px; height: 34px; display: inline-flex; align-items: center; justify-content: center;
  background: none; border: 1px solid transparent; border-radius: 8px; color: var(--inv-text-2); font-size: 17px; cursor: pointer; transition: background .15s ease, color .15s ease; }
.books-bell:hover { background: var(--inv-card-hover); color: var(--inv-text); }
.books-bell-dot { position: absolute; top: 2px; right: 2px; min-width: 16px; height: 16px; padding: 0 4px; box-sizing: border-box;
  display: inline-flex; align-items: center; justify-content: center; background: var(--inv-danger, #dc2626); color: #fff;
  font-size: 10px; font-weight: 700; line-height: 1; border-radius: 999px; border: 2px solid var(--inv-bg); }
.books-bell-panel { position: absolute; top: 42px; right: 0; width: 320px; max-width: 86vw; max-height: 60vh; overflow-y: auto; z-index: 200;
  background: var(--inv-card); border: 1px solid var(--inv-border-2); border-radius: 12px; box-shadow: 0 16px 44px rgba(0,0,0,.4); padding: 6px; animation: bfx-pop .16s ease both; }
.bell-row { display: grid; grid-template-columns: 20px 1fr auto; align-items: start; gap: 9px; padding: 9px 10px; border-radius: 9px;
  text-decoration: none; color: var(--inv-text); font-size: 13px; line-height: 1.4; }
.bell-row:hover { background: var(--inv-card-hover); }
.bell-row i { font-size: 16px; margin-top: 1px; color: var(--inv-muted); }
.bell-row.warn i { color: var(--warn); } .bell-row.good i { color: var(--ok); } .bell-row.info i { color: #1493e6; }
.bell-row em { font-style: normal; color: var(--inv-muted); font-size: 11px; white-space: nowrap; }
.bell-empty { display: flex; align-items: center; gap: 7px; padding: 18px 12px; color: var(--inv-muted); font-size: 13px; justify-content: center; }
.bell-empty i { color: var(--ok); font-size: 16px; }
.bell-notif { display: flex; align-items: center; gap: 8px; padding: 9px 10px; margin-bottom: 4px; border-radius: 9px;
  background: var(--inv-input-bg); font-size: 12.5px; color: var(--inv-text-2); }
.bell-notif > span { flex: 1; display: inline-flex; align-items: center; gap: 6px; }
.bell-notif i { color: var(--inv-accent); font-size: 15px; }
.bell-notif-btn { flex: none; font: inherit; font-size: 12px; font-weight: 600; border-radius: 7px; padding: 5px 10px; cursor: pointer;
  background: transparent; color: var(--inv-text-2); border: 1px solid var(--inv-border-2); }
.bell-notif-btn.primary { background: var(--inv-accent); color: var(--inv-accent-fg); border-color: transparent; }
.bell-notif-btn:disabled { opacity: .6; cursor: default; }
@media (max-width: 560px) { .books-install span, .books-install { font-size: 0; gap: 0; padding: 0 10px; } .books-install i { font-size: 17px; } }

/* Mobile: keep the search results on-screen and turn the activity panel into a
   bottom sheet. Placed AFTER the base .books-search-results / .books-bell-panel
   rules so these win on source order (equal specificity). */
@media (max-width: 560px) {
  .books-search-results { width: min(360px, calc(100vw - 20px)); }
  .books-bell-panel { position: fixed; left: 0; right: 0; bottom: 0; top: auto; width: auto; max-width: none;
    max-height: 72vh; border-radius: 16px 16px 0 0; padding: 8px 10px calc(10px + env(safe-area-inset-bottom));
    box-shadow: 0 -14px 40px rgba(0, 0, 0, .5); animation: bell-sheet-up .22s ease both; }
  .books-bell-panel::before { content: ''; display: block; width: 36px; height: 4px; border-radius: 999px;
    background: var(--inv-border-2); margin: 2px auto 8px; }
}
@keyframes bell-sheet-up { from { transform: translateY(100%); } to { transform: none; } }
@media (prefers-reduced-motion: reduce) { .books-bell-panel { animation: none; } }

/* Mobile bottom navigation — the tabs become a fixed, icon+label bottom bar
   (native-app feel). Placed after the base .books-tabs/.books-tab rules so these
   win on source order. Bottom-anchored UI (editor action bar, bulk bar, flash)
   is lifted to clear the ~58px nav, and the page gains matching bottom padding. */
:root { --books-navh: 58px; }
@media (max-width: 560px) {
  body { padding-bottom: calc(var(--books-navh) + env(safe-area-inset-bottom)); }
  .books-tabs { position: fixed; left: 0; right: 0; bottom: 0; z-index: 90;
    display: flex; justify-content: space-around; align-items: stretch; gap: 0;
    background: var(--inv-surface); border-top: 1px solid var(--inv-border);
    padding: 6px 4px calc(4px + env(safe-area-inset-bottom)); box-shadow: 0 -6px 20px rgba(0, 0, 0, .28); }
  .books-tab { position: relative; flex: 1 1 0; display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 3px; padding: 4px 2px; font-size: 11px; font-weight: 600; line-height: 1.1; border: 0; border-radius: 10px;
    color: var(--inv-text-2); background: none; text-align: center; }
  .books-tab i.ti { display: block; font-size: 21px; }
  .books-tab.is-active { color: var(--inv-accent); background: none; border-color: transparent; }
  .books-tab:hover { background: none; }
  /* Badge as a dot on the icon corner rather than an inline pill. */
  .books-badge { position: absolute; top: 0; left: 58%; margin: 0; min-width: 16px; padding: 1px 5px;
    box-shadow: 0 0 0 2px var(--inv-surface); }
  /* Lift bottom-anchored chrome above the nav. */
  .ed-actions { bottom: calc(var(--books-navh) + env(safe-area-inset-bottom)); }
  .bulk-bar { bottom: calc(var(--books-navh) + 14px + env(safe-area-inset-bottom)); }
  #flash { bottom: calc(var(--books-navh) + 16px + env(safe-area-inset-bottom)); }
  .toast-container { bottom: calc(var(--books-navh) + 16px + env(safe-area-inset-bottom)); }
  /* The activity sheet sits above the nav (its own scrim covers it). */
  .books-bell-panel { z-index: 210; }
}

/* ── Light theme (books-bar toggle; pre-paint script sets the attr) ──
   Surface/text/accent/border/danger auto-flip via the shared --app-* light
   values in tokens.css (--inv-* reference them in :root above). Only the
   book-local hovers + card shadow need a light override here. */
:root[data-theme="light"] {
  --inv-accent-h: #0f76bf;
  --inv-link-h: #0f76bf;
  --inv-card-shadow: 0 2px 10px rgba(15,23,42,.07);
  /* The status pastels are dark-theme values: #fbbf24 is ~1.67:1 on white and
     #4ade80 ~1.74:1. They are used as FOREGROUND on ~20 surfaces that carry
     real numbers — .cust-owing is the amount a customer owes you, the headline
     figure on the Customers page. Correcting the tokens fixes every consumer at
     once; the per-selector overrides below predate this and are now redundant
     but harmless. */
  --ok: #15803d; --ok-soft: #15803d; --warn: #b45309; --warn-soft: #b45309;
}
:root[data-theme="light"] .btn-ghost { background: #e2e8f0; }
:root[data-theme="light"] .btn-ghost:hover { background: #cbd5e1; }
:root[data-theme="light"] #flash { background: #0f172a; }
/* The toast pill stays dark in light theme (like #flash), so pin the accent
   pieces to the bright dark-theme blue — the light accent is too dim on it. */
:root[data-theme="light"] .toast { background: #0f172a; }
:root[data-theme="light"] .toast-info i, :root[data-theme="light"] .toast-action { color: #33b1ff; }
:root[data-theme="light"] .stat-ok .stat-val { color: #15803d; }
:root[data-theme="light"] .books-search-results { box-shadow: 0 10px 30px rgba(15,23,42,.18); }
/* The tinted banners carry dark-theme pastels — deepen them for light bg. */
:root[data-theme="light"] .sb-stripe { color: #4338ca; }
:root[data-theme="light"] .sb-xero { color: #0e7490; }
:root[data-theme="light"] .sb-stripe strong, :root[data-theme="light"] .sb-xero strong { color: #0f172a; }
:root[data-theme="light"] .sb-warn { color: #92400e; }
:root[data-theme="light"] .sb-ok, :root[data-theme="light"] .sb-chip,
:root[data-theme="light"] .sb-chip-body-inner { color: #15803d; }
:root[data-theme="light"] .warn { color: #92400e; }

/* ── Invoice status timeline (editor) ───────────────────────── */
.tl { display: flex; align-items: flex-start; margin: 0 0 16px; }
.tl-step { flex: 1; position: relative; text-align: center; font-size: 12px; }
.tl-step::before { content: ''; position: absolute; top: 6px; left: -50%; width: 100%; height: 2px; background: var(--inv-border-2); }
.tl-step:first-child::before { display: none; }
.tl-dot { position: relative; display: inline-block; width: 13px; height: 13px; border-radius: 50%; background: var(--inv-card); border: 2px solid var(--inv-border-2); z-index: 1; }
.tl-step.done .tl-dot { background: var(--ok); border-color: var(--ok); }
.tl-step.warn .tl-dot { background: #ef4444; border-color: #ef4444; }
.tl-step.done::before { background: var(--ok); }
.tl-step.warn::before { background: #ef4444; }
.tl-label { display: block; margin-top: 5px; font-weight: 600; color: var(--inv-text-2); }
.tl-step.done .tl-label { color: var(--inv-text); }
.tl-step.warn .tl-label { color: var(--inv-danger); }
.tl-date { display: block; color: var(--inv-muted); font-size: 11px; margin-top: 1px; }

/* ── Line-item drag reorder ─────────────────────────────────── */
.dragcell { width: 22px; }
.draghandle { cursor: grab; color: var(--inv-muted); font-size: 13px; letter-spacing: -1px; user-select: none; }
.draghandle:hover { color: var(--inv-text-2); }
tr.drag-over td { border-top: 2px solid var(--inv-accent); }

/* ── List toolbar extras (sort + date range) ────────────────── */
.list-actions select.inv-search, .list-actions input[type="date"].inv-search { width: auto; }

/* ── List toolbar on mobile ──────────────────────────────────
   The shared list toolbar (invoices / quotes / customers / catalog /
   suppliers / purchase-orders) is a flex-wrap pile-up on a phone —
   date pickers, sort, search and a wall of buttons all fighting for the
   same row. Below, we stack the heading over the controls and lay the
   controls out on a tidy 2-column grid with a clear priority order:
   primary action first (full width), then search, then the date range
   sharing a row, then sort, then the ghost buttons flowing two-up. */
@media (max-width: 720px) {
  .list-head { flex-direction: column; align-items: stretch; gap: 12px; }
  .list-head h2 { font-size: 20px; }
  .list-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; width: 100%; }
  .list-actions > input, .list-actions > select, .list-actions > button { width: 100%; margin: 0; box-sizing: border-box; }
  .list-actions .btn-primary { grid-column: 1 / -1; order: 1; padding: 12px; font-size: 15px; }
  .list-actions input[type="search"] { grid-column: 1 / -1; order: 2; }
  .list-actions input[type="date"] { order: 3; }              /* the two dates share a row */
  .list-actions select.inv-search { grid-column: 1 / -1; order: 4; }
  .list-actions .btn-ghost { order: 5; }                      /* secondary buttons flow two-up */
}

/* ── First-run checklist (Overview) ─────────────────────────── */
.ck-list { display: flex; flex-direction: column; gap: 8px; }
.ck-item { display: flex; align-items: center; gap: 10px; font-size: 13px; color: var(--inv-text-2); }
.ck-item i { font-size: 16px; }
.ck-item.done { color: var(--inv-muted); text-decoration: line-through; }
.ck-item.done i { color: var(--ok); }
.ck-item a { color: var(--inv-link); text-decoration: none; font-weight: 600; }
.ck-item a:hover { color: var(--inv-link-h); }

/* Clickable Overview rows (aging buckets / top customers). */
a.ov-row { text-decoration: none; color: inherit; border-radius: 6px; }
a.ov-row:hover { background: var(--inv-card-hover); }

/* ── Overview tab ────────────────────────────────────────────── */
.ov-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 14px; }
.ov-card { background: var(--inv-card); border-radius: 12px; box-shadow: var(--inv-card-shadow); padding: 18px 20px; animation: inv-fade-up .5s ease both; }
.ov-wide { grid-column: 1 / -1; }
.ov-title { font-size: 12px; font-weight: 600; color: var(--inv-text-2); text-transform: uppercase; letter-spacing: .04em; margin-bottom: 12px; }
.ov-big { font-size: 26px; font-weight: 700; letter-spacing: -.01em; margin-bottom: 10px; }
.ov-sub { font-size: 13px; font-weight: 500; color: var(--inv-muted); }
.ov-line { font-size: 13px; color: var(--inv-text-2); margin-top: 4px; }
.ov-empty { font-size: 13px; color: var(--inv-muted); padding: 8px 0; }
/* "Couldn't load" stands alone across the grid — it replaces every card rather
   than sitting beside figures that would contradict it. */
.ov-loaderr { grid-column: 1 / -1; }
.ov-row { display: flex; align-items: center; gap: 10px; padding: 4px 0; font-size: 13px; }
.ov-row-label { flex: 0 0 116px; color: var(--inv-text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ov-bar { flex: 1; height: 8px; background: var(--inv-input-bg); border-radius: 4px; overflow: hidden; }
.ov-bar-fill { display: block; height: 100%; border-radius: 4px; }
.ov-row-val { flex: 0 0 auto; min-width: 62px; text-align: right; font-weight: 600; color: var(--inv-text); }
.ov-table { width: 100%; border-collapse: collapse; }
.ov-table th { text-align: left; font-size: 12px; color: var(--inv-muted); font-weight: 600; padding: 4px 12px 6px 0; border-bottom: 1px solid var(--inv-border); }
.ov-table td { font-size: 13px; color: var(--inv-text-2); padding: 6px 12px 6px 0; border-bottom: 1px solid var(--inv-border); }
.ov-table tr:last-child td { border-bottom: 0; }
/* Accrual/Cash basis toggle on the GST summary card. */
.ov-seg { display: inline-flex; gap: 2px; padding: 2px; background: var(--inv-input-bg); border-radius: 8px; }
.ov-seg-btn { font-size: 12px; font-weight: 600; color: var(--inv-text-2); background: transparent; border: 0; border-radius: 6px; padding: 4px 12px; cursor: pointer; }
.ov-seg-btn.is-on { background: var(--inv-card); color: var(--inv-text); box-shadow: 0 1px 2px rgba(0,0,0,.08); }

/* ── AI insights card ────────────────────────────────────────── */
.ov-ai { position: relative; overflow: hidden; }
.ov-ai::before { content: ''; position: absolute; inset: 0; padding: 1px; border-radius: 12px; pointer-events: none;
  background: var(--inv-accent);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); -webkit-mask-composite: xor; mask-composite: exclude; opacity: .28; }
.ov-ai .ov-title { display: flex; align-items: center; gap: 6px; }
.ov-ai .ov-title i.ti-sparkles { color: var(--inv-accent); font-size: 14px; text-transform: none; }
.ov-ai-refresh { margin-left: auto; background: transparent; border: 0; color: var(--inv-muted); cursor: pointer; padding: 2px 6px; border-radius: 6px; line-height: 1; }
.ov-ai-refresh:hover { color: var(--inv-text); background: var(--inv-input-bg); }
.ov-ai-body { display: flex; flex-direction: column; gap: 8px; }
.ov-ai-item { display: flex; align-items: flex-start; gap: 9px; font-size: 13.5px; line-height: 1.45; color: var(--inv-text); }
.ov-ai-item i { flex: 0 0 auto; margin-top: 2px; font-size: 15px; color: var(--inv-muted); }
.ov-ai-item.good i { color: var(--ok); }
.ov-ai-item.warn i { color: var(--warn); }
.ov-ai-item.info i { color: #1493e6; }
.ov-ai-loading { display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--inv-muted); }
.ov-ai-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; opacity: .35; animation: ov-ai-bounce 1.1s infinite ease-in-out; }
.ov-ai-dot:nth-child(2) { animation-delay: .16s; } .ov-ai-dot:nth-child(3) { animation-delay: .32s; }
@keyframes ov-ai-bounce { 0%, 80%, 100% { opacity: .3; transform: translateY(0); } 40% { opacity: .9; transform: translateY(-3px); } }
.ov-ai-err { font-size: 13px; color: var(--inv-text-2); display: flex; align-items: center; gap: 6px; }
.ov-linklike { background: none; border: 0; color: var(--inv-link); cursor: pointer; padding: 0; font: inherit; text-decoration: underline; }
@media (prefers-reduced-motion: reduce) { .ov-ai-dot { animation: none; } }

/* ── AI modal (books-fx promptBox / resultBox) ───────────────── */
.bfx-back { position: fixed; inset: 0; z-index: 1000; display: flex; align-items: center; justify-content: center; padding: 20px;
  background: rgba(15, 23, 42, .55); backdrop-filter: blur(2px); animation: bfx-fade .18s ease both; }
.bfx-modal { width: 100%; max-width: 520px; background: var(--inv-card); color: var(--inv-text); border-radius: 14px;
  box-shadow: 0 20px 60px rgba(0, 0, 0, .35); padding: 20px; animation: bfx-pop .2s cubic-bezier(.2, .9, .3, 1.2) both;
  border: 1px solid var(--inv-border); }
.bfx-title { display: flex; align-items: center; gap: 8px; font-size: 16px; font-weight: 700; margin-bottom: 6px; }
.bfx-title i.ti-sparkles { color: var(--inv-accent); }
.bfx-hint { font-size: 13px; color: var(--inv-muted); margin-bottom: 12px; line-height: 1.45; }
.bfx-ta { width: 100%; box-sizing: border-box; resize: vertical; font: inherit; font-size: 14px; line-height: 1.5;
  background: var(--inv-input-bg); color: var(--inv-text); border: 1px solid var(--inv-border); border-radius: 9px; padding: 10px 12px; }
.bfx-ta:focus { outline: none; border-color: var(--inv-accent); box-shadow: 0 0 0 3px var(--nd-accent-soft); }
.bfx-actions { display: flex; align-items: center; gap: 8px; margin-top: 14px; }
.bfx-ghost, .bfx-primary { font: inherit; font-size: 14px; font-weight: 600; border-radius: 9px; padding: 9px 16px; cursor: pointer; border: 1px solid transparent; display: inline-flex; align-items: center; gap: 6px; }
.bfx-ghost { background: transparent; color: var(--inv-text-2); border-color: var(--inv-border); }
.bfx-ghost:hover { background: var(--inv-input-bg); }
.bfx-primary { background: var(--inv-accent); color: var(--inv-accent-fg); }
.bfx-primary:hover { background: var(--inv-accent-h); }
.bfx-primary:disabled, .bfx-ghost:disabled { opacity: .6; cursor: default; }
@keyframes bfx-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes bfx-pop { from { opacity: 0; transform: translateY(8px) scale(.98); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .bfx-back, .bfx-modal { animation: none; } }
/* Sparkle accent on AI affordances */
.ai-link, .ai-btn { display: inline-flex; align-items: center; gap: 5px; }
.ai-link i.ti-sparkles, .ai-btn i.ti-sparkles { color: var(--inv-accent); }

/* ── Customers view ──────────────────────────────────────────── */
.cust-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 14px; }
.cust-card { background: var(--inv-card); border-radius: 12px; box-shadow: var(--inv-card-shadow); padding: 16px 18px; animation: inv-fade-up .5s ease both; }
.cust-head { display: flex; align-items: flex-start; gap: 8px; }
.cust-head > div { flex: 1; min-width: 0; }
.cust-name { font-weight: 600; font-size: 15px; overflow-wrap: anywhere; }
.cust-meta { font-size: 12px; color: var(--inv-muted); margin-top: 3px; overflow-wrap: anywhere; }
.cust-meta i { font-size: 11px; margin-right: 3px; }
.cust-notes { font-style: italic; }
.cust-empty { grid-column: 1 / -1; text-align: center; padding: 40px 16px; color: var(--inv-muted); font-size: 13px; }

/* ── Shared list zero-state (core/ui/doclist.js empty()) ─────────────────────
   One designed empty state for every doc list, so a first-run Invoices or
   Quotes page offers a way forward instead of a single line of grey text. */
.nd-empty { display: flex; flex-direction: column; align-items: center; gap: 10px; padding: 48px 20px; text-align: center; }
.nd-empty > i { font-size: 30px; line-height: 1; color: var(--inv-muted); opacity: .75; }
.nd-empty-title { margin: 0; font-size: 15px; font-weight: 600; color: var(--inv-text); }
.nd-empty-body { margin: 0; max-width: 42ch; font-size: 13px; line-height: 1.5; color: var(--inv-muted); }
.nd-empty .btn-primary { margin-top: 6px; }
.btn-sm { padding: 6px 10px; font-size: 12px; border-radius: 7px; }

/* Whole-card click opens the activity/edit modal. */
.cust-card.is-click { cursor: pointer; transition: transform .12s ease, box-shadow .12s ease, border-color .12s ease; border: 1px solid transparent; }
.cust-card.is-click:hover { transform: translateY(-2px); box-shadow: 0 8px 22px rgba(0,0,0,.32); border-color: var(--inv-border-2); }
.cust-card.is-click:focus-visible { outline: 2px solid var(--inv-accent); outline-offset: 2px; }
.cust-unsaved { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; color: var(--inv-muted); background: var(--inv-input-bg); border: 1px solid var(--inv-border-2); border-radius: 999px; padding: 1px 7px; vertical-align: 2px; margin-left: 4px; }

/* Money line — outstanding balance leads (the reason to open this page). */
.cust-money { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; margin-top: 10px; font-size: 13px; }
.cust-owing { font-weight: 700; font-size: 15px; color: var(--warn-soft); }
.cust-clear { font-weight: 600; color: var(--ok-soft); }
.cust-none { color: var(--inv-muted); }
.cust-last { color: var(--inv-muted); font-size: 12px; }

/* Fixed metric chip strip — same four on every card so heights match. */
.cust-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
.cchip { font-size: 12px; color: var(--inv-text-2); background: var(--inv-input-bg); border: 1px solid var(--inv-border); border-radius: 7px; padding: 3px 8px; white-space: nowrap; }
.cchip em { font-style: normal; color: var(--ok-soft); }
.cchip.is-zero { color: var(--inv-muted); opacity: .5; }

/* ── Price catalog (catalog.html) ───────────────────────────── */
.cat-intro { color: var(--inv-muted); font-size: 13px; margin: -4px 0 14px; max-width: 620px; }
.cat-cats { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; }
.cat-chip { display: inline-flex; align-items: center; min-height: 36px; background: var(--inv-card); color: var(--inv-text-2); border: 1px solid var(--inv-border); border-radius: 999px; padding: 6px 13px; font-size: 13px; font-weight: 600; cursor: pointer; transition: background .12s ease, border-color .12s ease, color .12s ease; }
.cat-chip:hover { border-color: var(--inv-border-2); }
.cat-chip.is-active { background: var(--inv-accent); border-color: var(--inv-accent); color: var(--inv-accent-fg); }
.cat-chip-n { opacity: .6; font-weight: 500; margin-left: 3px; }
.cat-chip.is-active .cat-chip-n { opacity: .85; }

/* Count + window state, above the grid rather than below 300 cards. */
.cat-count { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: 0 0 12px; font-size: 13px; color: var(--inv-text-2); }
.cat-count:empty { display: none; }
.cat-count b { color: var(--inv-text); font-weight: 700; font-variant-numeric: tabular-nums; }
.cat-count-clear { background: none; border: 0; color: var(--inv-accent); font-size: 12.5px; font-weight: 600; cursor: pointer; padding: 5px 7px; border-radius: 6px; display: inline-flex; align-items: center; gap: 4px; }
.cat-count-clear:hover { background: var(--inv-accent-soft, rgba(51,177,255,.12)); }
.cat-count-clear i { font-size: 13px; }
.cat-count-right { margin-left: auto; display: flex; align-items: center; gap: 8px; }
.cat-window { display: inline-flex; align-items: center; gap: 6px; border: 1px solid var(--warn); color: var(--warn-soft); border-radius: 999px; padding: 3px 11px; font-size: 11.5px; font-weight: 600; }
.cat-window i { font-size: 13px; }
.cat-loading { display: inline-flex; align-items: center; gap: 6px; border: 1px solid var(--inv-accent); color: var(--inv-accent); border-radius: 999px; padding: 3px 11px; font-size: 11.5px; font-weight: 600; }
.cat-loading i { font-size: 13px; animation: cat-spin 1s linear infinite; }
@keyframes cat-spin { to { transform: rotate(360deg); } }

/* Placeholder cards while the first page of the catalog is in flight — the
   grid has its eventual shape before any data lands. */
.cat-sk { animation: none; pointer-events: none; }
.cat-sk-line { height: 12px; border-radius: 5px; background: var(--inv-input-bg); position: relative; overflow: hidden; margin-bottom: 9px; }
.cat-sk-line:last-child { margin-bottom: 0; }
.cat-sk-sm { height: 9px; }
.cat-sk-line::after {
  content: ''; position: absolute; inset: 0; transform: translateX(-100%);
  background: linear-gradient(90deg, transparent, var(--inv-card-hover), transparent);
  animation: cat-sk 1.25s ease-in-out infinite;
}
@keyframes cat-sk { to { transform: translateX(100%); } }
@media (prefers-reduced-motion: reduce) {
  .cat-sk-line::after { animation: none; }
  .cat-loading i { animation: none; }
}
@media (max-width: 640px) {
  .cat-count-right { margin-left: 0; width: 100%; }
  .cat-window { white-space: normal; }
}

/* ── Catalog row view (feature_catalog_v2, js/books/catalog-rows.js) ────────
   Rows instead of cards: prices right-aligned in one scannable column, and a
   spec line saying what the item is. Only the rows near the viewport exist in
   the DOM — .catv2-vport carries the full height so the scrollbar is honest,
   .catv2-vwin holds the painted slice.
   Namespaced .catv2-* on purpose: the old grid borrows .cust-* from the
   Customers page, and restyling those would break Customers. */
/* Supplier + data-quality facets (catalog-page.js renderExtraFacets). */
.cat-facets { display: flex; flex-wrap: wrap; gap: 8px; margin: -8px 0 16px; }
.cat-facets:empty { display: none; }
.cat-facet {
  width: auto; max-width: 100%;   /* the base `select { width: 100% }` would stretch each to a full row */
  min-height: 34px; padding: 6px 11px; border: 1px solid var(--inv-border);
  border-radius: 999px; background: var(--inv-card); color: var(--inv-text-2);
  font-family: inherit; font-size: 12.5px; font-weight: 600; cursor: pointer; max-width: 100%;
}
.cat-facet:hover { border-color: var(--inv-border-2); }
.cat-facet.is-set { border-color: var(--inv-accent); color: var(--inv-accent); }
.cat-facet-flag.is-set { border-color: var(--warn); color: var(--warn-soft); }

/* No-results: a dead end is the most annoying moment on the page. */
.cat-noresults b { color: var(--inv-text); }
.cat-noresults-acts { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; margin-top: 14px; }
.cat-noresults-acts .btn-ghost, .cat-noresults-acts .btn-primary { min-height: 44px; }

/* Bulk-action bar — appears once anything is ticked in the row view. */
.cat-selbar {
  position: fixed; left: 50%; bottom: 18px; z-index: 70;
  transform: translateX(-50%) translateY(150%);
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  max-width: calc(100vw - 24px);
  background: var(--inv-card); border: 1px solid var(--inv-border-2);
  border-radius: 14px; box-shadow: var(--inv-card-shadow), 0 16px 40px rgba(0,0,0,.42);
  padding: 9px 12px; transition: transform .18s ease;
}
.cat-selbar.is-on { transform: translateX(-50%) translateY(0); }
.cat-selbar-n { font-size: 13px; font-weight: 600; white-space: nowrap; }
.cat-selbar-n b { color: var(--inv-accent); font-variant-numeric: tabular-nums; }
.cat-selbar-locked { font-size: 11.5px; color: var(--warn-soft); white-space: nowrap; }
.cat-selbar-sep { width: 1px; height: 22px; background: var(--inv-border); }
.cat-selbar select { width: auto; min-width: 150px; }
.cat-bulk-markup { display: inline-flex; align-items: center; gap: 4px; font-size: 12.5px; color: var(--inv-text-2); }
.cat-bulk-markup input {
  width: 56px; padding: 5px 7px; font-size: 13px; text-align: right;
  -moz-appearance: textfield; appearance: textfield;
}
.cat-bulk-markup input::-webkit-outer-spin-button,
.cat-bulk-markup input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
@media (max-width: 760px) {
  .cat-selbar { left: 12px; right: 12px; bottom: 12px; max-width: none; transform: translateY(150%); }
  .cat-selbar.is-on { transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) { .cat-selbar { transition: none; } }

/* Row tick — 44px target on a 20px box, per the touch-target floor. */
.catv2-tickwrap { display: grid; place-items: center; min-width: 34px; min-height: 44px; cursor: pointer; }
.catv2-tick { width: 18px; height: 18px; accent-color: var(--inv-accent); cursor: pointer; }
.catv2-row.is-sel { background: var(--inv-accent-soft, rgba(51,177,255,.12)); }

/* The host is .cust-grid; in row mode it stops being a grid. */
.cust-grid.cat-rows-host { display: block; }
/* Same job for the comparison. #catGrid is .cust-grid — an auto-fill grid of
   280px card cells — and a view that renders its own block layout into it must
   say so, or its stats band and its card list become two cells of somebody
   else's card grid. Shipped without this and that is exactly what happened. */
.cust-grid.cat-cmp-host { display: block; }
.cat-viewtoggle { display: inline-flex; gap: 2px; background: var(--inv-input-bg); border: 1px solid var(--inv-border); border-radius: 8px; padding: 2px; }
.cat-viewtoggle button {
  display: inline-flex; align-items: center; gap: 6px; min-height: 34px; padding: 0 11px;
  border: 0; border-radius: 6px; background: none; color: var(--inv-muted);
  font-family: inherit; font-size: 13px; cursor: pointer;
}
.cat-viewtoggle button.is-active { background: var(--inv-card); color: var(--inv-text); }
.cat-viewtoggle button i { font-size: 15px; }

.catv2-list { border: 1px solid var(--inv-border); border-radius: 12px; background: var(--inv-card); }
.catv2-head, .catv2-row {
  display: grid;
  grid-template-columns: 34px 34px minmax(220px, 1fr) 112px 104px 88px 88px 100px 84px;
  align-items: center; gap: 10px; padding: 0 12px;
}
.catv2-head {
  position: sticky; top: var(--catv2-stick, 0px); z-index: 5;
  height: 38px; background: var(--inv-input-bg);
  border-bottom: 1px solid var(--inv-border); border-radius: 11px 11px 0 0;
  font-size: 10.5px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--inv-muted);
}
.catv2-head button {
  background: none; border: 0; color: inherit; font: inherit; letter-spacing: inherit;
  text-transform: inherit; cursor: pointer; padding: 6px 0; min-height: 34px;
  display: inline-flex; align-items: center; gap: 4px;
}
.catv2-head button:hover { color: var(--inv-text-2); }
.catv2-head button[aria-sort="ascending"], .catv2-head button[aria-sort="descending"] { color: var(--inv-accent); }
.catv2-head button i { font-size: 12px; }
.catv2-head .catv2-n button { justify-content: flex-end; width: 100%; }

/* NO overflow:hidden on .catv2-vport's ancestors — it would capture the sticky
   header and pin it inside the table instead of below the page chrome. */
.catv2-vport { position: relative; }
.catv2-vwin { position: absolute; top: 0; left: 0; right: 0; will-change: transform; }

/* EVERY row keeps its border and its min-height. No :last-child exception —
   under virtualisation "last child" is the last PAINTED row, not the last item,
   so it would silently shorten an arbitrary row by 1px and desynchronise the
   window maths from what is on screen. */
.catv2-row { min-height: 52px; border-bottom: 1px solid var(--inv-border); cursor: pointer; }
.catv2-row:hover { background: var(--inv-card-hover); }
.catv2-row:focus-visible { outline: 2px solid var(--inv-accent); outline-offset: -2px; }

.catv2-thumb {
  width: 32px; height: 32px; border-radius: 7px; border: 1px solid var(--inv-border);
  background: var(--inv-input-bg); display: grid; place-items: center;
  color: var(--inv-muted); font-size: 15px; overflow: hidden;
}
.catv2-thumb img { width: 100%; height: 100%; object-fit: cover; }
.catv2-main { min-width: 0; display: block; }
.catv2-name { display: block; font-size: 13.5px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.catv2-badge {
  font-size: 9.5px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  color: var(--inv-accent); border: 1px solid var(--inv-accent); border-radius: 999px;
  padding: 0 6px; margin-left: 6px; vertical-align: 1px;
}
.catv2-badge i { font-size: 10px; }

/* Spec line — replaces a subtitle that repeated the Supplier and Part # columns
   sitting beside it. Clips rather than wraps: the virtualiser needs every row
   the same height. */
.catv2-spec { display: flex; align-items: center; gap: 5px; margin-top: 2px; white-space: nowrap; overflow: hidden; }
.catv2-sp {
  flex: none; font-size: 10.5px; font-weight: 600; color: var(--inv-text-2);
  background: var(--inv-input-bg); border: 1px solid var(--inv-border);
  border-radius: 5px; padding: 1px 6px; font-variant-numeric: tabular-nums;
}
.catv2-sp-cat { display: inline-flex; align-items: center; gap: 4px; color: var(--inv-muted); background: none; border-color: transparent; padding-left: 0; font-weight: 500; }
.catv2-sp-cat i { font-size: 12px; }
.catv2-sp-path { min-width: 0; font-size: 11px; color: var(--inv-muted); overflow: hidden; text-overflow: ellipsis; }
.catv2-sp-path::before { content: '·'; margin-right: 5px; opacity: .6; }

.catv2-cell { font-size: 12.5px; color: var(--inv-text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.catv2-n { font-variant-numeric: tabular-nums; font-size: 13px; text-align: right; }
.catv2-c-cost.is-missing, .catv2-c-rrp.is-missing { color: var(--inv-muted); opacity: .5; }
.catv2-c-rrp { color: var(--inv-text-2); font-size: 12.5px; }
.catv2-sell { font-weight: 700; font-size: 13.5px; color: var(--inv-text); }
.catv2-sell small { display: block; font-size: 10.5px; font-weight: 500; color: var(--inv-muted); }

/* Margin: the number and its sign carry the meaning, so colour is reinforcement
   rather than the signal (WCAG "not by colour alone"). */
.catv2-m { display: inline-block; font-variant-numeric: tabular-nums; font-size: 11.5px; font-weight: 700; border-radius: 999px; padding: 2px 8px; border: 1px solid; }
.catv2-m.is-good { color: var(--ok-soft); border-color: var(--ok); }
.catv2-m.is-mid  { color: var(--inv-text-2); border-color: var(--inv-border-2); }
.catv2-m.is-thin { color: var(--warn-soft); border-color: var(--warn); }
.catv2-m.is-loss { color: var(--inv-danger); border-color: var(--inv-danger); }
.catv2-m.is-none { color: var(--inv-muted); border-color: transparent; font-weight: 500; }

/* Columns shed least-useful-first. The spec line never goes — it is the part
   that says what the item is. */
@media (max-width: 1240px) {
  .catv2-head, .catv2-row { grid-template-columns: 34px 34px minmax(200px, 1fr) 112px 88px 88px 100px 84px; }
  .catv2-c-part { display: none; }
}
@media (max-width: 1060px) {
  .catv2-head, .catv2-row { grid-template-columns: 34px 34px minmax(180px, 1fr) 88px 88px 100px 84px; }
  .catv2-c-sup { display: none; }
}
@media (max-width: 900px) {
  .catv2-head, .catv2-row { grid-template-columns: 34px 34px minmax(160px, 1fr) 88px 100px 84px; }
  .catv2-c-rrp { display: none; }
}
@media (max-width: 700px) {
  .catv2-head { display: none; }
  .catv2-row { grid-template-columns: 34px 34px minmax(0, 1fr) auto; gap: 10px; padding: 8px 12px; }
  .catv2-c-cost { display: none; }
}

/* ── Supplier price comparison (feature_catalog_compare) ────────────────────
   books/catalog-compare-view.js. A card per PART rather than per row, so this
   deliberately does not inherit the .catv2-* table: the unit here is "one part,
   priced by three wholesalers", which is a small stack, not a line.

   THE ACCENT IS RATIONED. Only .is-live — a part you are demonstrably buying
   from the dearer supplier — gets colour on its left edge and its verdict. A
   spread you are not paying for is information and is drawn as information.
   Every card lit the same way is a page that reads as noise. */
.catcmp-stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--sp-3); margin-bottom: var(--sp-4); }
.catcmp-s {
  padding: var(--sp-3) var(--sp-3_5); border: 1px solid var(--inv-border);
  border-radius: 12px; background: var(--inv-card);
}
.catcmp-s-v { display: flex; align-items: baseline; gap: 7px; font-size: 22px; font-weight: 700; color: var(--inv-text); font-variant-numeric: tabular-nums; }
.catcmp-s-v i { font-size: 16px; color: var(--inv-muted); align-self: center; }
.catcmp-s-v small { font-size: 12px; font-weight: 600; color: var(--inv-muted); }
.catcmp-s-l { display: block; margin-top: 3px; font-size: 12px; color: var(--inv-muted); }
.catcmp-s.is-bad .catcmp-s-v { color: var(--warn-soft); }
.catcmp-s.is-ok .catcmp-s-v { color: var(--ok-soft); }

.catcmp-list { display: flex; flex-direction: column; gap: var(--sp-3); }
.catcmp-g {
  border: 1px solid var(--inv-border); border-left: 3px solid var(--inv-border);
  border-radius: 12px; background: var(--inv-card); overflow: hidden;
}
.catcmp-g.is-live { border-left-color: var(--warn); }
.catcmp-h { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-3); padding: var(--sp-3) var(--sp-3_5) 0; }
.catcmp-name { font-size: 14px; font-weight: 700; color: var(--inv-text); min-width: 0; }
.catcmp-sup { font-size: 11.5px; color: var(--inv-muted); white-space: nowrap; }

.catcmp-v { display: flex; align-items: baseline; gap: 7px; flex-wrap: wrap; padding: 5px var(--sp-3_5) var(--sp-3); font-size: 12.5px; color: var(--inv-muted); }
.catcmp-v i { font-size: 14px; align-self: center; }
.catcmp-v b { font-weight: 700; color: var(--inv-text-2); font-variant-numeric: tabular-nums; }
.catcmp-v.is-live, .catcmp-v.is-live b { color: var(--warn-soft); }
.catcmp-v.is-live span { color: var(--inv-text-2); }

.catcmp-os { border-top: 1px solid var(--inv-border); }
.catcmp-o {
  width: 100%; display: grid; align-items: center; gap: var(--sp-3);
  grid-template-columns: minmax(110px, 1.1fr) minmax(0, 1.4fr) 108px 104px;
  padding: 9px var(--sp-3_5); min-height: 44px;
  font: inherit; text-align: left; border: 0; border-top: 1px solid var(--inv-border);
  background: none; color: var(--inv-text-2); cursor: pointer;
}
.catcmp-os .catcmp-o:first-child { border-top: 0; }
.catcmp-o:hover { background: var(--inv-card-hover); }
.catcmp-o:focus-visible { outline: 2px solid var(--inv-accent); outline-offset: -2px; }
.catcmp-o-sup { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; font-size: 13px; font-weight: 600; color: var(--inv-text); }
.catcmp-o-why { font-size: 11.5px; color: var(--inv-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.catcmp-o-n { font-variant-numeric: tabular-nums; font-size: 14px; font-weight: 700; text-align: right; color: var(--inv-text); white-space: nowrap; }
.catcmp-o-n small { font-size: 11px; font-weight: 600; color: var(--inv-muted); }
.catcmp-o-d { font-variant-numeric: tabular-nums; font-size: 12px; text-align: right; color: var(--inv-muted); white-space: nowrap; }
.catcmp-o-d b { font-weight: 700; }
.catcmp-o-cap { font-size: 10.5px; letter-spacing: .02em; opacity: .7; }
.catcmp-o.is-best .catcmp-o-n { color: var(--ok-soft); }
.catcmp-o.is-over .catcmp-o-d { color: var(--warn-soft); }
.catcmp-o.is-out { cursor: pointer; opacity: .62; }
.catcmp-o.is-out .catcmp-o-n { font-weight: 600; color: var(--inv-muted); }

/* Chips that say which line this is. Outlined, not filled: two filled pills on
   one row start competing with the price, which is the number being read. */
.catcmp-tag {
  display: inline-flex; align-items: center; gap: 3px;
  font-size: 10.5px; font-weight: 700; letter-spacing: .02em;
  padding: 1px 7px; border-radius: 999px; border: 1px solid;
}
.catcmp-tag i { font-size: 11px; }
.catcmp-tag.is-best { color: var(--ok-soft); border-color: var(--ok); }
.catcmp-tag.is-cur { color: var(--inv-muted); border-color: var(--inv-border-2); }

/* The AI adjudication bar. Sits between the stat band and the list because it
   is about the list's COMPLETENESS — "there are more of these than you can see"
   — rather than being an action on any one card. */
.catcmp-ai {
  display: flex; align-items: center; gap: var(--sp-3);
  margin-bottom: var(--sp-3); padding: var(--sp-3) var(--sp-3_5);
  border: 1px solid var(--inv-border-2); border-radius: 12px;
  background: var(--inv-card);
}
.catcmp-ai > i { font-size: 18px; color: var(--inv-accent); flex: none; }
.catcmp-ai > span { min-width: 0; flex: 1; }
.catcmp-ai b { display: block; font-size: 13px; font-weight: 700; color: var(--inv-text); }
.catcmp-ai small { display: block; margin-top: 2px; font-size: 11.5px; line-height: 1.45; color: var(--inv-muted); }
.catcmp-ai button { flex: none; }
.catcmp-ai.is-busy > i { animation: catcmp-spin 1s linear infinite; }
@keyframes catcmp-spin { to { transform: rotate(360deg); } }

@media (max-width: 640px) {
  .catcmp-ai { flex-wrap: wrap; }
  .catcmp-ai button { width: 100%; }
}

.catcmp-note {
  display: flex; align-items: flex-start; gap: 6px;
  margin: calc(var(--sp-3) * -1 + 2px) 0 var(--sp-4);
  font-size: 12px; line-height: 1.45; color: var(--inv-muted);
}
.catcmp-note i { font-size: 14px; flex: none; margin-top: 1px; }
.catcmp-more { margin-top: var(--sp-3); font-size: 12px; color: var(--inv-muted); display: flex; align-items: center; gap: 6px; }
.catcmp-empty { text-align: left; }

/* The delta column is the first thing to go: it is derived from the two prices
   either side of it, so losing it costs the least. The supplier and the price
   never separate — that pair IS the comparison. */
@media (max-width: 900px) {
  .catcmp-stats { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--sp-2); }
  .catcmp-s-v { font-size: 18px; }
  .catcmp-o { grid-template-columns: minmax(100px, 1fr) 100px 96px; }
  .catcmp-o-why { display: none; }
}
@media (max-width: 640px) {
  .catcmp-stats { grid-template-columns: minmax(0, 1fr); }
  .catcmp-o { grid-template-columns: minmax(0, 1fr) auto; gap: var(--sp-2); }
  .catcmp-o-d { grid-column: 2; font-size: 11.5px; }
  .catcmp-o-n { grid-row: 1; grid-column: 2; }
  .catcmp-o-sup { grid-row: 1; }
}

/* THE EXCLUDED ROW KEEPS ITS REASON AT EVERY WIDTH. The rule above drops the
   note column on narrow screens, and on an .is-out row that note is the only
   thing standing between "$4.00, greyed out" and the reader concluding it is
   the cheapest — which is precisely the wrong answer, because that row is a
   3m length priced per-each against a group priced per metre. So it comes back
   under the supplier name, wrapping, and the delta column goes instead: there
   is no delta on a row that was never ranked. Placed after the 640px block so
   it wins at both widths. */
@media (max-width: 900px) {
  .catcmp-o.is-out { grid-template-columns: minmax(0, 1fr) auto; row-gap: 2px; }
  .catcmp-o.is-out .catcmp-o-sup { grid-column: 1; grid-row: 1; }
  .catcmp-o.is-out .catcmp-o-why { display: block; grid-column: 1; grid-row: 2; white-space: normal; }
  .catcmp-o.is-out .catcmp-o-n { grid-column: 2; grid-row: 1; }
  .catcmp-o.is-out .catcmp-o-d { display: none; }
}

/* Category overflow menu (core/ui/catmenu.js) — the "More" chip and the
   searchable panel it opens, for catalogs whose supplier import brought
   dozens of categories with it. */
.catm { position: relative; display: inline-block; }
.catm-trigger { display: inline-flex; align-items: center; gap: 5px; }
.catm-caret { font-size: 14px; opacity: .7; margin-left: 1px; }
/* Fixed, not absolute: this opens inside .bk-modal (overflow-y:auto), which
   would clip an absolutely-positioned panel. catmenu.js sets top/left. */
.catm-panel {
  position: fixed; z-index: 300;
  width: 260px; padding: 8px; box-sizing: border-box;
  background: var(--inv-card); border: 1px solid var(--inv-border-2);
  border-radius: 12px; box-shadow: var(--inv-card-shadow), 0 12px 32px rgba(0,0,0,.3);
  display: flex; flex-direction: column; gap: 8px;
}
.catm-panel[hidden] { display: none; }
.catm-panel .catm-search { width: 100%; box-sizing: border-box; margin: 0; }
.catm-rows { display: flex; flex-direction: column; overflow-y: auto; min-height: 0; }
.catm-row {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  width: 100%; text-align: left; background: transparent; border: 0;
  border-radius: 8px; padding: 8px 10px; cursor: pointer;
  color: var(--inv-text); font-size: 13px; font-weight: 500;
}
.catm-row:hover, .catm-row:focus-visible { background: var(--inv-card-hover); outline: none; }

/* ── Touch targets (Apple HIG 44pt / Material 48dp) ─────────────
   These are the two most-tapped controls on the catalog and both sat around
   28–33px: .cat-chip at 6px padding on 13px text, and .catm-row at 8px. They
   are comfortable with a mouse and a coin-toss with a thumb, so they grow on
   coarse pointers only — desktop density is unchanged.
   NOT flag-gated: a target too small to hit is a bug, not a redesign. */
@media (hover: none) {
  .cat-chip { min-height: 44px; padding: 8px 15px; }
  .catm-row { min-height: 44px; }
  .catm-trigger { min-height: 44px; }
  .cat-facet { min-height: 44px; }
  .cat-viewtoggle button { min-height: 44px; }
  /* The shared list toolbar, not just the catalog's: .btn-ghost and .btn-primary
     sit at ~36px and the selects at ~39px on every Books list page, which is the
     same too-small-to-thumb bug. Coarse pointers only, so desktop is untouched. */
  .list-actions .btn-ghost,
  .list-actions .btn-primary,
  .list-actions .ui-actmenu__trigger,
  .list-actions select,
  .list-actions input { min-height: 44px; }
  .cat-count-clear { min-height: 44px; }
  .cat-selbar .btn-sm { min-height: 44px; padding: 8px 14px; }
  .cat-selbar select { min-height: 44px; }
  .catv2-row { min-height: 60px; }
}
.catm-row.is-active { background: var(--inv-accent); color: var(--inv-accent-fg); font-weight: 600; }
.catm-row-label { min-width: 0; overflow-wrap: anywhere; }
.catm-row-n { flex: none; font-size: 12px; opacity: .6; font-variant-numeric: tabular-nums; }
.catm-row.is-active .catm-row-n { opacity: .85; }
.catm-none { margin: 0; padding: 10px; font-size: 13px; color: var(--inv-muted); }
.cat-card .cat-price { text-align: right; font-weight: 700; font-size: 15px; white-space: nowrap; }
.cat-card .cat-unit { display: block; font-size: 11px; font-weight: 500; color: var(--inv-muted); }
.cat-card .cat-perunit { display: block; font-size: 11px; font-weight: 600; color: var(--inv-accent); }
.cat-pack-hint { font-size: 12px; font-weight: 600; color: var(--inv-accent); margin-top: -4px; }
.cat-shared { color: var(--inv-accent); }
.cat-shared i { font-size: 11px; }
.cat-share { display: flex; align-items: flex-start; gap: 8px; font-size: 13px; cursor: pointer; }
.cat-share input { margin-top: 2px; }
.cat-share .hint { display: block; }

/* Catalog picker overlay (quote editor → "From catalog") */
.cat-picker { max-width: 520px; display: flex; flex-direction: column; }
.cat-picker .cat-cats { margin: 12px 0; }
.cat-picker-list { display: flex; flex-direction: column; gap: 6px; overflow-y: auto; max-height: 46vh; margin: 0 -4px; padding: 0 4px; }
.cat-pick-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; width: 100%; text-align: left; background: var(--inv-card); border: 1px solid var(--inv-border); border-radius: 9px; padding: 11px 13px; cursor: pointer; color: var(--inv-text); transition: background .12s ease, border-color .12s ease; }
.cat-pick-row:hover, .cat-pick-row:focus-visible { background: var(--inv-card-hover); border-color: var(--inv-border-2); outline: none; }
.cat-pick-main { min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.cat-pick-name { font-weight: 600; font-size: 14px; overflow-wrap: anywhere; }
.cat-pick-meta { font-size: 11px; color: var(--inv-muted); overflow-wrap: anywhere; }
.cat-pick-price { font-weight: 700; font-size: 14px; white-space: nowrap; text-align: right; }
.cat-pick-price .cat-unit { display: block; font-size: 11px; font-weight: 500; color: var(--inv-muted); }
.cat-pick-perunit { display: block; font-size: 11px; font-weight: 600; color: var(--inv-accent); }

/* Per-line "save to catalog" action in the quote editor */
.it-actions { display: flex; align-items: center; gap: 4px; white-space: nowrap; }
.it-save { background: none; border: 0; color: var(--inv-muted); cursor: pointer; font-size: 16px; line-height: 1; padding: 4px; border-radius: 6px; transition: color .12s ease, background .12s ease; }
.it-save:hover { color: var(--inv-accent); background: var(--inv-card-hover); }

/* Site / scope photos (on-site quoting) */
.scope-photos { margin-top: 8px; }
.scope-grid { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 4px; }
.scope-thumb { position: relative; width: 96px; height: 96px; border-radius: 10px; overflow: hidden; border: 1px solid var(--inv-border); }
.scope-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.scope-del { position: absolute; top: 4px; right: 4px; width: 24px; height: 24px; border: 0; border-radius: 50%; background: rgba(0,0,0,.6); color: #fff; font-size: 16px; line-height: 1; cursor: pointer; display: flex; align-items: center; justify-content: center; }
.scope-del:hover { background: #dc2626; }
/* "Show on quote" — promotes a working site photo into the customer-facing set.
   Sits along the bottom edge of its own thumbnail so the state is read on the
   photo it belongs to, not in a list somewhere else. The ON state is filled
   rather than outlined: "the customer can see this" is the condition worth
   spotting at a glance across a grid of a dozen shots. */
.scope-show { position: absolute; left: 4px; right: 4px; bottom: 4px; border: 0; border-radius: 6px; padding: 4px 6px; font-size: 10.5px; font-weight: 600; line-height: 1.2; cursor: pointer; background: rgba(0,0,0,.62); color: #fff; text-align: center; }
.scope-show:hover { background: rgba(0,0,0,.8); }
.scope-show.on { background: var(--inv-accent, #33b1ff); color: #04121c; }
.scope-show.on:hover { background: var(--inv-accent, #33b1ff); filter: brightness(1.08); }
.scope-show:focus-visible { outline: 2px solid var(--inv-accent, #33b1ff); outline-offset: 2px; }
.scope-add { width: 96px; height: 96px; border: 1.5px dashed var(--inv-border-2); border-radius: 10px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; cursor: pointer; color: var(--inv-muted); font-size: 12px; transition: border-color .12s ease, color .12s ease; }
.scope-add:hover { border-color: var(--inv-accent); color: var(--inv-accent); }
.scope-add i { font-size: 22px; }

/* Customer modal (add/edit + equipment + job history) */
.bk-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.55); display: flex; align-items: center; justify-content: center; z-index: 60; padding: 16px; animation: inv-fade-in .15s ease; }
.bk-modal { background: var(--inv-surface); border: 1px solid var(--inv-border-2); border-radius: 14px; padding: 22px; width: 100%; max-width: 460px; max-height: 90vh; overflow-y: auto; }
.bk-modal h3 { margin: 0 0 14px; font-size: 17px; display: flex; align-items: center; gap: 8px; }
.bk-modal h3 i { color: var(--inv-accent); }
.bk-form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.bk-span2 { grid-column: 1 / -1; }
.bk-modal .lbl { margin: 0 0 4px; display: block; }
/* Standing note at the top of a modal — why the form below is the way it is
   (e.g. a read-only row you may look at but not write). */
.bk-note {
  display: flex; gap: 8px; align-items: flex-start; margin: 0 0 14px;
  padding: 9px 11px; border: 1px solid var(--inv-border); border-radius: var(--r, 8px);
  background: var(--inv-input-bg); color: var(--inv-muted);
  font-size: 12.5px; line-height: 1.5;
}
.bk-note i { color: var(--inv-accent); flex: none; }
/* Every control inside a locked form reads as text, not as an invitation. */
.bk-modal.is-readonly input,
.bk-modal.is-readonly select,
.bk-modal.is-readonly textarea { opacity: .72; cursor: not-allowed; }
.bk-sec { border-top: 1px solid var(--inv-border); margin-top: 16px; padding-top: 12px; }
.bk-sec-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 8px; }
.bk-sec-head .lbl { margin: 0; }
.bk-items { display: flex; flex-direction: column; gap: 6px; }
.bk-item { display: flex; align-items: flex-start; gap: 6px; border: 1px solid var(--inv-border); border-radius: 8px; padding: 8px 10px; }
.bk-item > div { flex: 1; min-width: 0; }
.bk-item-title { font-size: 13px; font-weight: 600; }
.bk-item-meta { font-size: 11px; color: var(--inv-muted); margin-top: 2px; }
.equip-thumbs { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
.equip-thumb { width: 52px; height: 52px; object-fit: cover; border-radius: 8px; border: 1px solid var(--inv-border); display: block; cursor: zoom-in; }
.bk-subform { display: none; margin-top: 10px; border: 1px solid var(--inv-border); border-radius: 8px; padding: 10px; }
.bk-subform-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.bk-subform-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 8px; }
.bk-modal-actions { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin-top: 18px; }
.bk-modal-actions .bk-right { display: flex; gap: 8px; margin-left: auto; }

/* Manual-send compose step (feature_email_templates) — "Email it yourself".
   Wider than the stock modal because the body is the thing being judged: it
   should read at roughly the width it'll land in the customer's inbox. */
.mail-modal { max-width: 640px; }
.mail-intro { font-size: 12.5px; color: var(--inv-muted); margin: 0 0 var(--sp-4); line-height: 1.5; }
.mail-tpl-row { display: flex; align-items: center; gap: var(--sp-3); }
.mail-tpl-row select { flex: 1; min-width: 0; }
.mail-edit-link { font-size: 12.5px; color: var(--inv-link); white-space: nowrap; }
.mail-edit-link:hover { color: var(--inv-link-h); }
.mail-modal textarea {
  width: 100%; min-height: 220px; max-height: 46vh; resize: vertical;
  line-height: 1.55; white-space: pre-wrap;
}
/* Which mailbox this will compose from — the thing mailto: can't control. */
.mail-from { font-size: 12px; color: var(--inv-muted); line-height: 1.5; margin-top: var(--sp-1); }
/* Post-open confirm state: where to check the account, and did it actually go. */
.mail-check { font-size: 13px; color: var(--inv-text-2); line-height: 1.6; margin: 0 0 var(--sp-3); }
.mail-check b { color: var(--inv-text); }
.mail-count { font-size: 12px; color: var(--inv-muted); text-align: right; margin-top: var(--sp-1); }
.mail-count.over { color: var(--warn); font-weight: 600; }
@media (max-width: 700px) {
  /* Stacked, and the body gets the room instead of the chrome. */
  .mail-tpl-row { flex-wrap: wrap; }
  .mail-modal textarea { min-height: 180px; max-height: 40vh; }
}

/* New-invoice chooser (blank / from accepted quote / from completed job) */
.ni-choices { display: flex; flex-direction: column; gap: 8px; margin-top: 4px; }
.ni-choice { display: flex; align-items: center; gap: 12px; width: 100%; text-align: left; background: var(--inv-card); border: 1px solid var(--inv-border); border-radius: 10px; padding: 14px; color: var(--inv-text); cursor: pointer; transition: background .12s ease, border-color .12s ease; }
.ni-choice:hover { background: var(--inv-card-hover); border-color: var(--inv-accent); }
.ni-choice > i { font-size: 22px; color: var(--inv-accent); flex: none; }
.ni-choice-t { font-size: 14px; font-weight: 600; }
.ni-choice-s { font-size: 12px; color: var(--inv-muted); margin-top: 2px; }

/* ── "From supplier" price-list import (catalog-import.js) ────── */
.cat-imp-modal { max-width: 880px; }
.cat-imp-hint { color: var(--inv-muted); font-size: 13px; margin: 0 0 14px; }
.cat-imp-sources { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.cat-imp-source { display: flex; flex-direction: column; gap: 6px; align-items: flex-start; text-align: left; background: var(--inv-card); border: 1px solid var(--inv-border); border-radius: 10px; padding: 14px; color: var(--inv-text); font-size: 13px; }
button.cat-imp-source { cursor: pointer; transition: background .12s ease, border-color .12s ease; }
button.cat-imp-source:hover { background: var(--inv-card-hover); border-color: var(--inv-border-2); }
.cat-imp-source > i { font-size: 22px; color: var(--inv-accent); }
.cat-imp-source span { color: var(--inv-muted); font-size: 12px; }
.cat-imp-paste textarea { width: 100%; resize: vertical; font-size: 12px; }
.cat-imp-paste input[type="url"] { width: 100%; font-size: 12px; padding: 8px; }
/* Product images (feature_catalog_images). Thumbnail above the name in the
   review grid; small square on catalog cards; live preview in the item modal. */
.cat-imp-thumb { display: block; width: 40px; height: 40px; object-fit: cover; border-radius: 6px; border: 1px solid var(--inv-border); margin-bottom: 4px; background: var(--inv-card); }
.cat-card-thumb { width: 46px; height: 46px; object-fit: cover; border-radius: 8px; border: 1px solid var(--inv-border); background: var(--inv-card); flex-shrink: 0; }
.cat-img-field { display: flex; align-items: center; gap: 10px; }
.cat-img-field input { flex: 1; min-width: 0; }
.cat-img-preview { width: 56px; height: 56px; object-fit: cover; border-radius: 8px; border: 1px solid var(--inv-border); background: var(--inv-card); flex-shrink: 0; }
.cat-imp-tools { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-bottom: 10px; }
.cat-imp-tools .lbl { margin: 0; }
.cat-imp-supwrap { display: flex; align-items: center; gap: 8px; }
.cat-imp-supwrap select, .cat-imp-supwrap #catImpSupplier { max-width: 220px; }
.cat-imp-fill { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-left: auto; }
.cat-imp-markup { display: flex; align-items: center; gap: 4px; font-size: 13px; color: var(--inv-text-2); }
.cat-imp-markup input { width: 58px; -moz-appearance: textfield; appearance: textfield; }
.cat-imp-markup input::-webkit-outer-spin-button,
.cat-imp-markup input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.cat-imp-scroll { overflow: auto; max-height: 52vh; border: 1px solid var(--inv-border); border-radius: 10px; }
.cat-imp-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.cat-imp-table th { position: sticky; top: 0; background: var(--inv-card); color: var(--inv-muted); font-size: 11px; font-weight: 600; text-align: left; padding: 8px; border-bottom: 1px solid var(--inv-border); white-space: nowrap; }
.cat-imp-table td { padding: 5px 6px; border-bottom: 1px solid var(--inv-border); vertical-align: middle; }
.cat-imp-table tr.is-off td { opacity: .45; }
.cat-imp-table input[type="text"], .cat-imp-table input[type="number"], .cat-imp-table select { width: 100%; min-width: 0; padding: 6px 8px; font-size: 13px; }
.cat-imp-table input[type="number"] { text-align: right; font-variant-numeric: tabular-nums; -moz-appearance: textfield; appearance: textfield; }
/* Hide the native spinner buttons on money inputs — their default styling
   clashes with the dark grid and collides with the right-aligned value. */
.cat-imp-table input[type="number"]::-webkit-outer-spin-button,
.cat-imp-table input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.cat-imp-name { min-width: 220px; }
.cat-imp-perunit { display: block; margin-top: 3px; font-size: 11px; font-weight: 600; color: var(--inv-accent); }
.cat-imp-part { min-width: 90px; }
.cat-imp-num { width: 84px; }
.cat-imp-sell { font-weight: 700; }
.cat-imp-badge { display: inline-block; font-size: 10px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; border-radius: 999px; padding: 2px 8px; background: var(--inv-card); color: var(--inv-muted); border: 1px solid var(--inv-border); white-space: nowrap; }
.cat-imp-badge.is-new { color: var(--inv-accent); border-color: var(--inv-accent); }
.cat-imp-badge.is-upd { color: var(--warn); border-color: var(--warn); }
/* Supplier-invoice reconciliation variance badges (invoice-import.js). */
.cat-imp-badge.is-ok { color: var(--ok); border-color: var(--ok); }
.cat-imp-badge.is-over { color: var(--inv-danger); border-color: var(--inv-danger); }
.cat-imp-badge.is-under { color: var(--inv-accent); border-color: var(--inv-accent); }
.cat-imp-badge.is-cost { color: var(--warn); border-color: var(--warn); }
.cat-imp-badge.is-off { color: var(--inv-muted); border-color: var(--inv-border); }
.siv-header { font-weight: 700; color: var(--inv-text); margin-right: auto; }
.siv-var { white-space: normal; line-height: 1.9; }
.siv-dim { color: var(--inv-muted); }
.cat-imp-progress { display: flex; align-items: center; gap: 10px; margin-top: 14px; padding: 10px 12px; border: 1px solid var(--inv-border); border-radius: 10px; background: var(--inv-card); font-size: 13px; color: var(--inv-text-2); }
.cat-imp-spinner { width: 16px; height: 16px; border: 2px solid var(--inv-border-2); border-top-color: var(--inv-accent); border-radius: 50%; animation: cat-imp-spin .7s linear infinite; flex: none; }
@keyframes cat-imp-spin { to { transform: rotate(360deg); } }
/* While reading/writing, the step content dims and stops taking clicks — the
   progress row is the live element. */
.cat-imp-modal.is-busy #catImpStep1, .cat-imp-modal.is-busy #catImpStep2,
.cat-imp-modal.is-busy #sivStep1, .cat-imp-modal.is-busy #sivStep2 { opacity: .55; pointer-events: none; }

/* ── Price-list CSV import summary (catalog-csv-import.js) ────────────────── */
.cat-csv-modal { max-width: 620px; }
.cat-csv-body.is-busy { opacity: .55; pointer-events: none; }
/* Column mapping read back to the user: "Name ← Description". */
.cat-csv-maps { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 14px; }
.cat-csv-map { font-size: 12px; color: var(--inv-text-2); background: var(--inv-card); border: 1px solid var(--inv-border); border-radius: 999px; padding: 3px 10px; }
.cat-csv-map b { color: var(--inv-text); font-weight: 600; }
/* "Last MFW import 13 Aug — 7,838 items." Quiet, but it's the difference
   between a re-import and an accident once several wholesalers are in here. */
.cat-csv-history { display: flex; align-items: baseline; gap: 6px; font-size: 13px; color: var(--inv-text-2); background: var(--inv-card); border: 1px solid var(--inv-border); border-radius: 8px; padding: 8px 10px; margin: 0 0 14px; }
.cat-csv-history i { color: var(--inv-accent); }
.cat-csv-counts { display: flex; gap: 10px; margin-bottom: 10px; }
.cat-csv-count { flex: 1 1 0; border: 1px solid var(--inv-border); border-radius: 10px; padding: 10px 12px; background: var(--inv-card); }
.cat-csv-count b { display: block; font-size: 20px; font-weight: 700; color: var(--inv-text); line-height: 1.2; }
.cat-csv-count span { font-size: 12px; color: var(--inv-muted); }
.cat-csv-dupes { border: 1px solid var(--inv-border); border-radius: 10px; padding: 12px; margin-bottom: 14px; }
.cat-csv-dupes select { width: 100%; margin-bottom: 8px; }
.cat-csv-check { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--inv-text-2); cursor: pointer; }
.cat-csv-check input { flex: none; }
.cat-csv-clean { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--inv-border); }
@media (max-width: 640px) {
  .cat-imp-sources { grid-template-columns: 1fr; }
  .cat-imp-tools #catImpSupplier { max-width: none; flex: 1 1 100%; }
  .cat-imp-fill { margin-left: 0; flex: 1 1 100%; }
  .cat-imp-fill select, .cat-imp-fill .btn-ghost { flex: 1 1 auto; }

  /* The 9-column grid can't fit a phone without hiding Category/prices off the
     right edge — become one stacked card per item so every field is reachable
     (mirrors table.ed-items in the invoice editor). */
  .cat-imp-scroll { border: 0; max-height: 58vh; }
  .cat-imp-table, .cat-imp-table tbody, .cat-imp-table tr, .cat-imp-table td { display: block; width: auto; }
  .cat-imp-table thead { display: none; }
  .cat-imp-table tr { position: relative; border: 1px solid var(--inv-border); border-radius: 10px; padding: 12px 12px 10px; margin-bottom: 10px; background: var(--inv-card); }
  .cat-imp-table tr.is-off { background: transparent; }
  .cat-imp-table td { border: 0; padding: 5px 0; display: flex; align-items: center; gap: 10px; }
  .cat-imp-table td[data-label]::before { content: attr(data-label); flex: 0 0 82px; color: var(--inv-muted); font-size: 11px; text-transform: uppercase; letter-spacing: .03em; }
  .cat-imp-table td.cat-imp-name { padding-right: 34px; }   /* clear the badge */
  .cat-imp-table input, .cat-imp-table select { min-height: 40px; font-size: 16px; }   /* 16px avoids iOS zoom */
  .cat-imp-num { width: 100%; }
  /* Include-checkbox rides top-left, status badge top-right, of each card. */
  .cat-imp-table td.cat-imp-cell-sel { position: absolute; top: 12px; right: 12px; padding: 0; }
  .cat-imp-table td.cat-imp-cell-sel input { min-height: 22px; width: 22px; height: 22px; }
  .cat-imp-table td.cat-imp-cell-badge { position: absolute; top: 14px; left: 12px; padding: 0; }
  .cat-imp-table td.cat-imp-name { margin-top: 6px; }
}

/* ── Motion ─────────────────────────────────────────────────── */
@keyframes inv-fade-up { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
@keyframes inv-fade-in { from { opacity: 0; } to { opacity: 1; } }

/* Summary stat cards stagger in on load/filter. */
.inv-summary .stat { animation: inv-fade-up .6s ease both; }
.inv-summary .stat:nth-child(2) { animation-delay: .1s; }
.inv-summary .stat:nth-child(3) { animation-delay: .2s; }
.inv-summary .stat:nth-child(4) { animation-delay: .3s; }

/* Settings panel + the editor's blocks ease in when rendered. */
.settings-card { animation: inv-fade-up .5s ease both; }
.ed-from       { animation: inv-fade-up .5s ease both; }
.ed-grid       { animation: inv-fade-up .55s ease both; animation-delay: .1s; }
table.ed-items { animation: inv-fade-up .6s ease both;  animation-delay: .2s; }
.ed-totals     { animation: inv-fade-up .65s ease both; animation-delay: .3s; }

/* Flash toast lifts as it fades in. */
#flash { transition: opacity .3s ease, transform .3s ease; }
#flash.show { transform: translateX(-50%) translateY(-6px); }

/* ── Mobile editor (≤560px) — field use on a phone ────────────────
   The line-items table overflows a phone (6 cols + inputs). Below this
   breakpoint each row becomes a stacked card, the customer/dates grid goes
   single-column, and the action bar sticks to the bottom so Finalize/Save is
   always one tap away instead of buried under the items. */
@media (max-width: 560px) {
  .ed-grid { grid-template-columns: 1fr; gap: 12px; padding: 16px; }

  /* Table → card list: hide the header, block-ify, one card per row. */
  table.ed-items { display: block; background: transparent; box-shadow: none; border-radius: 0; }
  table.ed-items thead { display: none; }
  table.ed-items tbody { display: block; }
  table.ed-items tr { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px;
    background: var(--inv-card); border: 1px solid var(--inv-border); border-radius: 10px;
    padding: 12px; margin-bottom: 10px; box-shadow: var(--inv-card-shadow); }
  table.ed-items td { display: flex; align-items: center; gap: 6px; padding: 0; border: 0; font-size: 13px; }
  table.ed-items td.dragcell { display: none; }         /* reorder is a desktop affordance */
  table.ed-items td.it-desc { flex: 1 1 100%; }
  table.ed-items td.it-desc input,
  table.ed-items td.it-desc textarea { width: 100%; }
  table.ed-items td[data-label]::before { content: attr(data-label); color: var(--inv-muted); font-size: 11px; white-space: nowrap; }
  table.ed-items input.w-qty, table.ed-items input.w-unit { width: 64px; }
  table.ed-items input.w-price { width: 96px; }
  /* Comfortable touch targets for on-site quoting. */
  table.ed-items input,
  table.ed-items textarea.it-desc-input { min-height: 42px; font-size: 16px; }  /* 16px avoids iOS zoom-on-focus */
  table.ed-items td.num { margin-left: auto; font-weight: 700; font-size: 15px; }
  table.ed-items td.it-actions { margin-left: 4px; gap: 2px; }
  table.ed-items td.it-actions .x { font-size: 26px; padding: 4px 6px; }
  table.ed-items td.it-actions .it-save { font-size: 20px; padding: 6px; }

  /* Line-item action row — make "From catalog" the obvious on-site path. */
  .items-head span { gap: 10px 14px; }
  .items-head .link { padding: 8px 4px; font-size: 14px; }

  /* Sticky action bar — full-bleed footer, primary action always reachable. */
  .ed-actions { position: sticky; bottom: 0; margin: 20px -16px 0; padding: 12px 16px;
    background: var(--inv-bg); border-top: 1px solid var(--inv-border); box-shadow: 0 -4px 16px rgba(0,0,0,.28); z-index: 20; }
  .ed-actions .btn-primary, .ed-actions .btn-ghost, .ed-actions .btn-danger-ghost { flex: 1 1 auto; text-align: center; }
  .ed-payments { width: 100%; }
}

/* ── Decluttered list toolbar (feature_list_toolbar_v2) ───────────
   core/ui/listtoolbar.js folds the occasional-use toolbar controls into two
   dropdowns: "Filter" (date range / sort / void toggle, count badge while
   active) and "More" (the secondary ghost buttons). The moved controls keep
   their classes, so only the dropdown chrome is styled here. */
.lt-dd { position: relative; display: inline-block; }
.lt-dd__trigger { display: inline-flex; align-items: center; gap: 7px; }
.lt-dd__caret { font-size: 15px; opacity: .7; }
.lt-dd__count { min-width: 18px; height: 18px; padding: 0 5px; box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; border-radius: 999px; background: var(--inv-accent); color: var(--inv-accent-fg); font-size: 11px; font-weight: 700; line-height: 1; }
.lt-dd__count[hidden] { display: none; }
.lt-dd__panel {
  position: absolute; right: 0; top: calc(100% + 6px); z-index: 200;
  min-width: 250px; padding: 12px; box-sizing: border-box;
  background: var(--inv-card); border: 1px solid var(--inv-border-2);
  border-radius: 12px; box-shadow: var(--inv-card-shadow), 0 12px 32px rgba(0,0,0,.3);
  display: flex; flex-direction: column; gap: 11px;
}
.lt-dd__panel[hidden] { display: none; }
.lt-field { display: flex; flex-direction: column; gap: 5px; font-size: 12px; font-weight: 600; color: var(--inv-muted); }
.lt-field .inv-search { width: 100%; box-sizing: border-box; margin: 0; }
.lt-dd__panel .inv-voidtoggle { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--inv-text-2); cursor: pointer; }
/* Undo the page-wide `input { width:100% }` — inside the definite-width panel
   it stretches the checkbox and shoves the label text to the far edge. */
.lt-dd__panel .inv-voidtoggle input { width: auto; }
/* Ghost buttons restyle as menu rows inside the More panel (specificity beats
   the light-theme .btn-ghost background override — keep this section last). */
.lt-dd__panel--menu { min-width: 230px; padding: 6px; gap: 0; }
.list-actions .lt-dd__panel--menu .btn-ghost { display: flex; align-items: center; gap: 10px; width: 100%; padding: 9px 11px; border-radius: 8px; background: transparent; text-align: left; font-weight: 500; white-space: nowrap; }
.list-actions .lt-dd__panel--menu .btn-ghost:hover { background: var(--inv-card-hover); }
.lt-dd__panel--menu .btn-ghost i { font-size: 17px; color: var(--inv-muted); }
@media (max-width: 720px) {
  /* Ride the mobile toolbar grid like the ghost buttons they replaced:
     two-up after the primary CTA and search rows. */
  .list-actions .lt-dd { order: 5; width: 100%; }
  .lt-dd__trigger { width: 100%; justify-content: center; }
  .lt-dd__panel { right: auto; left: 0; min-width: min(260px, 86vw); }
}

/* ── Draft editor sticky action bar (desktop) ─────────────────────
   Mobile already pins .ed-actions to the bottom (≤560px block above). This
   extends the same affordance to desktop DRAFTS only — renderEditor adds
   .ed-actions--sticky while the doc is editable — so Save / Finalize stay
   reachable on a long invoice instead of scrolling away. Scoped ≥561px so
   the mobile full-bleed padding/margins are untouched. */
@media (min-width: 561px) {
  .ed-actions--sticky { position: sticky; bottom: 0; z-index: 20; padding: 12px 0; background: var(--inv-bg); border-top: 1px solid var(--inv-border); }
}

/* ── Quick-view drawer (feature_doc_drawer) ───────────────────────
   core/ui/drawer.js — clicking a finalized invoice/quote row opens this
   read-and-act panel instead of swapping the list for the full editor.
   Right panel on desktop, bottom sheet on phones. Sits above the
   .bk-overlay layer (60), so actions that open a modal close it first. */
.nd-drawer-back { position: fixed; inset: 0; background: rgba(0,0,0,.45); z-index: 70; animation: inv-fade-in .15s ease; }
.nd-drawer { position: fixed; top: 0; right: 0; bottom: 0; width: min(430px, 94vw); display: flex; flex-direction: column; background: var(--inv-surface); border-left: 1px solid var(--inv-border-2); box-shadow: -16px 0 44px rgba(0,0,0,.35); z-index: 71; outline: none; animation: nd-drawer-in .22s cubic-bezier(.22,.61,.36,1) both; }
@keyframes nd-drawer-in { from { transform: translateX(28px); opacity: 0; } to { transform: none; opacity: 1; } }
.nd-drawer__head { display: flex; align-items: center; gap: 10px; padding: 16px 20px; border-bottom: 1px solid var(--inv-border); }
.nd-drawer__title { flex: 1; min-width: 0; display: flex; align-items: center; gap: 8px; font-size: 15px; font-weight: 700; }
.nd-drawer__close { background: none; border: 0; color: var(--inv-muted); font-size: 18px; cursor: pointer; padding: 6px; border-radius: 8px; }
.nd-drawer__close:hover { background: var(--inv-card-hover); color: var(--inv-text); }
.nd-drawer__body { flex: 1; overflow-y: auto; padding: 18px 20px; }
.nd-drawer__foot { display: flex; flex-wrap: wrap; gap: 8px; padding: 14px 20px calc(14px + env(safe-area-inset-bottom)); border-top: 1px solid var(--inv-border); }
.nd-drawer__foot .btn-primary, .nd-drawer__foot .btn-ghost, .nd-drawer__foot .btn-danger-ghost { flex: 1 1 auto; text-align: center; }
.nd-drawer .ed-payments { width: 100%; margin: 14px 0 0; }
.nd-qv-cust { font-size: 14px; font-weight: 600; margin: 2px 0 1px; }
.nd-qv-sub { font-size: 12.5px; color: var(--inv-muted); overflow-wrap: break-word; }
.nd-qv-hero { margin: 16px 0 4px; padding: 14px 16px; background: var(--inv-card); border: 1px solid var(--inv-border); border-radius: 12px; }
.nd-qv-hero .amt { font-size: 26px; font-weight: 800; letter-spacing: -.02em; }
.nd-qv-hero .sub { font-size: 12.5px; color: var(--inv-muted); margin-top: 3px; }
.nd-qv-rows { margin-top: 14px; }
.nd-qv-row { display: flex; justify-content: space-between; gap: 12px; padding: 7px 0; font-size: 13px; border-top: 1px solid var(--inv-border); color: var(--inv-text-2); }
.nd-qv-row b { color: var(--inv-text); font-weight: 600; }
@media (max-width: 560px) {
  .nd-drawer { top: auto; left: 0; right: 0; width: auto; max-height: 86vh; border-left: 0; border-top: 1px solid var(--inv-border-2); border-radius: 16px 16px 0 0; box-shadow: 0 -16px 44px rgba(0,0,0,.4); animation-name: nd-drawer-up; }
  @keyframes nd-drawer-up { from { transform: translateY(24px); opacity: 0; } to { transform: none; opacity: 1; } }
}

@media (prefers-reduced-motion: reduce) {
  *, ::before, ::after { animation-duration: .001ms !important; animation-iteration-count: 1 !important; transition-duration: .001ms !important; scroll-behavior: auto !important; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   AIRFLOW RESTYLE — feature_books_restyle (pre-paint adds html.books-v2; the
   flag→cache→class sync lives in js/books/books-bar.js, QA via ?ndbooks=1).
   SAME palette; a light-touch refinement — adopts the shared plenum shape/
   elevation/motion primitives (--r-*, --sh-*, --spring/--out from tokens.css)
   without changing colour, type sizes, or the shared nav. Every rule scoped
   under html.books-v2 → flag OFF renders byte-identical to today.
   Phase 2 of docs/visual-overhaul-plan.md.
   ═══════════════════════════════════════════════════════════════════════════ */

/* Elevation: cards, stats, settings panels and the editor tables all already
   read --inv-card-shadow — swap that one token and they inherit the layered,
   ink-tinted plenum shadow in a single stroke. */
html.books-v2 { --inv-card-shadow: var(--sh-1); }

/* Cards & panels — softer plenum corner radius. */
html.books-v2 .card,
html.books-v2 .settings-card,
html.books-v2 .ed-from,
html.books-v2 .ed-grid,
html.books-v2 table.ed-items { border-radius: var(--r-lg); }

/* Stat tiles — plenum radius + spring lift to the deeper --sh-2 on hover. */
html.books-v2 .stat {
  border-radius: var(--r);
  transition: transform var(--dur-2) var(--out), box-shadow var(--dur-2) var(--out),
              border-color var(--dur-2) var(--out), background var(--dur-2) var(--out);
}
html.books-v2 .stat:hover { transform: translateY(-3px); box-shadow: var(--sh-2); }

/* Buttons — tighter plenum radius, gentle elevation on the primary, spring press. */
html.books-v2 .btn-primary,
html.books-v2 .btn-ghost,
html.books-v2 .btn-danger-ghost {
  border-radius: var(--r-xs);
  transition: transform var(--dur-1) var(--out), background var(--dur-2) var(--out),
              box-shadow var(--dur-2) var(--out), color var(--dur-2) var(--out);
}
html.books-v2 .btn-primary { box-shadow: var(--sh-1); }
html.books-v2 .btn-primary:hover { box-shadow: var(--sh-2); }
html.books-v2 .btn-primary:active,
html.books-v2 .btn-ghost:active,
html.books-v2 .btn-danger-ghost:active { transform: translateY(1px) scale(.99); }

/* Inputs & search — plenum radius + a soft accent focus ring. */
html.books-v2 input,
html.books-v2 textarea,
html.books-v2 select,
html.books-v2 .inv-search {
  border-radius: var(--r-xs);
  transition: border-color var(--dur-2) var(--out), box-shadow var(--dur-2) var(--out);
}
html.books-v2 input:focus,
html.books-v2 textarea:focus,
html.books-v2 select:focus,
html.books-v2 .inv-search:focus {
  outline: none;
  border-color: var(--app-accent);
  box-shadow: 0 0 0 3px var(--nd-accent-soft);
}

/* List rows — spring the hover wash (colour unchanged). */
html.books-v2 .row { transition: background var(--dur-2) var(--out); }

/* Modals — plenum radius + pop elevation + blurred backdrop + spring entry. */
html.books-v2 .bk-overlay { backdrop-filter: blur(6px) saturate(115%); -webkit-backdrop-filter: blur(6px) saturate(115%); }
html.books-v2 .bk-modal { border-radius: var(--r-lg); box-shadow: var(--sh-pop); animation: bkv2-pop .19s var(--spring) both; }
@keyframes bkv2-pop { from { opacity: 0; transform: translateY(8px) scale(.985); } to { opacity: 1; transform: none; } }

/* Floating chrome — dropdown panels + bulk bar get the pop shadow to match. */
html.books-v2 .lt-dd__panel,
html.books-v2 .bulk-bar { border-radius: var(--r); box-shadow: var(--sh-pop); }

/* ── De-"AI"-ing the AI surfaces ──────────────────────────────────
   The insights card + AI modal shipped in the generic "AI product" costume:
   an indigo #6366f1 sparkle, a rainbow (indigo→cyan→green) gradient card
   border, and an indigo gradient primary button. That palette reads as bolted
   on — it was the single biggest "vibe-coded" tell in Books. The AI
   affordances now wear the product's OWN cold-blue accent so they read as a
   native part of neuralbooks rather than a third-party widget.

   These rules USED to be gated on html.books-v2, which meant the indigo was
   still live for anyone without the restyle flag. The fix is now applied at
   the source declarations above (.ov-ai::before, .bfx-primary, .bfx-ta:focus,
   the .ti-sparkles rules and .bell-notif i), so it holds regardless of flag
   state and this override block is no longer needed.

   (Stripe #635bff / Xero #13b5ea brand colours are deliberately untouched —
   those SHOULD look like the vendor.) */

/* ── Billing overhaul (2026-07) ───────────────────────────────────
   Per-line discount column (feature_line_discounts), doc_events activity
   timeline (feature_doc_timeline) and the split-screen live preview
   (feature_doc_preview). All flag-gated — none of this renders while the
   flags are off. */

/* Disc % column input (sits between Unit $ and GST). */
.w-disc { width: 74px; }
@media (max-width: 900px) { table.ed-items input.w-disc { width: 64px; } }

/* Activity timeline — the doc_events audit trail on a finalized document. */
.ed-activity { margin: 18px 0 6px; }
.ed-activity .act-row {
  display: flex; align-items: baseline; gap: 10px;
  padding: 5px 0; font-size: 13px; color: var(--inv-text-2);
  border-top: 1px solid var(--inv-border);
}
.ed-activity .act-dot {
  flex: 0 0 auto; width: 7px; height: 7px; border-radius: 50%;
  background: var(--inv-muted); position: relative; top: -1px;
}
.ed-activity .act-row[data-actor="customer"] .act-dot { background: #0891b2; }
.ed-activity .act-row[data-actor="system"] .act-dot { background: #94a3b8; }
.ed-activity .act-label { flex: 1; min-width: 0; }
.ed-activity .act-detail { color: var(--inv-muted); }
.ed-activity .act-actor { color: var(--inv-muted); font-size: 11px; text-transform: uppercase; letter-spacing: .04em; white-space: nowrap; }
.ed-activity .act-when { color: var(--inv-muted); white-space: nowrap; }

/* Split-screen editor + live preview (drafts, wide screens only). The wrap is
   capped at 920px — too narrow to host both columns — so while a preview is
   mounted (body.has-doc-preview, toggled by renderEditor/showList) the wrap
   widens enough that the editor column keeps its full ~900px width and the
   preview rides alongside instead of squashing it. */
.doc-split { display: grid; grid-template-columns: minmax(0, 1fr); gap: 26px; align-items: start; }
.doc-split-side { display: none; }
@media (min-width: 1280px) {
  body.has-doc-preview .wrap { max-width: 1340px; }
  .doc-split { grid-template-columns: minmax(0, 1fr) clamp(300px, 28vw, 400px); }
  .doc-split-side { display: block; position: sticky; top: 16px; }
}

/* ── Paper-style preview card ───────────────────────────────────────────────
   The draft as the customer will receive it, in the editor's right column.

   It used to be a THIRD design: Tailwind slate on a teal #0e7490 default with
   an accent stripe on a rounded card, agreeing neither with the app around it
   nor with the document it previews. The colours below are the customer page's
   own — the --cd-* LIGHT set in tokens.css — spelled out as literals because
   those names are scoped to html.cdoc-v2 and this is a Books page.

   It stays light in dark theme on purpose. This pane is the paper: a contractor
   asks it "what will they get", and the answer they act on is the printed and
   emailed form. The customer's own copy follows their device; this is the
   preview of a document, not a preview of a screen.

   NOT DONE, deliberately: loading customer-doc-v2.css in here and rendering the
   real customer markup. That markup is a 760px page with a rail and a card; this
   column is clamp(300px, 28vw, 400px). A faithful miniature at that width is
   less legible than a summary designed for it, and the thing that actually has
   to agree — the money — already does, because both derive their totals from
   invComputeTotals rather than computing their own. */
.doc-preview .dp-paper {
  background: #ffffff; color: #131722; border: 1px solid #e4e9f1;
  border-radius: 10px; box-shadow: 0 1px 2px rgba(16, 24, 40, .05), 0 8px 24px -14px rgba(16, 24, 40, .18);
  padding: 20px 22px; font-size: 12.5px; line-height: 1.45;
}
/* The brand closes the header, as it does on the letterhead of the real
   document — rather than striping the top of the card, which is the one device
   the canon retired. Falls back to a hairline for a contractor with no colour. */
.doc-preview .dp-head {
  display: flex; justify-content: space-between; gap: 16px;
  margin-bottom: 16px; padding-bottom: 12px;
  border-bottom: 2px solid var(--dp-accent, #e4e9f1);
}
.doc-preview .dp-title {
  font-size: 10px; font-weight: 600; letter-spacing: .08em;
  text-transform: uppercase; color: #6b7686;
}
.doc-preview .dp-number { font-size: 17px; font-weight: 600; letter-spacing: -.02em; color: #131722; margin-top: 1px; }
.doc-preview .dp-jobname { color: #131722; font-weight: 600; margin-top: 3px; }
.doc-preview .dp-meta { color: #6b7686; font-size: 11px; margin-top: 2px; }
.doc-preview .dp-seller { text-align: right; max-width: 46%; }
.doc-preview .dp-seller-name { font-weight: 600; color: #131722; }
.doc-preview .dp-logo { max-height: 34px; max-width: 120px; object-fit: contain; margin-bottom: 4px; }
.doc-preview .dp-fine { color: #6b7686; font-size: 11px; }
.doc-preview .dp-billto { margin-bottom: 14px; }
.doc-preview .dp-billto-lbl { text-transform: uppercase; letter-spacing: .08em; font-size: 10px; font-weight: 600; margin-bottom: 2px; }
.doc-preview .dp-items { width: 100%; border-collapse: collapse; margin-bottom: 12px; }
.doc-preview .dp-items th {
  text-align: left; font-size: 10px; text-transform: uppercase; letter-spacing: .08em;
  font-weight: 600; color: #6b7686; padding: 4px 6px; border-bottom: 1px solid #e4e9f1;
}
.doc-preview .dp-items td { padding: 5px 6px; border-bottom: 1px solid #e4e9f1; vertical-align: top; }
.doc-preview .dp-items tbody tr:last-child td { border-bottom: 0; }
.doc-preview .dp-items .num, .doc-preview .dp-items th.num { text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }
.doc-preview .dp-empty { color: #6b7686; text-align: center; padding: 14px 0; }
.doc-preview .dp-disc { color: #0f7a38; font-size: 11px; font-weight: 500; }
.doc-preview .dp-totals { margin-left: auto; max-width: 240px; }
.doc-preview .dp-totals > div { display: flex; justify-content: space-between; gap: 14px; padding: 2.5px 0; color: #4a5568; font-variant-numeric: tabular-nums; }
.doc-preview .dp-grand { font-weight: 600; font-size: 14px; color: #131722 !important; border-top: 1px solid #e4e9f1; margin-top: 4px; padding-top: 6px !important; }
.doc-preview .dp-notes { margin-top: 14px; padding-top: 10px; border-top: 1px solid #e4e9f1; color: #4a5568; white-space: pre-wrap; font-size: 11.5px; }

/* ═══════════════════════════════════════════════════════════════════════════
   LAYOUT V3 — feature_books_layout_v3 (pre-paint adds html.books-v3; the
   flag→cache→class sync lives in js/books/books-bar.js, QA via ?ndbooks3=1).
   A structural pass on top of the books-v2 restyle: a wider content shell, a
   sticky list toolbar, and — on the doc editor, wide screens only — a sticky
   right-hand context rail that lifts Customer-360 insight, the coach chips and
   the running totals out of the scroll (relocated by js/core/ui/layout-v3.js).
   Every rule scoped under html.books-v3 → flag OFF renders exactly as today.
   Step 1–3 of the layout-v3 blueprint (docs/mockups/layout-v3).
   ═══════════════════════════════════════════════════════════════════════════ */

/* Wider content shell. The editor hosts its context rail inside this same wrap
   (main 1fr + 300px rail). The live-preview split (feature_doc_preview) keeps
   its own wider cap — re-asserted here so v3 never narrows it. */
html.books-v3 .wrap { max-width: 1200px; }
html.books-v3 body.has-doc-preview .wrap { max-width: 1340px; }

/* List header → a sticky command band: title + search/filter/CTA stay reachable
   on long lists instead of scrolling away. Bleeds to the wrap's padding edges. */
html.books-v3 .list-head {
  position: sticky; top: 0; z-index: 6;
  margin: 0 -16px 16px; padding: 14px 16px;
  background: color-mix(in srgb, var(--inv-bg) 90%, transparent);
  -webkit-backdrop-filter: blur(10px) saturate(1.2);
  backdrop-filter: blur(10px) saturate(1.2);
  border-bottom: 1px solid var(--inv-border);
}
html.books-v3 .list-head h2 { font-size: 25px; letter-spacing: -.02em; }
/* Don't let the sticky band cover the editor (listView hidden → no .list-head). */
html.books-v3 #editorView { position: relative; z-index: 1; }

/* Card + grid elevation (independent of the v2 flag so v3 looks modern alone). */
html.books-v3 .card,
html.books-v3 .cust-card { border-radius: var(--r-lg); box-shadow: var(--sh-1); }
html.books-v3 .cust-grid { grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; }

/* Stat summary tiles read as a KPI band. */
html.books-v3 .inv-summary { gap: 14px; }
html.books-v3 .inv-summary .stat { border-radius: var(--r); box-shadow: var(--sh-1); }

/* ── Editor context rail (≥1200px, drafts & finals; NOT the preview split) ──
   layout-v3.js wraps the flat editor into .bk3-main + .bk3-rail and moves the
   insight strip, coach chips and totals into the rail; these rules lay it out.
   Below 1200px (or under the preview split) the wrapper is never added, so the
   editor renders in its normal single-column flow. */
html.books-v3 #editorView.bk3-has-rail {
  display: grid; grid-template-columns: minmax(0, 1fr) 300px;
  gap: 26px; align-items: start;
}
html.books-v3 .bk3-rail {
  position: sticky; top: 16px; align-self: start;
  display: flex; flex-direction: column; gap: 14px; min-width: 0;
}
/* Rail cards: give the moved blocks a uniform framed-card look. */
html.books-v3 .bk3-rail > * { margin: 0 !important; }
html.books-v3 .bk3-rail #edTotals {
  width: auto; background: var(--inv-card); border: 1px solid var(--inv-border);
  border-radius: var(--r); box-shadow: var(--sh-1); padding: 14px 16px;
}
html.books-v3 .bk3-rail .doc-insight,
html.books-v3 .bk3-rail #edCoach {
  background: var(--inv-card); border: 1px solid var(--inv-border);
  border-radius: var(--r); box-shadow: var(--sh-1); padding: 14px 16px;
}
html.books-v3 .bk3-rail #edCoach:empty,
html.books-v3 .bk3-rail .doc-insight:empty { display: none; }

/* ── Customer quick-view peek (mini-360 in the shared drawer) ──
   customers-page.js builds the body; these style the compact metric tiles and
   the open-items list inside the drawer. */
.nd-qc-hero { margin-bottom: 4px; }
.nd-qc-cust { font-size: 12px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--inv-muted); }
.nd-qc-amt { font-size: 28px; font-weight: 800; font-family: var(--num-font); font-variant-numeric: tabular-nums; margin: 4px 0 2px; }
.nd-qc-amt.owing { color: var(--warn-soft); }
.nd-qc-amt.clear { color: var(--ok-soft); }
.nd-qc-sub { color: var(--inv-text-2); font-size: 13px; margin-bottom: 14px; }
.nd-qc-mets { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin: 14px 0; }
.nd-qc-met { background: var(--inv-card); border: 1px solid var(--inv-border); border-radius: var(--r-xs); padding: 9px 10px; }
.nd-qc-met .mk { color: var(--inv-muted); font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; }
.nd-qc-met .mv { font-size: 16px; font-weight: 700; margin-top: 2px; font-family: var(--num-font); font-variant-numeric: tabular-nums; }
.nd-qc-row { display: flex; justify-content: space-between; gap: 12px; padding: 9px 0; border-top: 1px solid var(--inv-border); font-size: 13px; }
.nd-qc-row .k { color: var(--inv-muted); }
.nd-qc-row .v { font-weight: 600; text-align: right; }

/* ── Expenses source switch: recorded in-app vs read live from Xero ──
   Spacing uses --sp-* tokens deliberately (ui_ux_audit.md §1). */
.exp-seg { display: inline-flex; gap: var(--sp-px); background: var(--inv-input-bg); border: 1px solid var(--inv-border); border-radius: var(--r-xs); padding: var(--sp-px); }
.exp-seg button { border: 0; background: transparent; color: var(--inv-muted); border-radius: 6px; padding: var(--sp-2) var(--sp-3); cursor: pointer; font: inherit; font-size: 13px; line-height: 1; white-space: nowrap; }
.exp-seg button.on { background: var(--inv-card-hover); color: var(--inv-text); font-weight: 600; }

/* Read-only Xero bill card — visually quieter than an app expense you can edit,
   and it carries the "what's missing" note so a partial list can't read as a
   complete purchase history. */
.exp-xnote { display: flex; gap: var(--sp-2); align-items: flex-start; margin: var(--sp-0) var(--sp-0) var(--sp-3); padding: var(--sp-3) var(--sp-4); border: 1px solid var(--inv-border); border-radius: var(--r); background: var(--inv-input-bg); color: var(--inv-muted); font-size: 12.5px; line-height: 1.5; }
.exp-xnote i { color: var(--inv-accent); flex: none; }

/* ── Retention held (feature_progress_claims, js/invoices/invoices-page.js) ──
   Money earned and certified that cannot be invoiced yet. It sits ABOVE the
   invoice list rather than in it: this is not an invoice, and filing it among
   them would make it look like one that had been forgotten. */
.ret-card { background: var(--inv-card); border: 1px solid var(--inv-border); border-radius: var(--r); margin: var(--sp-0) var(--sp-0) var(--sp-4); overflow: hidden; }
.ret-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-3); padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--inv-border); }
.ret-head b { display: block; font-size: 14px; }
.ret-head span { display: block; font-size: 12px; color: var(--inv-muted); margin-top: var(--sp-px); max-width: 56ch; }
.ret-total { font-size: 18px; font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; }
.ret-row { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--inv-border); }
.ret-row:last-child { border-bottom: 0; }
.ret-main { flex: 1; min-width: 0; }
.ret-main b { display: block; font-size: 13px; font-weight: 600; }
.ret-main span { display: block; font-size: 12px; color: var(--inv-muted); }
.ret-amt { font-variant-numeric: tabular-nums; font-weight: 600; white-space: nowrap; }
/* A retention whose defects period has ended is the only row on this card that
   needs anything doing today; everything else is a note to self about money
   that is not yet yours to ask for. Marked with weight and a left edge rather
   than colour alone — the state has to survive a greyscale print and a
   colour-blind reader (ui_ux_audit.md §1). */
.ret-row.is-due { box-shadow: inset 3px 0 0 var(--app-accent); }
.ret-due { font-variant-numeric: tabular-nums; }
.ret-due.is-due { color: var(--app-accent); font-weight: 600; }
/* Quiet until touched: the date is there to be corrected occasionally, not to
   compete with the amount and the Release button for the row's attention. */
.ret-dueinput { font: inherit; font-size: 12px; color: var(--inv-muted); background: transparent;
  border: 1px solid transparent; border-radius: var(--radius-sm, 4px); padding: 0 var(--sp-1); }
.ret-dueinput:hover, .ret-dueinput:focus { border-color: var(--inv-border); color: inherit; }
@media (max-width: 560px) { .ret-row { flex-wrap: wrap; } .ret-main { flex: 1 1 100%; } }

/* A queried invoice is the one row on the list that will not chase itself. The
   icon and the wording carry the meaning; the colour only reinforces it, so the
   chip still reads correctly in greyscale and to a colour-blind reader
   (ui_ux_audit.md §1). */
.row-job.is-query { color: var(--doc-overdue); font-weight: 600; }

/* ── Cash-flow tile (feature_books_cashflow) ───────────────────────
   Two bars per period, in over out, with the net stated in words beside them.
   The bars are the shape of the answer; the number is the answer. Direction is
   carried by position and by the labelled key, never by colour alone. */
.cf-key { display: flex; gap: var(--sp-3); font-size: 12px; color: var(--inv-muted); margin-bottom: var(--sp-2); }
.cf-key span { display: inline-flex; align-items: center; gap: var(--sp-1); }
.cf-dot { width: 8px; height: 8px; border-radius: 2px; display: inline-block; }
.cf-dot.in { background: var(--app-accent); }
.cf-dot.out { background: var(--inv-muted); }
.cf-row { display: flex; align-items: center; gap: var(--sp-2); margin-bottom: var(--sp-2); }
.cf-when { flex: 0 0 84px; font-size: 12px; color: var(--inv-muted); }
.cf-bars { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.cf-bar { height: 8px; border-radius: 2px; background: var(--inv-border); overflow: hidden; }
.cf-bar span { display: block; height: 100%; border-radius: 2px; }
.cf-bar.in span { background: var(--app-accent); }
.cf-bar.out span { background: var(--inv-muted); }
.cf-net { flex: 0 0 auto; font-size: 13px; font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }
.cf-net.bad { color: var(--doc-overdue); }
.cf-note { margin: var(--sp-2) 0 0; font-size: 11px; color: var(--inv-muted); }

/* ── Chasing ladder editor (feature_dunning_ladder) ───────────────────
   A short list of steps, each "when" and "how", with a summary that says the
   whole ladder back in the words the chase queue uses. The summary is the part
   that earns its place: it is where a step in the wrong order becomes obvious,
   before a customer is the one who notices. */
.ladder-card { margin-top: var(--sp-3); padding: var(--sp-3); border: 1px solid var(--inv-border);
  border-radius: var(--radius-md, 8px); background: var(--inv-lift, transparent); }
.ladder-head b { display: block; font-size: 13px; }
.ladder-head .hint { display: block; margin-top: var(--sp-px); max-width: 62ch; }
.ladder-row { display: flex; gap: var(--sp-2); align-items: center; margin-top: var(--sp-2); }
.ladder-row select { flex: 1 1 0; min-width: 0; }
.ladder-foot { display: flex; gap: var(--sp-3); margin-top: var(--sp-2); }
.ladder-summary { margin-top: var(--sp-3); padding-top: var(--sp-3); border-top: 1px solid var(--inv-border); }
.ladder-summary b { font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: var(--inv-muted); }
.ladder-summary ol { margin: var(--sp-2) 0 0; padding-left: var(--sp-4); font-size: 13px; }
.ladder-summary li { margin-bottom: var(--sp-px); }
@media (max-width: 560px) { .ladder-row { flex-wrap: wrap; } .ladder-row select { flex: 1 1 100%; } }

/* ── Bills & bank spend: read-only money tables ────────────────────────────
   Both views rendered financial rows as .cust-card — the contact card from the
   customers directory — so amounts sat left-aligned inside a heading, there was
   no column to run an eye down, and no two totals lined up. These are lists of
   money, so they take the table pattern the Payments tab already established
   rather than inventing a third shape for the same job.

   Status is a word AND a colour, never colour alone: which bills are unpaid is
   a fact the owner acts on, and it has to survive a colour-blind reader and a
   phone in sunlight. */
.exp-tablewrap { overflow-x: auto; border: 1px solid var(--inv-border); border-radius: var(--r); background: var(--inv-card); }
table.exp-table { width: 100%; border-collapse: collapse; font-size: 13px; min-width: 760px; }
.exp-table th {
  text-align: left; font-size: 11.5px; font-weight: 600; text-transform: uppercase; letter-spacing: .04em;
  color: var(--inv-muted); background: var(--inv-input-bg);
  padding: var(--sp-2) var(--sp-3); border-bottom: 1px solid var(--inv-border); white-space: nowrap;
}
.exp-table td { padding: var(--sp-3); border-bottom: 1px solid var(--inv-border); vertical-align: top; }
.exp-table tr:last-child td { border-bottom: 0; }
.exp-table tbody tr:hover { background: var(--inv-card-hover); }
.exp-table td.date { white-space: nowrap; color: var(--inv-text-2); font-variant-numeric: tabular-nums; }
/* The supplier is the row's identity, so it gets a floor before the reference —
   which is the column that can afford to truncate — claims the width. Without
   this, auto table layout gave a long trading name four lines and left the
   reference half empty. */
.exp-table td.who { font-weight: 600; color: var(--inv-text); min-width: 18ch; }
.exp-table td.st { white-space: nowrap; }
/* Money is stored in cents and columns must align — tabular numerals are not
   optional here, they are the reason the column is scannable. */
.exp-table td.num { text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; font-weight: 600; }
.exp-table th.num { text-align: right; }
.exp-table td.act { text-align: right; white-space: nowrap; }

/* A second fact riding under the one above it: GST under a total, what is still
   owing under a status. Never a row of its own — it belongs to its number. */
/* nowrap: "$1,845.30 owing · due 18 Aug 26" broke after "due", which reads as
   two facts instead of one. */
.exp-sub { display: block; font-size: 11.5px; font-weight: 400; color: var(--inv-muted); margin-top: var(--sp-px); white-space: nowrap; }
.exp-sub.owing { color: var(--app-warn); font-weight: 500; }
.exp-ref { color: var(--inv-muted); font-size: 12px; display: block; max-width: 26ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.exp-pill {
  display: inline-flex; align-items: center; gap: var(--sp-1); padding: 2px 9px; border-radius: 100px;
  font-size: 11.5px; font-weight: 600; white-space: nowrap;
  border: 1px solid var(--inv-border-2); color: var(--inv-muted);
}
.exp-pill.paid { color: var(--app-ok); border-color: color-mix(in srgb, var(--app-ok) 40%, transparent); background: color-mix(in srgb, var(--app-ok) 12%, transparent); }
.exp-pill.wait { color: var(--app-warn); border-color: color-mix(in srgb, var(--app-warn) 40%, transparent); background: color-mix(in srgb, var(--app-warn) 12%, transparent); }
.exp-pill.late { color: var(--inv-danger); border-color: color-mix(in srgb, var(--inv-danger) 40%, transparent); background: color-mix(in srgb, var(--inv-danger) 12%, transparent); }

/* "Recorded here" — this Xero bill started life as an expense claim in this
   app. Quiet: it is provenance, not status. */
.exp-here { display: inline-flex; align-items: center; gap: 3px; font-size: 11px; font-weight: 500; color: var(--inv-muted); margin-left: var(--sp-2); }
/* The action column's header has no visible label — the icon link is
   self-evident sighted, and silent otherwise. Named exp-sr rather than sr-only:
   the shared .sr-only lives in auth.css (not loaded here) and in
   books-canon.css scoped to :root.books-v4, so a bare .sr-only on this page
   resolves to nothing and the label renders. */
.exp-sr { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.exp-xlink { color: var(--inv-link); text-decoration: none; font-size: 12px; font-weight: 600; white-space: nowrap; }
.exp-xlink:hover { text-decoration: underline; }
.exp-table a:focus-visible { outline: 2px solid var(--inv-accent); outline-offset: 2px; border-radius: 3px; }

@media (max-width: 640px) {
  /* A six-column money table cannot be squeezed into 390px: compressing it
     clipped the status pills mid-word and pushed TOTAL off the right edge —
     and the amount is the one thing a money row may never lose. So each row
     stops being a row and becomes a record: who and how much on the first
     line, because that is the pair being scanned, then the rest beneath. */
  table.exp-table { min-width: 0; display: block; }
  .exp-table thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
  .exp-table tbody { display: block; }
  .exp-table tr {
    display: grid; grid-template-columns: minmax(0, 1fr) auto;
    gap: var(--sp-1) var(--sp-3); padding: var(--sp-3);
    border-bottom: 1px solid var(--inv-border);
  }
  .exp-table tr:last-child { border-bottom: 0; }
  .exp-table td { display: block; padding: 0; border: 0; }
  .exp-table td.who { grid-column: 1; grid-row: 1; min-width: 0; font-size: 14px; }
  .exp-table td.num { grid-column: 2; grid-row: 1; }
  .exp-table td.st { grid-column: 1 / -1; grid-row: 2; white-space: normal; }
  .exp-table td.acct { grid-column: 1 / -1; }
  .exp-table td.date { grid-column: 1 / -1; }
  .exp-table td.act { grid-column: 1 / -1; }
  /* Reference goes: it identifies a row you have already found. */
  .exp-table th.ref, .exp-table td.ref { display: none; }
  /* The sub-lines may wrap here — there is no column left to hold them open. */
  .exp-table .exp-sub { white-space: normal; }
  .exp-table .exp-ref { max-width: none; }
}
.cust-card.is-readonly { cursor: default; }
.cust-card.is-readonly:hover { background: var(--inv-card); }

/* ── List/cards view toggle (invoices & quotes) ──
   A segmented ≡/⊞ control in the toolbar (built by layout-v3.js, persisted per
   list in localStorage). "Cards" reflows the SAME .row markup into a grid of
   cards via CSS only — no re-render, so selection/handlers are untouched. */
html.books-v3 .bk3-view-seg { display: inline-flex; gap: 2px; background: var(--inv-input-bg); border: 1px solid var(--inv-border); border-radius: var(--r-xs); padding: 2px; }
html.books-v3 .bk3-view-seg button { border: 0; background: transparent; color: var(--inv-muted); border-radius: 6px; padding: 6px 10px; cursor: pointer; font-size: 16px; line-height: 1; display: inline-flex; align-items: center; transition: background var(--dur-2, .15s) var(--out, ease), color var(--dur-2, .15s) var(--out, ease); }
html.books-v3 .bk3-view-seg button.on { background: var(--inv-card-hover); color: var(--inv-text); }

html.books-v3 .card.is-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 14px; background: transparent; border: 0; box-shadow: none; padding: 0; overflow: visible; }
html.books-v3 .card.is-cards .row { position: relative; flex-direction: column; align-items: stretch; gap: 10px; padding: 15px 16px; border: 1px solid var(--inv-border); border-radius: var(--r); background: var(--inv-card); box-shadow: var(--sh-1); }
html.books-v3 .card.is-cards .row:hover { background: var(--inv-card-hover); }
html.books-v3 .card.is-cards .row-check { position: absolute; top: 12px; right: 12px; }
html.books-v3 .card.is-cards .row-num { font-size: 15px; color: var(--inv-accent); }
html.books-v3 .card.is-cards .row-meta { display: flex; flex-direction: column; align-items: flex-start; gap: 8px; margin-left: 0; }
html.books-v3 .card.is-cards .row-total { font-size: 19px; text-align: left; }
html.books-v3 .card.is-cards .row-date { text-align: left; min-width: 0; }
html.books-v3 .card.is-cards .row-actions { width: auto; justify-content: flex-start; }
html.books-v3 .card.is-cards .row-paybar { margin-left: 0; }
/* Skeleton/empty states keep a sensible full-width slot in card mode. */
html.books-v3 .card.is-cards .sk-row, html.books-v3 .card.is-cards > .muted { grid-column: 1 / -1; }

/* Reverse direction: the customer/catalog card grids opt IN to a compact list.
   Cards reflow into single-column rows; the verbose extras (notes, cost/RRP,
   support code, insight strip) fold away for density — the primary line, money
   and chips stay. */
html.books-v3 .cust-grid.is-list { grid-template-columns: 1fr; gap: 0; background: var(--inv-card); border: 1px solid var(--inv-border); border-radius: var(--r-lg); box-shadow: var(--sh-1); overflow: hidden; }
html.books-v3 .cust-grid.is-list .cust-card { border: 0; border-radius: 0; box-shadow: none; border-bottom: 1px solid var(--inv-border); background: transparent; display: flex; align-items: center; gap: 16px; padding: 12px 16px; animation: none; }
html.books-v3 .cust-grid.is-list .cust-card:last-child { border-bottom: 0; }
html.books-v3 .cust-grid.is-list .cust-card.is-click:hover { transform: none; box-shadow: none; border-color: transparent; border-bottom-color: var(--inv-border); background: var(--inv-card-hover); }
html.books-v3 .cust-grid.is-list .cust-head { flex: 1; min-width: 0; }
html.books-v3 .cust-grid.is-list .cust-money { margin: 0 0 0 auto; flex: none; white-space: nowrap; }
html.books-v3 .cust-grid.is-list .cust-chips { flex: none; margin: 0; }
html.books-v3 .cust-grid.is-list > .cust-card > .cust-meta,
html.books-v3 .cust-grid.is-list .insight-strip { display: none; }

/* Overview widget grid: a deterministic 2-column layout under v3. The four
   single analytical cards (aging / revenue / quotes / top-customers) tile 2×2
   instead of leaving one orphaned in a 3-column auto-fit row; the KPI band and
   the full-width (.ov-wide) cards span both columns. Collapses to one column on
   narrow screens. Flag-off keeps the classic auto-fit grid. */
html.books-v3 .ov-grid { display: flex; flex-direction: column; gap: 16px; }
html.books-v3 .ov-cols { display: grid; grid-template-columns: 2fr 1fr; gap: 16px; align-items: start; }
html.books-v3 .ov-col-main, html.books-v3 .ov-col-side { display: grid; gap: 16px; align-content: start; }
@media (max-width: 900px) { html.books-v3 .ov-cols { grid-template-columns: 1fr; } }

/* ── Books-home KPI band (Overview) ──
   A full-width row of headline figures atop the widget grid. overview-page.js
   always emits it; hidden unless v3 so flag-off renders the classic overview. */
html:not(.books-v3) .ov-kpi-band { display: none; }
html.books-v3 .ov-kpi-band { grid-column: 1 / -1; display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; margin: 0; }
html.books-v3 .ov-kpi { background: var(--inv-card); border: 1px solid var(--inv-border); border-radius: var(--r); box-shadow: var(--sh-1); padding: 16px 18px; text-decoration: none; color: inherit; display: block; transition: transform var(--dur-2, .15s) var(--out, ease), box-shadow var(--dur-2, .15s) var(--out, ease), border-color var(--dur-2, .15s) var(--out, ease); }
html.books-v3 a.ov-kpi:hover { transform: translateY(-2px); box-shadow: var(--sh-2); border-color: var(--inv-border-2); }
html.books-v3 .ov-kpi-l { color: var(--inv-muted); font-size: 11.5px; font-weight: 600; text-transform: uppercase; letter-spacing: .05em; }
html.books-v3 .ov-kpi-v { font-size: 29px; font-weight: 700; margin-top: 6px; letter-spacing: -.02em; font-family: var(--num-font); font-variant-numeric: tabular-nums; }
html.books-v3 .ov-kpi-v.is-warn { color: var(--warn-soft); }
html.books-v3 .ov-kpi-v.is-ok { color: var(--ok-soft); }
html.books-v3 .ov-kpi-s { font-size: 12px; margin-top: 5px; color: var(--inv-muted); }
@media (max-width: 720px) { html.books-v3 .ov-kpi-band { grid-template-columns: repeat(2, 1fr); } }

/* Ready to bill (Overview) — work finished or approved and not yet invoiced.
   Unlike the needs-attention queue this is NOT v3-gated: an owner on the
   classic grid has the same unbilled work and the same reason to see it. */
.ov-rtb-row { display: flex; align-items: center; gap: 12px; padding: 11px 0; border-top: 1px solid var(--inv-border); text-decoration: none; color: inherit; }
.ov-rtb-row:hover strong { color: var(--inv-accent); }
.ov-rtb-ico { width: 30px; height: 30px; border-radius: 8px; flex: none; display: inline-flex; align-items: center; justify-content: center; font-size: 15px; background: rgba(74, 222, 128, .13); color: var(--ok-soft); }
.ov-rtb-main { flex: 1; min-width: 0; font-size: 13.5px; }
.ov-rtb-sub { display: block; color: var(--inv-muted); font-size: 12px; margin-top: 2px; }
.ov-rtb-go { color: var(--inv-muted); flex: none; }

/* Needs-attention queue (Overview) — actionable rows; hidden unless v3. */
html:not(.books-v3) .ov-na-card { display: none; }
html.books-v3 .ov-na-chase { display: inline-flex; align-items: center; gap: 6px; margin-top: 12px; font-size: 13px; font-weight: 600; color: var(--inv-accent); text-decoration: none; }
html.books-v3 .ov-na-chase:hover { text-decoration: underline; }
html.books-v3 .ov-na-row { display: flex; align-items: center; gap: 12px; padding: 10px 0; border-top: 1px solid var(--inv-border); text-decoration: none; color: inherit; }
html.books-v3 .ov-na-row:hover strong { color: var(--inv-accent); }
html.books-v3 .ov-na-ico { width: 30px; height: 30px; border-radius: 8px; display: inline-flex; align-items: center; justify-content: center; flex: none; font-size: 15px; }
html.books-v3 .ov-na-ico.is-warn { background: rgba(245, 158, 11, .13); color: var(--warn-soft); }
html.books-v3 .ov-na-ico.is-info { background: rgba(51, 177, 255, .13); color: var(--inv-accent); }
html.books-v3 .ov-na-main { flex: 1; min-width: 0; font-size: 13.5px; }
html.books-v3 .ov-na-sub { display: block; color: var(--inv-muted); font-size: 12px; margin-top: 1px; }
html.books-v3 .ov-na-amt { font-weight: 700; font-family: var(--num-font); font-variant-numeric: tabular-nums; white-space: nowrap; color: var(--warn-soft); }
html.books-v3 .ov-na-amt.ov-na-go { color: var(--inv-muted); font-weight: 400; }
html.books-v3 .ov-na-ico.is-danger { background: rgba(255, 92, 92, .14); color: var(--inv-danger); }

/* ── Overview redesign refinements (mockup fidelity) — all v3-scoped ────────
   KPI icon chips + hover accent bar; AI wash; vertical revenue chart; donut
   win-rate gauge. Flag-off never sees these (the band/cards are hidden). */

/* Page-head subtitle (Overview) — shown only under v3 (mockup page-head). */
.ov-head-sub { display: none; }
html.books-v3 .ov-head-sub { display: block; margin-top: 3px; color: var(--inv-muted); font-size: 13.5px; }

/* Card titles: sentence-case with a leading muted glyph (mockup), replacing the
   classic uppercase micro-label. Covers card()-built titles + the NA/AI cards. */
html.books-v3 .ov-title { display: flex; align-items: center; gap: 9px; margin-bottom: 14px; font-size: 14.5px; font-weight: 600; text-transform: none; letter-spacing: -.01em; color: var(--inv-text); }
html.books-v3 .ov-title i { font-size: 18px; font-weight: 400; color: var(--inv-muted); }

/* KPI tiles: label+icon top row, tinted chip, left accent bar on hover. */
html.books-v3 .ov-kpi { position: relative; overflow: hidden; }
html.books-v3 .ov-kpi::before { content: ''; position: absolute; inset: 0 auto 0 0; width: 3px; background: var(--inv-accent); opacity: 0; transition: opacity var(--dur-2, .15s) var(--out, ease); }
html.books-v3 a.ov-kpi:hover::before { opacity: 1; }
html.books-v3 .ov-kpi-top { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
html.books-v3 .ov-kpi-ico { width: 30px; height: 30px; border-radius: 9px; flex: none; display: inline-flex; align-items: center; justify-content: center; font-size: 16px; background: rgba(51, 177, 255, .13); color: var(--inv-accent); }
html.books-v3 .ov-kpi-ico.is-warn { background: rgba(245, 158, 11, .14); color: var(--warn-soft); }
html.books-v3 .ov-kpi-ico.is-ok { background: rgba(34, 197, 94, .14); color: var(--ok-soft); }
html.books-v3 .ov-kpi-v { margin-top: 10px; }

/* AI insights: subtle accent wash + accent hairline. */
html.books-v3 .ov-ai { background: linear-gradient(150deg, color-mix(in srgb, var(--inv-accent) 8%, var(--inv-card)), var(--inv-card) 60%); border: 1px solid rgba(51, 177, 255, .26); }

/* Revenue: vertical column chart, current month highlighted. */
html.books-v3 .ov-rev { display: flex; align-items: stretch; gap: 10px; height: 172px; padding-top: 6px; }
html.books-v3 .ov-rev-col { flex: 1 1 0; min-width: 0; height: 100%; display: flex; flex-direction: column; }
html.books-v3 .ov-rev-track { flex: 1; display: flex; align-items: flex-end; }
html.books-v3 .ov-rev-bar { width: 100%; max-width: 40px; margin: 0 auto; border-radius: 6px 6px 3px 3px; background: linear-gradient(180deg, var(--inv-accent), color-mix(in srgb, var(--inv-accent) 55%, transparent)); transition: filter var(--dur-2, .15s) ease; }
html.books-v3 .ov-rev-col:hover .ov-rev-bar { filter: brightness(1.12); }
html.books-v3 .ov-rev-lab { margin-top: 8px; text-align: center; font-size: 11.5px; color: var(--inv-muted); }
html.books-v3 .ov-rev-col.is-cur .ov-rev-bar { background: linear-gradient(180deg, var(--ok-soft), color-mix(in srgb, var(--ok) 45%, transparent)); }
html.books-v3 .ov-rev-col.is-cur .ov-rev-lab { color: var(--ok-soft); font-weight: 600; }

/* Quotes: conic-gradient donut gauge + legend. */
html.books-v3 .ov-gauge-wrap { display: flex; align-items: center; gap: 20px; }
html.books-v3 .ov-gauge { position: relative; width: 104px; height: 104px; flex: none; border-radius: 50%; background: conic-gradient(var(--inv-accent) calc(var(--pct, 0) * 1%), var(--inv-input-bg) 0); }
html.books-v3 .ov-gauge::after { content: ''; position: absolute; inset: 11px; border-radius: 50%; background: var(--inv-card); }
html.books-v3 .ov-gauge-num { position: absolute; inset: 0; display: grid; place-content: center; text-align: center; }
html.books-v3 .ov-gauge-num b { display: block; line-height: 1; font-size: 24px; font-weight: 700; font-family: var(--num-font); font-variant-numeric: tabular-nums; }
html.books-v3 .ov-gauge-num small { color: var(--inv-muted); font-size: 11px; }
html.books-v3 .ov-gauge-legend { font-size: 13px; color: var(--inv-text-2); }
html.books-v3 .ov-gauge-legend > div { padding: 3px 0; }
html.books-v3 .ov-gdot { display: inline-block; width: 9px; height: 9px; border-radius: 3px; margin-right: 7px; vertical-align: middle; }
html.books-v3 .ov-gdot-win { background: var(--inv-accent); }
html.books-v3 .ov-gdot-empty { background: var(--inv-input-bg); border: 1px solid var(--inv-border-2); }
html.books-v3 .ov-gauge-pending { margin-top: 6px; color: var(--inv-muted); }
html.books-v3 .ov-gauge-pending a { color: var(--inv-accent); text-decoration: none; font-weight: 600; }

/* ── List redesign refinements (Invoices / Quotes) — v3-scoped ─────────────
   The shared summary bar (.ui-sbar) already carries per-status colour; v3 just
   rounds it to the redesign's card radius and enlarges the headline figure.
   Rows already use .ui-badge pill-chips — v3 adds a leading status dot to the
   primary (lifecycle) badge for stronger at-a-glance encoding. Overrides live
   here (never in the lint-gated ui.css) and touch no markup. */
html.books-v3 .ui-sbar__seg { border-radius: var(--r-lg); padding: 14px 16px; border-bottom: 1px solid color-mix(in srgb, currentColor 10%, transparent); }
html.books-v3 .ui-sbar__val { font-size: 22px; letter-spacing: -.01em; }
html.books-v3 .ui-sbar__lbl { display: flex; align-items: center; gap: 7px; }
html.books-v3 .ui-sbar__lbl::before { content: ''; width: 8px; height: 8px; border-radius: 3px; background: var(--seg, currentColor); flex: none; }
html.books-v3 .row-badges .ui-badge:first-child::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: var(--_c); flex: 0 0 auto; }

/* List → mockup table: aligned columns + header row + hover actions. On desktop
   the row becomes a grid; display:contents flattens .row-meta so total/badges/
   date are direct cells, then `order` lays them out Status · Amount · Due. Row
   actions overlay on the right on hover. Mobile keeps the existing stacked
   reflow. Scoped to list view (:not(.is-cards)) so the card toggle is untouched. */
html.books-v3 .dl-head { display: none; }
@media (min-width: 700px) {
  html.books-v3 .card:not(.is-cards) .row { display: grid; grid-template-columns: 22px minmax(0, 2.3fr) 1.4fr 1.1fr 1fr; align-items: center; gap: 14px; padding: 15px 18px; position: relative; }
  html.books-v3 .card:not(.is-cards) .row-meta { display: contents; }
  html.books-v3 .card:not(.is-cards) .row-check { order: 1; margin: 0; }
  html.books-v3 .card:not(.is-cards) .row-main { order: 2; min-width: 0; }
  html.books-v3 .card:not(.is-cards) .row-badges { order: 3; flex-wrap: wrap; }
  html.books-v3 .card:not(.is-cards) .row-total { order: 4; text-align: right; }
  html.books-v3 .card:not(.is-cards) .row-date { order: 5; min-width: 0; margin: 0; text-align: left; }
  html.books-v3 .card:not(.is-cards) .row-paybar { margin: 6px 0 0 auto; }
  html.books-v3 .card:not(.is-cards) .row-actions { order: 6; position: absolute; right: 16px; top: 50%; transform: translateY(-50%); width: auto; opacity: 0; transition: opacity var(--dur-2, .13s) ease; background: var(--inv-card-hover); border-radius: 8px; padding: 4px 6px; box-shadow: var(--sh-1); }
  html.books-v3 .card:not(.is-cards) .row:hover .row-actions { opacity: 1; }
  html.books-v3 .card:not(.is-cards) .dl-head { display: grid; grid-template-columns: 22px minmax(0, 2.3fr) 1.4fr 1.1fr 1fr; gap: 14px; padding: 11px 18px; border-bottom: 1px solid var(--inv-border); border-radius: var(--r-lg) var(--r-lg) 0 0; color: var(--inv-muted); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .05em; background: var(--inv-input-bg); }
  html.books-v3 .card:not(.is-cards) .dl-head .r { text-align: right; }
}

/* Quotes: win-rate strip above the summary (mockup). Rendered only under v3. */
#quoteWinStrip:empty { display: none; }
html.books-v3 .qwin { display: flex; align-items: center; gap: 18px; margin-bottom: 16px; padding: 16px 20px; border-radius: var(--r-lg); border: 1px solid rgba(51, 177, 255, .26); background: linear-gradient(150deg, color-mix(in srgb, var(--inv-accent) 8%, var(--inv-card)), var(--inv-card) 60%); }
html.books-v3 .qwin-gauge { position: relative; width: 74px; height: 74px; flex: none; border-radius: 50%; background: conic-gradient(var(--inv-accent) calc(var(--pct, 0) * 1%), var(--inv-input-bg) 0); }
html.books-v3 .qwin-gauge::after { content: ''; position: absolute; inset: 8px; border-radius: 50%; background: var(--inv-card); }
html.books-v3 .qwin-gauge b { position: absolute; inset: 0; display: grid; place-content: center; font-family: var(--num-font); font-variant-numeric: tabular-nums; font-size: 17px; font-weight: 700; }
html.books-v3 .qwin-txt b { font-size: 15px; }
html.books-v3 .qwin-sub { color: var(--inv-muted); font-size: 12.5px; margin-top: 2px; }
html.books-v3 .qwin-push { margin-left: auto; text-align: right; }
html.books-v3 .qwin-push b { display: block; font-family: var(--num-font); font-variant-numeric: tabular-nums; color: var(--inv-accent); font-size: 18px; }
@media (max-width: 560px) { html.books-v3 .qwin { flex-wrap: wrap; } html.books-v3 .qwin-push { margin-left: 0; text-align: left; } }

/* Dedicated search toolbar directly above the list table (mockup places it below
   the chips + connection row, not in the header). invoices-page.js/quotes-page.js
   relocate the search input + sort + Filter into .dl-toolbar under v3; the search
   grows into a prominent field with a leading magnifier. */
html.books-v3 .dl-toolbar { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; flex-wrap: wrap; }
html.books-v3 .dl-toolbar .inv-search { height: 40px; margin: 0; }
html.books-v3 .dl-toolbar input[type="search"].inv-search {
  flex: 1 1 260px; min-width: 200px; width: auto; padding: 0 14px 0 40px; border-radius: var(--r-xs);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='%238b95a7' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='m21 21-4.3-4.3'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: 13px center;
}
html.books-v3 .dl-toolbar input[type="search"].inv-search:focus { outline: none; border-color: var(--inv-accent); box-shadow: 0 0 0 3px rgba(51, 177, 255, .15); }
html.books-v3 .dl-toolbar select.inv-search, html.books-v3 .dl-toolbar input[type="date"].inv-search { width: auto; }
html.books-v3 .dl-toolbar .btn-ghost, html.books-v3 .dl-toolbar .lt-dd__trigger { height: 40px; display: inline-flex; align-items: center; }
html.books-v3 .dl-toolbar .lt-dd { display: inline-flex; }

/* Connection banners → mockup pill-cards: neutral card face, prominent white
   logo tile, brand-coloured left edge; sit side-by-side. Both the connect-prompt
   (.sb-*) and the connected chip (.sb-chip-wrap, forced open so its status shows
   inline) get the same card treatment. CSS-only — no banner markup changes. */
html.books-v3 .int-banners { display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 16px; }
html.books-v3 .int-banners > div:not(.is-chip) { flex: 1 1 300px; }
html.books-v3 .sb { border-radius: var(--r); padding: 13px 15px; background: var(--inv-card); border: 1px solid var(--inv-border); }
html.books-v3 .sb-stripe { border-left: 3px solid #635bff; }
html.books-v3 .sb-xero { border-left: 3px solid #13b5ea; }
html.books-v3 .sb-square { border-left: 3px solid #006aff; }
html.books-v3 .sb-stripe, html.books-v3 .sb-xero, html.books-v3 .sb-square { color: var(--inv-text-2); }
html.books-v3 .sb-stripe strong, html.books-v3 .sb-xero strong, html.books-v3 .sb-square strong { color: var(--inv-text); }
html.books-v3 .sb-badge { width: 38px; height: 38px; border-radius: 10px; padding: 0; }
html.books-v3 .sb-badge svg { height: 22px; width: auto; }
html.books-v3 .sb-lead { gap: 13px; }
/* Connected state → compact green card: logo + brand + short "Connected" line,
   with the full config/actions collapsed behind the chevron (expands downward).
   The verbose detail never inflates the card at rest. */
html.books-v3 .int-banners > div.is-chip { display: block; flex: 1 1 300px; }
html.books-v3 .sb-card { display: block; border: 1px solid rgba(34, 197, 94, .28); border-left: 3px solid #22c55e; border-radius: var(--r); background: rgba(34, 197, 94, .07); overflow: hidden; }
html.books-v3 .sb-card .sb-chip { display: flex; align-items: center; gap: 13px; width: 100%; margin: 0; padding: 11px 14px; min-height: 0; background: none; border: 0; border-radius: 0; text-align: left; cursor: pointer; }
html.books-v3 .sb-card .sb-chip:hover { background: rgba(34, 197, 94, .06); }
html.books-v3 .sb-card .sb-badge { width: 38px; height: 38px; border-radius: 10px; padding: 0; flex: none; }
html.books-v3 .sb-card-txt { flex: 1; min-width: 0; display: flex; flex-direction: column; line-height: 1.32; }
html.books-v3 .sb-card-txt b { color: var(--inv-text); font-weight: 600; font-size: 14px; }
html.books-v3 .sb-card-txt small { color: var(--ok-soft); font-size: 12.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
html.books-v3 .sb-card .sb-chip-chev { flex: none; color: var(--inv-muted); transition: transform .25s ease; }
html.books-v3 .sb-card.open .sb-chip-chev { transform: rotate(180deg); }
/* Expand vertically (override the classic pill's horizontal grow). */
html.books-v3 .sb-card .sb-chip-body { order: 0; flex: none; width: 100%; }
html.books-v3 .sb-card.open .sb-chip-body { flex: none; width: 100%; }
html.books-v3 .sb-card .sb-chip-body-inner { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; padding: 11px 14px 12px; background: none; border: 0; border-top: 1px solid rgba(34, 197, 94, .18); color: var(--inv-text-2); font-size: 13px; line-height: 1.5; }
html.books-v3 .sb-card .sb-chip-body-inner .btn-ghost { background: var(--inv-card); border: 1px solid var(--inv-border); }

/* Motion honours the reduced-motion guard already in this file. */

/* ── Quote option set (feature_quote_options) ─────────────────────────────
   Sibling-option strip shown in the quote editor when a quote belongs to a
   "good/better/best" set. Chips jump between the options; colours read from
   the shared token palette so it matches the rest of Books. */
.opts-strip { border: 1px solid var(--inv-border); border-radius: 12px; padding: 12px 14px; margin: 0 0 14px; background: var(--inv-card); }
.opts-strip-head { display: flex; align-items: center; gap: 8px; font-weight: 700; font-size: 13px; color: var(--inv-text); }
.opts-strip-hint { font-weight: 400; color: var(--inv-muted); font-size: 12px; }
.opts-strip-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.opt-chip { display: inline-flex; align-items: center; gap: 8px; padding: 7px 12px; border: 1px solid var(--inv-border-2); border-radius: 999px; background: var(--inv-card); cursor: pointer; font: inherit; font-size: 12px; }
.opt-chip:hover:not(:disabled) { border-color: var(--inv-accent); }
.opt-chip.here { background: color-mix(in srgb, var(--inv-accent) 10%, var(--inv-card)); border-color: var(--inv-accent); cursor: default; }
.opt-chip-lbl { font-weight: 700; color: var(--inv-text); }
.opt-chip-amt { color: var(--inv-muted); }
.opt-chip-st { --_c: var(--doc-void); font-size: 10px; text-transform: uppercase; letter-spacing: .04em; padding: 2px 7px; border-radius: 999px; background: color-mix(in srgb, var(--_c) 15%, transparent); color: var(--_c); }
.opt-chip-st.st-sent { --_c: var(--doc-sent); }
.opt-chip-st.st-accepted { --_c: var(--doc-accepted); }
.opt-chip-st.st-declined, .opt-chip-st.st-void { --_c: var(--doc-declined); }

/* Collapsed option-set row in the quotes list (feature_quote_options). */
.row.row-optset .row-num { display: inline-flex; align-items: center; gap: 6px; }
.optset-count { font-weight: 500; font-size: 11px; color: var(--inv-muted); background: var(--inv-input-bg); padding: 1px 8px; border-radius: 999px; }
.optset-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; }
.opt-chip-mini { --_c: var(--doc-void); font-size: 10px; font-weight: 600; padding: 2px 8px; border-radius: 999px; background: color-mix(in srgb, var(--_c) 15%, transparent); color: var(--_c); }
.opt-chip-mini.st-sent { --_c: var(--doc-sent); }
.opt-chip-mini.st-accepted { --_c: var(--doc-accepted); }
.opt-chip-mini.st-declined, .opt-chip-mini.st-void { --_c: var(--doc-declined); }

/* ═══════════════════════════════════════════════════════════════════════
   Customers redesign (cx-*) — KPI band + master–detail split.
   Replaces the .cust-* card grid on customers.html. Theme-aware via the
   --inv-* aliases; soft fills use color-mix so they track light/dark.
   ═══════════════════════════════════════════════════════════════════════ */
.cx-wrap {
  --cx-accent: var(--inv-accent);
  --cx-accent-soft: color-mix(in srgb, var(--inv-accent) 14%, transparent);
  --cx-ok: var(--ok); --cx-ok-soft: color-mix(in srgb, var(--ok) 15%, transparent);
  --cx-warn: var(--warn); --cx-warn-soft: color-mix(in srgb, var(--warn) 15%, transparent);
  --cx-hot: var(--inv-danger); --cx-hot-soft: color-mix(in srgb, var(--inv-danger) 15%, transparent);
  --cx-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;
  /* --c-* are defined inside .c360 in customer360.css; re-alias them here so the
     timeline dots and the (NDIS) c360-card render themed outside that scope. */
  --c-pos: var(--ok); --c-neg: var(--inv-danger); --c-amber: var(--warn); --c-ink-3: var(--inv-muted);
  max-width: 1440px; margin: 0 auto; padding: 20px 24px 72px;
}

/* Page header */
.cx-head { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; margin-bottom: 18px; }
.cx-title { display: flex; align-items: baseline; gap: 10px; margin-right: auto; }
.cx-title h2 { font-size: 22px; font-weight: 640; letter-spacing: -0.02em; margin: 0; }
.cx-count {
  font: 600 12px/1 var(--cx-mono); color: var(--inv-muted);
  background: var(--inv-surface); border: 1px solid var(--inv-border);
  padding: 5px 9px; border-radius: 999px;
}
.cx-head .inv-search { min-width: 240px; }

/* KPI band */
.cx-kpis { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 16px; }
.cx-kpi {
  background: var(--inv-card); border: 1px solid var(--inv-border); border-radius: 15px;
  padding: 14px 16px; box-shadow: var(--inv-card-shadow);
}
.cx-kpi .k-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.07em; color: var(--inv-muted); font-weight: 600; }
.cx-kpi .k-val { font: 650 25px/1.1 var(--cx-mono); letter-spacing: -0.02em; margin-top: 8px; font-variant-numeric: tabular-nums; }
.cx-kpi .k-sub { font-size: 11.5px; color: var(--inv-text-2); margin-top: 6px; }
.cx-kpi.is-accent .k-val { color: var(--cx-accent); }
.cx-kpi.is-warn .k-val { color: var(--cx-warn); }
.cx-kpi .k-up { color: var(--cx-ok); } .cx-kpi .k-down { color: var(--cx-hot); }

/* Split */
.cx-split { display: grid; grid-template-columns: minmax(340px, 0.9fr) 1.35fr; gap: 16px; align-items: start; }

/* Directory list */
.cx-list { background: var(--inv-card); border: 1px solid var(--inv-border); border-radius: 15px; box-shadow: var(--inv-card-shadow); overflow: hidden; }
.cx-segs { display: flex; gap: 2px; padding: 8px 8px 0; border-bottom: 1px solid var(--inv-border); overflow-x: auto; }
.cx-seg {
  appearance: none; border: 0; background: transparent; cursor: pointer;
  font: 600 12.5px/1 var(--font-ui, inherit); color: var(--inv-muted);
  padding: 9px 12px 12px; border-bottom: 2px solid transparent; white-space: nowrap;
  display: flex; align-items: center; gap: 6px; transition: color 0.15s;
}
.cx-seg:hover { color: var(--inv-text-2); }
.cx-seg.is-active { color: var(--inv-text); border-bottom-color: var(--cx-accent); }
.cx-seg-n { font: 600 10.5px/1 var(--cx-mono); background: var(--inv-surface); border: 1px solid var(--inv-border); color: var(--inv-text-2); padding: 3px 6px; border-radius: 999px; }
.cx-seg.is-active .cx-seg-n { background: var(--cx-accent-soft); border-color: transparent; color: var(--cx-accent); }

.cx-tools { display: flex; align-items: center; gap: 10px; padding: 9px 14px; border-bottom: 1px solid var(--inv-border); }
.cx-showing { font-size: 11px; color: var(--inv-muted); text-transform: uppercase; letter-spacing: 0.06em; font-weight: 600; margin-right: auto; }
.cx-sortlbl { display: inline-flex; align-items: center; gap: 8px; font-size: 11px; color: var(--inv-muted); text-transform: uppercase; letter-spacing: 0.06em; font-weight: 600; }
.cx-sortlbl select {
  appearance: none; background: var(--inv-surface); border: 1px solid var(--inv-border); color: var(--inv-text);
  font: 600 12px var(--font-ui, inherit); text-transform: none; letter-spacing: 0; border-radius: 7px;
  padding: 6px 26px 6px 10px; cursor: pointer;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%238b95a7' stroke-width='2.5'><path d='M6 9l6 6 6-6'/></svg>");
  background-repeat: no-repeat; background-position: right 8px center;
}

.cx-rows { list-style: none; margin: 0; padding: 6px; max-height: calc(100vh - 340px); overflow-y: auto; }
.cx-row {
  display: grid; grid-template-columns: 38px 1fr auto; gap: 11px; align-items: center;
  padding: 10px 11px; border-radius: 11px; cursor: pointer; position: relative; transition: background 0.12s;
}
.cx-row:hover { background: var(--inv-card-hover); }
.cx-row.is-sel { background: var(--cx-accent-soft); }
.cx-row.is-sel::before { content: ""; position: absolute; left: 0; top: 8px; bottom: 8px; width: 3px; border-radius: 3px; background: var(--cx-accent); }
.cx-av { width: 38px; height: 38px; border-radius: 10px; display: grid; place-items: center; font: 650 13px/1 var(--font-ui, inherit); color: #fff; flex: none; }
.cx-rmain { min-width: 0; }
.cx-rname { font-weight: 600; font-size: 13.5px; display: flex; align-items: center; gap: 6px; }
.cx-rname .nm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cx-rsub { font-size: 11.5px; color: var(--inv-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-top: 2px; }
.cx-rright { text-align: right; flex: none; }
.cx-bal { font: 650 13px/1 var(--cx-mono); font-variant-numeric: tabular-nums; }
.cx-bal.owe { color: var(--cx-warn); } .cx-bal.od { color: var(--cx-hot); } .cx-bal.clear { color: var(--cx-ok); } .cx-bal.zero { color: var(--inv-muted); font-weight: 500; }
.cx-ractive { font-size: 10.5px; color: var(--inv-muted); margin-top: 3px; }
.cx-empty { padding: 40px 16px; text-align: center; color: var(--inv-muted); }

.cx-tag { font: 600 9.5px/1 var(--cx-mono); letter-spacing: 0.03em; padding: 3px 6px; border-radius: 5px; text-transform: uppercase; }
.cx-tag.portal { background: var(--cx-accent-soft); color: var(--cx-accent); }
.cx-tag.ndis { background: var(--cx-ok-soft); color: var(--cx-ok); }
.cx-tag.lead { background: var(--cx-warn-soft); color: var(--cx-warn); }
.cx-tag.unsaved { background: var(--inv-surface); color: var(--inv-muted); border: 1px solid var(--inv-border); }
/* What this customer is charged, when it isn't your standard prices. Quiet on
   purpose: it is a fact about the row, not a status to act on, and the two or
   three customers who carry one should read as different rather than urgent.
   Not uppercased — it holds a number, and "$95/HR" reads as shouting. */
.cx-tag.rate {
  background: var(--inv-surface); color: var(--inv-muted);
  border: 1px solid var(--inv-border); text-transform: none;
  display: inline-flex; align-items: center; gap: 3px;
}
.cx-tag.rate i { font-size: 10px; }

/* Profile panel */
.cx-profile { display: flex; flex-direction: column; gap: 14px; min-width: 0; }
.cx-pcard { background: var(--inv-card); border: 1px solid var(--inv-border); border-radius: 15px; box-shadow: var(--inv-card-shadow); }
.cx-hero { padding: 18px 20px; display: flex; gap: 16px; align-items: flex-start; flex-wrap: wrap; }
.cx-hero .cx-av { width: 56px; height: 56px; border-radius: 14px; font-size: 19px; }
.cx-hbody { flex: 1; min-width: 180px; }
.cx-hname { font-size: 20px; font-weight: 660; letter-spacing: -0.02em; display: flex; align-items: center; gap: 9px; flex-wrap: wrap; }
.cx-hcontact { color: var(--inv-text-2); font-size: 12.5px; margin-top: 6px; display: flex; flex-wrap: wrap; gap: 5px 14px; }
.cx-hcontact span { display: inline-flex; align-items: center; gap: 6px; }
.cx-hcontact i { color: var(--inv-muted); }
.cx-hactions { display: flex; gap: 8px; flex-wrap: wrap; margin-left: auto; }

.cx-metrics { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1px; background: var(--inv-border); border-top: 1px solid var(--inv-border); }
.cx-metric { background: var(--inv-card); padding: 14px 18px; }
.cx-metric .m-lbl { font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--inv-muted); font-weight: 600; }
.cx-metric .m-val { font: 650 19px/1.1 var(--cx-mono); font-variant-numeric: tabular-nums; margin-top: 7px; letter-spacing: -0.01em; }
.cx-metric .m-sub { font-size: 11px; color: var(--inv-text-2); margin-top: 4px; }
.cx-metric.pos .m-val { color: var(--cx-accent); }
.cx-metric.owe .m-val { color: var(--cx-warn); }
.cx-metric.od .m-val { color: var(--cx-hot); }
.cx-metric.small .m-val { font-size: 14px; padding-top: 3px; }

.cx-note { background: var(--inv-surface); border: 1px solid var(--inv-border); border-left: 3px solid var(--cx-accent); border-radius: 7px; padding: 11px 13px; font-size: 12.5px; color: var(--inv-text-2); font-style: italic; }

.cx-tabs { display: flex; gap: 2px; padding: 6px 10px 0; border-bottom: 1px solid var(--inv-border); overflow-x: auto; }
.cx-tab { appearance: none; border: 0; background: transparent; cursor: pointer; font: 600 12.5px/1 var(--font-ui, inherit); color: var(--inv-muted); padding: 10px 12px 12px; border-bottom: 2px solid transparent; white-space: nowrap; transition: color 0.15s; }
.cx-tab:hover { color: var(--inv-text-2); }
.cx-tab.is-active { color: var(--inv-text); border-bottom-color: var(--cx-accent); }
.cx-tabbody { padding: 16px 20px 20px; }

/* Activity timeline */
.cx-tl { list-style: none; margin: 0; padding: 0; position: relative; }
.cx-tl::before { content: ""; position: absolute; left: 15px; top: 6px; bottom: 6px; width: 1.5px; background: var(--inv-border); }
.cx-tl li { position: relative; padding: 0 0 16px 42px; }
.cx-tl li:last-child { padding-bottom: 0; }
.cx-tl .dot { position: absolute; left: 8px; top: 1px; width: 16px; height: 16px; border-radius: 50%; border: 3px solid var(--inv-card); display: grid; place-items: center; }
.cx-tl .dot i { font-size: 8px; color: #fff; }
.cx-tl .t-title { font-size: 13px; font-weight: 600; }
.cx-tl .t-meta { font-size: 11.5px; color: var(--inv-muted); margin-top: 2px; }
.cx-tl .t-amt { float: right; font: 650 12.5px var(--cx-mono); font-variant-numeric: tabular-nums; }

/* Doc table (invoices & quotes) */
.cx-dtable { width: 100%; border-collapse: collapse; font-size: 12.5px; }
.cx-dtable th { text-align: left; font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--inv-muted); font-weight: 600; padding: 0 10px 8px; border-bottom: 1px solid var(--inv-border); }
.cx-dtable td { padding: 11px 10px; border-bottom: 1px solid var(--inv-border); vertical-align: middle; }
.cx-dtable tr:last-child td { border-bottom: 0; }
.cx-dtable tbody tr { transition: background 0.12s; }
.cx-dtable tbody tr:hover { background: var(--inv-card-hover); }
.cx-dtable .num { text-align: right; font-family: var(--cx-mono); font-variant-numeric: tabular-nums; }
.cx-dtable .doc-id { font-family: var(--cx-mono); font-size: 11.5px; color: var(--inv-text-2); }

.cx-pill { display: inline-flex; align-items: center; gap: 5px; font: 600 10.5px/1 var(--font-ui, inherit); padding: 4px 9px; border-radius: 999px; }
.cx-pill::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.cx-pill.pos { background: var(--cx-ok-soft); color: var(--cx-ok); }
.cx-pill.sent { background: var(--cx-accent-soft); color: var(--cx-accent); }
.cx-pill.amber { background: var(--cx-warn-soft); color: var(--cx-warn); }
.cx-pill.neg { background: var(--cx-hot-soft); color: var(--cx-hot); }
.cx-pill.ink-3 { background: var(--inv-surface); color: var(--inv-muted); border: 1px solid var(--inv-border); }

/* Equipment cards */
.cx-equip-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; }
.cx-eq { border: 1px solid var(--inv-border); border-radius: 11px; padding: 13px 14px; background: var(--inv-surface); display: flex; gap: 12px; align-items: flex-start; }
.cx-eq-ic { width: 36px; height: 36px; border-radius: 9px; background: var(--cx-accent-soft); color: var(--cx-accent); display: grid; place-items: center; flex: none; font-size: 18px; }
.cx-eq-name { font-weight: 600; font-size: 13px; }
.cx-eq-meta { font-size: 11.5px; color: var(--inv-muted); margin-top: 2px; }
.cx-eq-w { font-size: 10.5px; margin-top: 7px; display: inline-flex; align-items: center; gap: 5px; padding: 3px 7px; border-radius: 6px; }
.cx-eq-w.ok { background: var(--cx-ok-soft); color: var(--cx-ok); }
.cx-eq-w.soon { background: var(--cx-warn-soft); color: var(--cx-warn); }
.cx-eq-w.exp { background: var(--cx-hot-soft); color: var(--cx-hot); }

/* Sites */
.cx-sites { display: flex; flex-direction: column; gap: 10px; }
.cx-site { border: 1px solid var(--inv-border); border-radius: 11px; padding: 13px 14px; display: flex; gap: 12px; align-items: center; }
.cx-site-ic { width: 34px; height: 34px; border-radius: 9px; background: var(--inv-surface); border: 1px solid var(--inv-border); display: grid; place-items: center; color: var(--inv-muted); flex: none; font-size: 16px; }
.cx-site-label { font-weight: 600; font-size: 13px; display: flex; align-items: center; gap: 7px; }
.cx-site-label .badge { font: 600 9px/1 var(--cx-mono); text-transform: uppercase; letter-spacing: 0.04em; padding: 2px 6px; border-radius: 5px; background: var(--cx-accent-soft); color: var(--cx-accent); }
.cx-site-addr { font-size: 11.5px; color: var(--inv-muted); margin-top: 2px; }
.cx-site-count { margin-left: auto; font: 600 11px var(--font-ui, inherit); color: var(--inv-text-2); text-align: right; }

.cx-tab-empty { color: var(--inv-muted); font-size: 12.5px; padding: 8px 0; }
.cx-profile-empty { padding: 60px 20px; text-align: center; color: var(--inv-muted); }

@media (max-width: 1040px) {
  .cx-split { grid-template-columns: 1fr; }
  .cx-kpis { grid-template-columns: repeat(2, 1fr); }
  .cx-rows { max-height: 420px; }
}
@media (max-width: 560px) {
  .cx-wrap { padding: 14px 14px 60px; }
  .cx-metrics { grid-template-columns: repeat(2, 1fr); }
  .cx-hactions { margin-left: 0; width: 100%; }
}

/* ── Customers under BOOKS LIST V2 ──────────────────────────────────────────
   feature_books_list_v2 restyled the invoices/quotes lists (ui-band money
   band, ui-rail pill chips, UI-font tabular numerals — see the LIST V2 block
   below). customers-page.js swaps its KPI band and segment tabs to those
   primitives when html.books-list-v2 is set; these overrides retune the cx
   chrome around them. Flag OFF renders the block above unchanged. */
html.books-list-v2 .cx-wrap {
  /* Match the 1200px cap the other v2 lists share (and books-v3's shell). */
  max-width: 1200px;
  /* v2 dropped mono numerals for the UI font + tabular-nums. Every cx number
     styles itself through this variable, so one swap converts the balances,
     metric tiles, doc tables and tags in step. Must be a real family list —
     `inherit` inside a font: shorthand invalidates the whole declaration. */
  --cx-mono: 'Sora', system-ui, -apple-system, sans-serif;
}
html.books-list-v2 .cx-title h2 { font-size: 25px; }
html.books-list-v2 .cx-head .inv-search { width: 300px; }
/* The container becomes a plain block — the ui-band brings its own card and
   its own 12px bottom margin. */
html.books-list-v2 .cx-kpis { display: block; grid-template-columns: none; gap: 0; margin-bottom: 4px; }
/* Rail chips replace the underline tabs inside the list card's header strip;
   the chips carry their own pill geometry, the container just spaces them.
   Wrap rather than scroll — the list column is too narrow to hide a chip
   behind an invisible horizontal scroll. */
html.books-list-v2 .cx-segs { gap: 6px; padding: 12px 14px; flex-wrap: wrap; overflow-x: visible; }

/* ── Multi-day billing ───────────────────────────────────────────────────────
   One invoice can bill a job that ran over several days (several job_schedule
   rows sharing invoice_id). Two surfaces: the "Billing N days" panel under the
   invoice editor's breadcrumb, and the day-picker in the create-from-job modal.
   Both stay quiet — this is provenance, not a second subject on the page. */
.inv-days { border: 1px solid var(--inv-border); border-radius: 10px; padding: 10px 12px; margin: 0 0 14px; background: var(--inv-surface); }
.inv-days-head { display: flex; align-items: center; gap: 10px; font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; color: var(--inv-muted); }
.inv-days-add { margin-left: auto; background: none; border: 0; padding: 2px 4px; font: 600 12px inherit; color: var(--inv-link); cursor: pointer; display: inline-flex; align-items: center; gap: 4px; text-transform: none; letter-spacing: 0; }
.inv-days-add:hover { color: var(--inv-link-h); }
.inv-days-list { list-style: none; margin: 6px 0 0; padding: 0; }
.inv-day { display: flex; align-items: center; gap: 9px; padding: 5px 0; font-size: 13px; color: var(--inv-text-2); border-top: 1px solid var(--inv-border); }
.inv-day:first-child { border-top: 0; }
.inv-day > i { font-size: 14px; opacity: 0.6; flex: none; }
.inv-day-when { font-weight: 600; color: var(--inv-text); flex: none; }
.inv-day-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.inv-day-x { margin-left: auto; background: none; border: 0; padding: 3px 5px; border-radius: 6px; color: var(--inv-muted); cursor: pointer; flex: none; }
.inv-day-x:hover { color: var(--inv-danger); background: var(--inv-card-hover); }

/* Day-picker rows in the "Bill more than one day?" modal. */
.md-day { display: flex; align-items: center; gap: 11px; padding: 9px 10px; border: 1px solid var(--inv-border); border-radius: 9px; margin-top: 6px; cursor: pointer; }
.md-day:hover { background: var(--inv-card-hover); }
.md-day input { width: 16px; height: 16px; flex: none; accent-color: var(--inv-accent); cursor: pointer; }
.md-day-main { min-width: 0; }
.md-day-name { display: block; font-weight: 600; font-size: 13.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.md-day-sub { display: block; font-size: 12px; color: var(--inv-muted); }
.md-opt { display: flex; align-items: flex-start; gap: 10px; padding: 8px 10px; border: 1px solid var(--inv-border); border-radius: 9px; margin-top: 6px; cursor: pointer; }
.md-opt:hover { background: var(--inv-card-hover); }
.md-opt input { margin-top: 3px; flex: none; accent-color: var(--inv-accent); cursor: pointer; }
.md-opt strong { display: block; font-size: 13.5px; font-weight: 600; }
.md-opt em { display: block; font-style: normal; font-size: 12px; color: var(--inv-muted); margin-top: 1px; }

/* ══════════════════════════════════════════════════════════════════════════
   BOOKS LIST V2 — feature_books_list_v2 (pre-paint adds html.books-list-v2;
   books-bar.js applyListV2Gate keeps the cached class in sync). Every rule
   below is scoped under that class, so flag OFF renders exactly as today.

   Three pieces, all fed by markup the page scripts already emit:
     .ui-band  — money band (core/ui/summarybar.js ndMoneyBand)
     .ui-rail  — status rail (invoices-page/quotes-page renderStatusRail)
     .row--v2  — customer-led list row (core/ui/doclist.js rowV2)
   ══════════════════════════════════════════════════════════════════════════ */

/* The command row needs the width the redesign assumes. Matches books-v3's cap
   so the two flags compose rather than fight. */
html.books-list-v2 .wrap { max-width: 1200px; }

/* ── Command row ─────────────────────────────────────────────────────────── */
html.books-list-v2 .list-head { align-items: center; gap: 12px; margin-bottom: 16px; }
html.books-list-v2 .list-head h2 { font-size: 25px; letter-spacing: -.02em; }
html.books-list-v2 .list-actions { gap: 9px; }
html.books-list-v2 .list-actions input[type="search"] { width: 300px; }

/* The container is .inv-summary, which books.css lays out as a FOUR-COLUMN
   grid for the classic stat cards. The v2 summary is two children — the band
   and the status rail — so they landed in the first two of four columns: a
   band squashed to a quarter width with its KPIs stacked, and a clipped rail
   beside it. v2 owns the internal layout, so the container is just a block. */
html.books-list-v2 .inv-summary { display: block; grid-template-columns: none; }

/* ── Money band ──────────────────────────────────────────────────────────── */
.ui-band {
  background: var(--inv-card);
  border: 1px solid var(--inv-border);
  border-radius: var(--r-lg, 15px);
  box-shadow: var(--sh-1);
  /* Same 20px inset as the list rows below, so the band, the rail and the
     table all sit on one left edge instead of three near-misses. */
  padding: var(--sp-5, 20px) var(--sp-6, 24px) var(--sp-4, 16px);
  margin-bottom: var(--sp-3, 12px);
}
.ui-band__top { display: flex; align-items: flex-end; gap: 26px; flex-wrap: wrap; }
.ui-band__kpi {
  display: flex; flex-direction: column; gap: 3px;
  padding: 3px 7px; margin: -3px -7px;
  border: 0; background: none; color: inherit; text-align: left;
  border-radius: var(--r-xs, 7px); cursor: pointer;
  transition: background var(--dur-2, .15s) var(--out, ease);
}
.ui-band__kpi:hover { background: var(--inv-card-hover); }
.ui-band__kpi:focus-visible { outline: var(--focus-ring); outline-offset: var(--focus-offset); }
/* Active filter reads as a pressed chip, not just a colour shift. */
.ui-band__kpi.is-active { background: var(--inv-card-hover); box-shadow: inset 0 0 0 1px var(--inv-border-2); }
.ui-band__k {
  display: flex; align-items: center; gap: 6px;
  font-size: 11.5px; font-weight: 600; color: var(--inv-muted); letter-spacing: .02em;
}
.ui-band__dot { width: 7px; height: 7px; border-radius: 50%; flex: none; }
.ui-band__v {
  font-size: 23px; font-weight: 700; letter-spacing: -.02em; line-height: 1.1;
  color: var(--inv-text); font-variant-numeric: tabular-nums;
}
/* Reserved even when empty — the KPIs bottom-align, so a sub-less KPI would
   otherwise drop its value onto its neighbours' sub-text line. summarybar.js
   always emits the span; min-height keeps the row at its natural height. */
.ui-band__sub { font-size: 11.5px; line-height: 1.45; min-height: 1.45em; color: var(--inv-muted); }
.ui-band__kpi.is-lead { padding-right: 26px; border-right: 1px solid var(--inv-border); margin-right: 0; }
.ui-band__kpi.is-lead .ui-band__v { font-size: 30px; }
.ui-band__kpi.is-hot .ui-band__v { color: var(--doc-overdue); }
/* Proportional split. Segments carry a title so the breakdown is reachable
   without relying on the colours alone. */
.ui-band__bar {
  display: flex; gap: 2px; height: 7px; margin-top: 15px;
  border-radius: 999px; background: var(--inv-input-bg); overflow: hidden;
}
.ui-band__bar i { display: block; height: 100%; border-radius: 2px; }

/* ── Status rail ─────────────────────────────────────────────────────────── */
.ui-rail {
  display: flex; gap: 6px; margin: var(--sp-3_5, 14px) 0 0;
  overflow-x: auto; scrollbar-width: none; padding-bottom: 2px;
}
.ui-rail::-webkit-scrollbar { display: none; }
.ui-rail__seg {
  display: inline-flex; align-items: center; gap: 7px;
  height: 32px; padding: 0 13px; border-radius: 999px;
  border: 1px solid var(--inv-border); background: var(--inv-card);
  color: var(--inv-text-2); font-size: 12.5px; font-weight: 600;
  white-space: nowrap; cursor: pointer;
  transition: background var(--dur-2, .15s) var(--out, ease), color var(--dur-2, .15s) var(--out, ease), border-color var(--dur-2, .15s) var(--out, ease);
}
.ui-rail__seg:hover { background: var(--inv-card-hover); color: var(--inv-text); }
.ui-rail__seg:focus-visible { outline: var(--focus-ring); outline-offset: var(--focus-offset); }
.ui-rail__seg b { font-size: 11px; font-weight: 700; color: var(--inv-muted); font-variant-numeric: tabular-nums; }
.ui-rail__seg i.ti { font-size: 14px; }
.ui-rail__dot { width: 7px; height: 7px; border-radius: 50%; flex: none; }
.ui-rail__seg.is-active { background: var(--inv-accent-soft, rgba(51,177,255,.12)); border-color: var(--inv-accent); color: var(--inv-accent); }
.ui-rail__seg.is-active b { color: var(--inv-accent); }
.ui-rail__seg.is-hot { border-color: color-mix(in srgb, var(--doc-overdue) 45%, transparent); color: var(--doc-overdue); }
.ui-rail__seg.is-hot b { color: var(--doc-overdue); }

/* ── v2 list row ─────────────────────────────────────────────────────────── */
/* ── Column template ────────────────────────────────────────────────────────
   The template lives in a CUSTOM PROPERTY and is applied by a separate,
   higher-specificity selector below. That indirection is deliberate:

   books-v3 defines its own five-column row grid —
     html.books-v3 .card:not(.is-cards) .row       specificity (0,4,1)
   which outranks a plain html.books-list-v2 .row--v2 at (0,2,1). With both
   flags on — which is how they shipped — v3 won, and its five tracks were
   applied to the v2 row's seven children, mangling the list in production.

   Custom properties cascade per-property, so the breakpoint overrides below
   stay simple and low-specificity while only the rule that CONSUMES them has
   to outrank v3. Do not inline these templates back into the selectors:
   that is exactly the bug. */
html.books-list-v2 {
  --v2-cols: 26px 96px minmax(0, 1.7fr) minmax(0, 1.3fr) 132px 122px;
}
html.books-list-v2 .dl-head--v2,
html.books-list-v2 .row--v2 { gap: var(--sp-4, 16px); align-items: center; padding: 0 var(--sp-6, 24px); }
/* Matched to books-v3's shape (0,4,1) and later in the file, so it wins. */
/* Geometry is set HERE, at v3-beating specificity, not in the low-specificity
   rule above: v3 also sets `padding: 15px 18px` on rows and `11px 18px` on the
   head at (0,4,1). Leaving padding to lose meant the head and the rows could be
   inset by different amounts — which reads as columns that don't line up even
   when the templates match. Head and rows share one horizontal inset. */
html.books-list-v2 .card:not(.is-cards) .dl-head--v2,
html.books-list-v2 .card:not(.is-cards) .row--v2,
html.books-list-v2 .dl-head--v2,
html.books-list-v2 .row--v2 {
  display: grid; grid-template-columns: var(--v2-cols);
  gap: var(--sp-4, 16px); align-items: center;
  padding-left: var(--sp-6, 24px); padding-right: var(--sp-6, 24px);
}
/* Vertical room. 9px + a two-line cell left the rows touching their dividers;
   14px on the 4/8 rhythm gives the customer/badge stack somewhere to sit. */
html.books-list-v2 .card:not(.is-cards) .row--v2,
html.books-list-v2 .row--v2 { padding-top: 18px; padding-bottom: 18px; }
html.books-list-v2 .card:not(.is-cards) .dl-head--v2,
html.books-list-v2 .dl-head--v2 { padding-top: 0; padding-bottom: 0; }
/* NO track for .row-actions, deliberately. cfg.actions is always supplied by
   both pages, so every row renders a .row-actions cell — even an empty one.
   Giving it a 7th `auto` track meant the header (six spans) and the rows (seven
   cells) disagreed, AND a row with a "Remind" button sized its columns
   differently from one without. Overlaying the actions on hover — the same
   thing books-v3 does — keeps every row and the header on one template, so
   they cannot drift. */
html.books-list-v2 .row--v2 { position: relative; }
/* The pin itself is declared further down, after books-v3's own .row-actions
   rules, so it wins on order. */
html.books-list-v2 .row--v2 .row-actions:empty { display: none; }

/* books-v3 also assigns explicit `order` to the CLASSIC row's children —
   .row-check:1, .row-main:2, .row-badges:3, .row-total:4, .row-date:5,
   .row-actions:6 — to lay its five columns out. The v2 row shares three of
   those class names but has seven cells, so the four it does NOT name
   (.row-num, .row-job, .row-when, .row-amt) keep the default order: 0 and jump
   AHEAD of the checkbox and customer. The result is a correctly-sized grid
   with its contents shuffled: number, job, due, amount, checkbox, customer.
   Reset every v2 cell to source order and let the DOM decide.

   Specificity matches v3's (0,4,1) and this is later in the file, so it wins. */
html.books-list-v2 .card:not(.is-cards) .row--v2 > *,
html.books-list-v2 .row--v2 > * { order: 0; }
/* .row-actions stays OUT of flow — see the "NO track" note above. --v2-cols
   declares six tracks and the row renders seven cells, so a statically-placed
   actions cell lands in an implicit seventh column past the row's right edge
   and .card's `overflow: hidden` clips it: the "Schedule" button on an accepted
   quote (and "Remind" on an overdue invoice) rendered off the table entirely.
   The pin below is re-stated here, after v3's (0,4,1) rules, so it wins. */
html.books-list-v2 .card:not(.is-cards) .row--v2 .row-actions,
html.books-list-v2 .row--v2 .row-actions {
  position: absolute; right: var(--sp-6, 24px); top: 50%; transform: translateY(-50%);
  width: auto; padding: 4px 6px;
  background: var(--inv-card-hover); border-radius: 8px; box-shadow: var(--sh-1);
}
html.books-list-v2 .dl-head--v2 {
  height: 44px; background: var(--inv-input-bg);
  border-bottom: 1px solid var(--inv-border);
  position: sticky; top: 0; z-index: 2;
}
html.books-list-v2 .dl-head--v2 span {
  font-size: 11px; font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; color: var(--inv-muted);
}
html.books-list-v2 .dl-head--v2 .r { text-align: right; }
/* Vertical padding rather than height alone: a row whose badge strip wraps to a
   second line grows instead of pressing its content against the divider. */
html.books-list-v2 .row--v2 {
  min-height: 80px;
  border-bottom: 1px solid var(--inv-border);
  position: relative; cursor: pointer;
}
html.books-list-v2 .row--v2:last-child { border-bottom: 0; }
html.books-list-v2 .row--v2:hover { background: var(--inv-card-hover); }
/* Overdue gets a colour edge IN ADDITION to its badge and its "N days late"
   wording — three cues, so the state never rides on colour alone. */
html.books-list-v2 .row--v2.is-late::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0;
  width: 3px; background: var(--doc-overdue);
}
html.books-list-v2 .row--v2 .row-main { display: flex; flex-direction: column; gap: 8px; min-width: 0; padding-right: 8px; }
html.books-list-v2 .row--v2 .row-cust {
  font-size: 13.5px; font-weight: 600; color: var(--inv-text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* row-gap is smaller than column-gap on purpose: when the strip wraps, the two
   badge lines should read as one block rather than as two separate rows. */
html.books-list-v2 .row--v2 .row-badges {
  display: flex; align-items: center; gap: 4px 7px; flex-wrap: wrap; margin: 0; min-width: 0;
}
/* In v2 the badge strip is the row's SECOND line — supporting detail under the
   customer, not the headline it is on the classic row. Trimming it to the same
   11.5px the number and sub-lines use buys back enough width for status +
   viewed + sent-via + delivery to sit on one line at the default column width,
   and keeps the strip from competing with the customer name above it. */
html.books-list-v2 .row--v2 .row-badges .ui-badge {
  font-size: 11.5px; padding: 2px 8px; gap: 3px;
}
/* Tabular figures so the numbers stack into a readable column rather than
   jittering; --inv-text-2 not --inv-muted, because this is a real identifier
   the customer will quote back at you, not decoration. */
html.books-list-v2 .row--v2 .row-num {
  font-size: 12.5px; font-weight: 600; color: var(--inv-text-2);
  font-variant-numeric: tabular-nums; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
}
/* A draft has no number yet. Saying so keeps the column from going ragged, but
   it is a placeholder, not an identifier — so it reads as one. */
html.books-list-v2 .row--v2 .row-num.is-unnumbered {
  color: var(--inv-muted); font-weight: 500; font-style: italic;
}
/* Job names are the longest text in the row — "AWHC 16321 - Stean Nicolls R/E
   - Service" and the like — so cutting them at one line loses the part that
   identifies the work. Wrap to two (truncation-strategy: prefer wrapping), and
   only then ellipsize; the cell already carries a title attribute with the
   full text. Two lines of 12.5px fit the 80px row without growing it. */
html.books-list-v2 .row--v2 .row-job {
  font-size: 12.5px; line-height: 1.42; color: var(--inv-text-2);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; text-overflow: ellipsis; white-space: normal;
  overflow-wrap: anywhere;
}
html.books-list-v2 .row--v2 .row-when { display: flex; flex-direction: column; gap: 3px; }
html.books-list-v2 .row--v2 .row-when b { font-size: 12.5px; font-weight: 500; color: var(--inv-text-2); }
html.books-list-v2 .row--v2 .row-when small { font-size: 11px; color: var(--inv-muted); }
html.books-list-v2 .row--v2 .row-when.is-late b { color: var(--doc-overdue); font-weight: 600; }
html.books-list-v2 .row--v2 .row-when.is-soon b { color: var(--doc-partial); font-weight: 600; }
html.books-list-v2 .row--v2 .row-amt { display: flex; flex-direction: column; gap: 3px; text-align: right; }
html.books-list-v2 .row--v2 .row-amt b {
  font-size: 14.5px; font-weight: 700; letter-spacing: -.01em;
  color: var(--inv-text); font-variant-numeric: tabular-nums;
}
html.books-list-v2 .row--v2 .row-amt small { font-size: 11px; color: var(--inv-muted); }
html.books-list-v2 .row--v2 .row-amt small.is-due { color: var(--doc-partial); font-weight: 600; }
/* Row actions stay hidden until the row is hovered or something inside it has
   focus — keyboard users get the same reach as the mouse. */
html.books-list-v2 .row--v2 .row-actions { opacity: 0; transition: opacity var(--dur-2, .15s) var(--out, ease); }
html.books-list-v2 .row--v2:hover .row-actions,
html.books-list-v2 .row--v2:focus-within .row-actions { opacity: 1; }

/* ── Narrow: shed the job column first; the number column stays ──────────── */
@media (max-width: 1080px) {
  html.books-list-v2 { --v2-cols: 26px 100px minmax(0, 1fr) 128px 116px; }
  html.books-list-v2 .dl-head--v2 span:nth-child(4),
  html.books-list-v2 .row--v2 .row-job { display: none; }
}
@media (max-width: 760px) {
  html.books-list-v2 .wrap { max-width: none; }
  html.books-list-v2 .ui-band__top { gap: 16px; }
  html.books-list-v2 .ui-band__kpi.is-lead { border-right: 0; padding-right: 0; }
  /* Card rows: the amount keeps its own column, everything else stacks. The
     number loses its column but not its place — it sits on the first line
     beside the checkbox, still ahead of the customer. */
  html.books-list-v2 .dl-head--v2 { display: none; }
  html.books-list-v2 { --v2-cols: 22px minmax(0, 1fr) auto; }
  html.books-list-v2 .row--v2 { row-gap: 9px; padding: var(--sp-4, 16px); }
  html.books-list-v2 .row--v2 .row-num { grid-column: 2; grid-row: 1; font-size: 11.5px; }
  html.books-list-v2 .row--v2 .row-main { grid-column: 2; }
  html.books-list-v2 .row--v2 .row-when { grid-column: 2; font-size: 11.5px; }
  html.books-list-v2 .row--v2 .row-when b { font-size: 11.5px; }
  html.books-list-v2 .row--v2 .row-amt { grid-row: 1; grid-column: 3; }
  /* Both selectors, to out-specify the (0,4,1) pin above — the card row stacks
     here, so the actions get a real line of their own instead of an overlay. */
  html.books-list-v2 .card:not(.is-cards) .row--v2 .row-actions,
  html.books-list-v2 .row--v2 .row-actions {
    position: static; right: auto; top: auto; transform: none;
    grid-column: 2 / -1; opacity: 1;
    background: transparent; box-shadow: none; padding: 0;
  }
}

/* ══════════════════════════════════════════════════════════════════════════
   DOCUMENT HEADER V2 — feature_doc_header_v2 (pre-paint adds html.doc-header-v2;
   books-bar.js applyHeaderV2Gate keeps the cached class in sync). Replaces
   .ed-top / .ed-status on the invoice + quote editors. Flag OFF → no rules
   below match anything, so the classic header renders exactly as today.

     .ed-head2  — sticky identity/status/money/next-action bar
     .ui-steps  — lifecycle stepper (core/ui/docsteps.js ndDocSteps)
   ══════════════════════════════════════════════════════════════════════════ */
.ed-head2 {
  position: sticky;
  /* Clears whichever chrome is above: the unified nav and the legacy books bar
     are both 58px, and books.css already pins them to the top of the page. */
  top: 0;
  z-index: var(--z-sticky, 20);
  margin: 0 0 16px;   /* no negative offset: it dragged the header over the node above it */
  background: var(--inv-surface);
  border: 1px solid var(--inv-border);
  border-radius: var(--r-lg, 15px);
  box-shadow: var(--sh-1);
}
.ed-head2__main { display: flex; align-items: center; gap: 14px; padding: 12px 16px; }
.ed-head2__back {
  width: 32px; height: 32px; flex: none;
  display: grid; place-items: center;
  border: 1px solid var(--inv-border); border-radius: var(--r-xs, 7px);
  background: var(--inv-card); color: var(--inv-text-2); cursor: pointer;
  transition: background var(--dur-2, .15s) var(--out, ease), color var(--dur-2, .15s) var(--out, ease);
}
.ed-head2__back:hover { background: var(--inv-card-hover); color: var(--inv-text); }
.ed-head2__back:focus-visible { outline: var(--focus-ring); outline-offset: var(--focus-offset); }
.ed-head2__id { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.ed-head2__row { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; }
.ed-head2__row h2 {
  margin: 0; font-size: 17px; font-weight: 700; letter-spacing: -.01em;
  color: var(--inv-text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 42ch;
}
/* The number is an identifier you read back to someone — boxed and tabular so
   it is findable at a glance, not folded into the badge run. */
.ed-head2__no {
  font-size: 12px; font-weight: 600; color: var(--inv-text-2);
  font-variant-numeric: tabular-nums;
  border: 1px solid var(--inv-border); border-radius: 6px; padding: 2px 7px;
}
/* Informational sibling of .warn — a deposit invoice saying where its balance
   comes from. Deliberately NOT amber: nothing is wrong, and dressing a plain
   signpost as a warning is how warning colour stops meaning anything. */
/* Block, not flex — same as .warn. As a flex row the whole sentence is ONE
   anonymous item, so the moment it is too wide to share the line it wraps
   underneath and strands the icon on a row of its own. Inline flow keeps the
   icon, the sentence and the trailing link/button in one paragraph. */
.inv-depnote {
  margin: 0 0 14px; padding: 10px 14px;
  border: 1px solid var(--inv-border); border-radius: var(--r-md, 11px);
  background: var(--inv-input-bg);
  font-size: 13px; line-height: 1.55; color: var(--inv-text-2);
}
.inv-depnote i { font-size: 15px; opacity: .7; margin-right: 5px; vertical-align: -2px; }
.inv-depnote a { color: var(--inv-accent); font-weight: 600; text-decoration: none; white-space: nowrap; }
.inv-depnote a:hover { text-decoration: underline; }
.inv-depnote .btn-sm { vertical-align: -1px; }

/* "This document is a teammate's" — invoices and quotes are org-readable but
   owner-writable (no org UPDATE policy), so a member can open every document in
   the workspace and save none of them. Warmer than the .warn band because
   nothing is wrong: it is a boundary, not an error. */
.doc-notowner {
  display: flex; align-items: flex-start; gap: 8px;
  margin: 0 0 14px; padding: 10px 14px;
  border: 1px solid var(--inv-border); border-radius: var(--r-md, 11px);
  background: var(--inv-input-bg);
  font-size: 13px; line-height: 1.55; color: var(--inv-text-2);
}
.doc-notowner i { font-size: 15px; opacity: .7; flex: none; margin-top: 1px; }

/* Linked-record chips (ndLifecycle compact) ride the title row, so "what job is
   this attached to?" is answered in the sticky header instead of in a band
   below it that scrolls away. They sit AFTER the number so the identity reads
   first — name, number, then what it hangs off. Empty collapses to nothing:
   the row's gap must not open for a slot with no chips in it. */
.ed-head2__links { display: inline-flex; min-width: 0; }
.ed-head2__links:empty { display: none; }
/* Only the type scale is Books' business here — the compact variant's own
   spacing (ui.css) owns how the chips group, so don't override its gap. */
.ed-head2__links .ui-lc { font-size: 11.5px; }
/* One notch smaller than the free-standing strip: beside a 17px title these are
   an aside, and at the band's size they competed with the customer's name. */
.ed-head2__links .ui-lc__step { padding: 2px 8px; gap: 5px; }
.ed-head2__links .ui-lc__step i:first-child { font-size: 12.5px; }
.ed-head2__links .ui-lc__step em { font-size: 10.5px; }
.ed-head2__warn {
  display: inline-flex; align-items: center; gap: 5px;
  height: 22px; padding: 0 9px; border-radius: 999px;
  font-size: 11.5px; font-weight: 700;
  color: var(--doc-expired);
  background: color-mix(in srgb, var(--doc-expired) 14%, transparent);
}
.ed-head2__warn i { font-size: 13px; }
/* One line, one fact: what work this document covers. Truncates rather than
   wrapping, so the header keeps a fixed height however long the job name is. */
.ed-head2__sub {
  font-size: 12px; color: var(--inv-text-2); min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ed-head2__money { text-align: right; flex: none; }
.ed-head2__money b {
  display: block; font-size: 21px; font-weight: 700; letter-spacing: -.02em;
  line-height: 1.15; color: var(--inv-text); font-variant-numeric: tabular-nums;
}
.ed-head2__money small { font-size: 11.5px; color: var(--inv-muted); }
.ed-head2__money small.is-due { color: var(--doc-partial); font-weight: 600; }
.ed-head2__act {
  display: flex; align-items: center; gap: 10px; flex: none;
  padding-left: 14px; border-left: 1px solid var(--inv-border);
}
.ed-head2__act .btn-primary { white-space: nowrap; }

/* ── Lifecycle stepper ───────────────────────────────────────────────────── */
.ui-steps {
  display: flex; align-items: center; gap: 0;
  padding: 0 16px 11px;
  overflow-x: auto; scrollbar-width: none;
}
.ui-steps::-webkit-scrollbar { display: none; }
.ui-steps__step {
  display: inline-flex; align-items: center; gap: 7px; flex: none;
  padding: 4px 10px; border-radius: 999px; border: 1px solid transparent;
  font-size: 11.5px; font-weight: 600; color: var(--inv-muted); white-space: nowrap;
}
.ui-steps__step i { font-size: 14px; }
.ui-steps__lbl { display: inline-flex; align-items: baseline; gap: 6px; }
.ui-steps__lbl small { font-weight: 500; opacity: .75; }
/* Done / now / todo are distinguished by ICON SHAPE as well as colour (filled
   check · ring · hollow circle), so the state survives a colour-blind read. */
.ui-steps__step.is-done { color: var(--inv-text-2); }
.ui-steps__step.is-done i { color: var(--doc-accepted); }
.ui-steps__step.is-now {
  color: var(--inv-accent);
  background: var(--inv-accent-soft, rgba(51,177,255,.12));
  border-color: var(--inv-accent);
}
.ui-steps__step.is-now i { color: var(--inv-accent); }
.ui-steps__line {
  width: 22px; height: 1.5px; flex: none; border-radius: 2px;
  background: var(--inv-border-2);
}
/* The travelled run stops where the document actually got to. */
.ui-steps__line.is-done { background: color-mix(in srgb, var(--doc-accepted) 45%, transparent); }
.ui-steps__trail { margin-left: auto; padding-left: 14px; flex: none; }
.mini-link {
  display: inline-flex; align-items: center; gap: 6px;
  height: 26px; padding: 0 10px; border-radius: 999px;
  border: 1px solid var(--inv-border); background: var(--inv-card);
  color: var(--inv-text-2); font-size: 11.5px; font-weight: 600; cursor: pointer;
  transition: background var(--dur-2, .15s) var(--out, ease), color var(--dur-2, .15s) var(--out, ease);
}
.mini-link:hover { background: var(--inv-card-hover); color: var(--inv-text); }
.mini-link:focus-visible { outline: var(--focus-ring); outline-offset: var(--focus-offset); }
.mini-link i { font-size: 14px; }

/* ── Narrow: the header stops being one line ─────────────────────────────── */
@media (max-width: 900px) {
  .ed-head2__main { flex-wrap: wrap; row-gap: 11px; }
  .ed-head2__id { order: 1; flex: 1 1 60%; }
  .ed-head2__money { order: 2; margin-left: auto; }
  /* Full-width action row so the primary stays a comfortable tap target
     instead of being squeezed by the money block beside it. */
  .ed-head2__act {
    order: 3; flex: 1 1 100%; padding-left: 0; border-left: 0;
    padding-top: 11px; border-top: 1px solid var(--inv-border);
  }
  .ed-head2__act .btn-primary { flex: 1; justify-content: center; min-height: 40px; }
}
/* On a phone the header stays PINNED but goes compact — one line carrying the
   two things you need while editing: who it's for, and what it comes to.
   (An earlier revision made it static instead. That was wrong: it left the
   phone with no persistent total, which then pushed the totals dock into
   pinning as well — and the dock would have collided with .ed-actions, which
   is itself sticky under 561px. One compact pinned bar beats three.) */
@media (max-width: 640px) {
  .ed-head2 { margin-bottom: 12px; }
  .ed-head2__main { padding: 9px 12px; gap: 10px; }
  .ed-head2__row h2 { font-size: 15px; max-width: none; }
  .ed-head2__money b { font-size: 17px; }
  .ed-head2__money small,
  .ed-head2__sub,
  .ed-head2__links,
  .ed-head2 .ui-steps { display: none; }
  /* The primary action lives in the sticky .ed-actions bar at the bottom on a
     phone — thumb-reachable — so the header does not carry it as well. */
  .ed-head2__act { display: none; }
}

/* ══════════════════════════════════════════════════════════════════════════
   EDITOR BODY V2 — feature_doc_editor_v2 (pre-paint adds html.doc-editor-v2;
   books-bar.js applyEditorV2Gate keeps the cached class in sync).

   DELIBERATELY CSS-ONLY. The line-item table keeps the exact <tr>/<td> markup
   core/ui/lineitems.js emits, so drag-reorder, the catalog typeahead, the
   line_type heading/text rows and every updItem(index, …) handler are
   untouched — the redesign is a restyle, not a re-render. It is also still a
   real <table>, which is what this data is.

   Flag OFF → none of these selectors match, so the editor renders as today.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── The items block reads as one card: .items-head is its header, the table
      its body. They are already adjacent siblings in renderEditor. ───────── */
html.doc-editor-v2 .items-head {
  margin: 22px 0 0;
  padding: 11px 15px;
  background: var(--inv-input-bg);
  border: 1px solid var(--inv-border);
  border-bottom: 0;
  border-radius: var(--r-lg, 15px) var(--r-lg, 15px) 0 0;
}
html.doc-editor-v2 .items-head h3 { font-size: 12.5px; font-weight: 700; letter-spacing: .02em; }
/* The add-line / catalog / AI actions become chips rather than a run of links.
   flex: 0 0 auto is load-bearing, not decoration. These sit in .items-head's
   space-between row, so as flex items they default to flex-shrink: 1 — the
   PILL shrinks under pressure while the text inside it does not, and the label
   spills out of its own border. Refusing to shrink (and wrapping instead, see
   below) is what keeps a pill wrapped around its text. */
html.doc-editor-v2 .items-head .link {
  display: inline-flex; align-items: center; gap: 6px;
  flex: 0 0 auto; white-space: nowrap;
  height: 28px; padding: 0 10px; border-radius: 999px;
  border: 1px solid var(--inv-border); background: var(--inv-card);
  color: var(--inv-text-2); font-size: 11.5px; font-weight: 600;
  text-decoration: none;
}
/* With shrinking off, the row needs somewhere to go when it runs out of width:
   wrap to a second line rather than overflow. The action <span> carries an
   inline display:flex from renderEditor, so only wrap/gap are set here. */
html.doc-editor-v2 .items-head { flex-wrap: wrap; row-gap: 10px; }
html.doc-editor-v2 .items-head > span { flex-wrap: wrap; row-gap: 8px; justify-content: flex-end; }
html.doc-editor-v2 .items-head .link:hover { background: var(--inv-card-hover); color: var(--inv-text); }
html.doc-editor-v2 .items-head .link.ai-link { color: var(--inv-accent); border-color: var(--inv-accent); background: var(--inv-accent-soft, rgba(51,177,255,.12)); }
html.doc-editor-v2 table.ed-items { border: 1px solid var(--inv-border); border-radius: 0 0 var(--r-lg, 15px) var(--r-lg, 15px); }
html.doc-editor-v2 table.ed-items th {
  font-size: 10.5px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase;
  color: var(--inv-muted); padding: 8px 10px;
  border-bottom: 1px solid var(--inv-border);   /* 2px double-rule was heavy */
  background: var(--inv-input-bg);
}
/* Money and quantities are columns of figures — right-align and use tabular
   numerals so they compare down the column instead of jittering. */
html.doc-editor-v2 table.ed-items th:nth-last-child(-n+4),
html.doc-editor-v2 table.ed-items td[data-label="Qty"],
html.doc-editor-v2 table.ed-items td[data-label="Unit $"],
html.doc-editor-v2 table.ed-items td[data-label="Disc %"],
html.doc-editor-v2 table.ed-items td[data-label="Disc"] { text-align: right; }
html.doc-editor-v2 table.ed-items td,
html.doc-editor-v2 table.ed-items input,
html.doc-editor-v2 .ed-totals { font-variant-numeric: tabular-nums; }
/* books.css sets no font-size on <body>, so table cells inherit the browser
   default 16px — while the inputs inside them are 14px. The effect is that a
   FINALIZED document (plain text cells) renders visibly larger than the same
   document as a draft (input cells). Pin the cells to the input size so the
   two states match. */
html.doc-editor-v2 table.ed-items td { padding: 7px 10px; font-size: 14px; }
html.doc-editor-v2 table.ed-items td.num { font-weight: 600; }
html.doc-editor-v2 table.ed-items tbody tr:hover { background: var(--inv-card-hover); }

/* Inputs lose their boxes until you reach for them — a table of 8 outlined
   fields per row reads as a form, not as the document it is producing. The
   border is only hidden, never removed, so nothing reflows on hover/focus. */
html.doc-editor-v2 table.ed-items td input,
html.doc-editor-v2 table.ed-items td textarea {
  border-color: transparent; background: transparent;
  transition: border-color var(--dur-2, .15s) var(--out, ease), background var(--dur-2, .15s) var(--out, ease);
}
html.doc-editor-v2 table.ed-items td input:hover,
html.doc-editor-v2 table.ed-items td textarea:hover { border-color: var(--inv-border); }
html.doc-editor-v2 table.ed-items td input:focus,
html.doc-editor-v2 table.ed-items td textarea:focus {
  border-color: var(--inv-accent); background: var(--inv-input-bg);
}
/* A pasted scope can run twenty lines, and the textarea auto-grows to fit it
   (ndLineItems.grow + field-sizing), which turned a single line item into a
   400px row and stranded the qty/price/GST cells in the middle of it.

   The cell is clamped to roughly four lines; the expand control beside it opens
   the same value in a proper writing box (ndLineItems.editDesc). Progressive
   disclosure: the table stays scannable, and writing gets a surface built for
   it instead of a 250px column. max-height beats the inline height grow() sets,
   so auto-sizing still works below the cap. */
/* Flag OFF: no clamp, so no expand affordance either. */
table.ed-items .it-desc-expand { display: none; }
html.doc-editor-v2 table.ed-items .it-desc-expand { display: grid; }
html.doc-editor-v2 table.ed-items td.it-desc { position: relative; }
/* NO max-height. A clamp you have to discover is worse than a tall row: the
   text was there, scrollable, with no reliable signal that anything was
   hidden — so a scope could silently read as complete when it wasn't. On a
   quote that is a correctness problem, not a cosmetic one.
   The cell grows to its content (ndLineItems.grow); the cells are top-aligned
   so the numbers sit against the first line rather than floating beside the
   middle of a tall block; and the expand control is still there for comfort
   when writing, not as the only way to READ. */
html.doc-editor-v2 table.ed-items textarea.it-desc-input { padding-right: 28px; }
html.doc-editor-v2 table.ed-items .it-desc-expand {
  position: absolute; top: 5px; right: 4px;
  width: 22px; height: 22px; padding: 0;
  display: grid; place-items: center;
  border: 1px solid var(--inv-border); border-radius: 6px;
  background: var(--inv-card); color: var(--inv-muted);
  cursor: pointer; opacity: .55;
  transition: opacity var(--dur-2, .15s) var(--out, ease), color var(--dur-2, .15s) var(--out, ease);
}
html.doc-editor-v2 table.ed-items .it-desc-expand i { font-size: 13px; }
/* Always present, brighter on hover. Unlike the × — which only removes
   something already visible — this reveals text the clamp is hiding, so it
   cannot be a hover-only affordance. */
html.doc-editor-v2 table.ed-items tr:hover .it-desc-expand,
html.doc-editor-v2 table.ed-items tr:focus-within .it-desc-expand { opacity: 1; }
html.doc-editor-v2 table.ed-items .it-desc-expand:hover { color: var(--inv-accent); border-color: var(--inv-accent); }
html.doc-editor-v2 table.ed-items .it-desc-expand:focus-visible { opacity: 1; outline: var(--focus-ring); outline-offset: var(--focus-offset); }
@media (hover: none) {
  html.doc-editor-v2 table.ed-items .it-desc-expand { opacity: 1; }
}

/* With a capped cell, top-align the row so the numbers sit against the first
   line of the description rather than floating beside its middle. */
html.doc-editor-v2 table.ed-items td { vertical-align: top; }
html.doc-editor-v2 table.ed-items td.dragcell { padding-top: 13px; }

html.doc-editor-v2 table.ed-items input.w-qty,
html.doc-editor-v2 table.ed-items input.w-price,
html.doc-editor-v2 table.ed-items input.w-disc { text-align: right; }

/* Heading + free-text rows are STRUCTURE, not broken product rows.
   lineitems.js sets these two via INLINE style attributes, which no class rule
   can beat — hence !important here. Two things it gets wrong that matter once
   the document is finalized:

   1. the note colour is `var(--text-muted, #94a3b8)`, and --text-muted is not a
      books token, so it always resolves to the hardcoded slate. On the LIGHT
      theme that is ~2.6:1 on white — below the 4.5:1 AA floor for body text.
      --inv-text-2 is theme-aware and passes in both.
   2. the heading is font-weight 600 read-only but 700 while editable, so the
      same document changes weight when you finalize it. One weight, both states. */
html.doc-editor-v2 table.ed-items tr.li-heading { background: var(--inv-input-bg); }
html.doc-editor-v2 table.ed-items tr.li-heading .it-desc-input,
html.doc-editor-v2 table.ed-items tr.li-heading td.it-desc {
  font-size: 12px; font-weight: 700 !important; letter-spacing: .06em;
  text-transform: uppercase; color: var(--inv-text-2) !important;
}
html.doc-editor-v2 table.ed-items tr.li-text .it-desc-input,
html.doc-editor-v2 table.ed-items tr.li-text td.it-desc {
  font-style: italic; font-size: 12.5px; color: var(--inv-text-2) !important;
}

/* …AND ON A PHONE THOSE TWO RULES BREAK THE ONE ABOVE THEM. The card layout at
   560px sets a 16px floor on every field in this table for one reason — below
   16px iOS zooms the whole page when you focus an input, and you are then
   pinch-scrolling a half-built invoice on a roof. But the heading and free-text
   styling above is desktop styling with no media query and a heavier selector
   (0,4,3 against 0,2,2), so it reached the phone too and put those two row
   types back at 12px and 12.5px.
   Restated here rather than at the 560px block, which comes EARLIER in the file
   and would lose on source order at equal specificity.

   The same two rows were also 100px tall to hold one line of text: td.it-desc
   claims the full width of the card, which pushes the delete button onto a
   second line. A heading has nothing else in it, so the two share the line. */
@media (max-width: 560px) {
  html.doc-editor-v2 table.ed-items tr.li-heading .it-desc-input,
  html.doc-editor-v2 table.ed-items tr.li-text .it-desc-input { font-size: 16px; }
  /* basis 0, not auto: the cell holds a width:100% textarea, so an `auto`
     basis resolves to the full card and the two still wrap. Zero lets both sit
     on the line and the text take what is left. */
  table.ed-items tr.li-heading td.it-desc,
  table.ed-items tr.li-text td.it-desc { flex: 1 1 0; min-width: 0; }
}

/* The × sits quiet until the row is hovered or focused within. Kept in the
   layout at all times (opacity, not display) so no column shifts. */
html.doc-editor-v2 table.ed-items td .x {
  opacity: 0; transition: opacity var(--dur-2, .15s) var(--out, ease);
}
html.doc-editor-v2 table.ed-items tr:hover .x,
html.doc-editor-v2 table.ed-items tr:focus-within .x { opacity: 1; }
html.doc-editor-v2 table.ed-items .draghandle { opacity: .45; }
html.doc-editor-v2 table.ed-items tr:hover .draghandle { opacity: 1; }

/* ── Coach chips ride under the items card rather than floating loose ────── */
/* #edCoach sits AFTER #edTotals in renderEditor, not after the items table —
   so it cannot be welded to the bottom of the items card. Styling it that way
   (border-top: 0, only the bottom corners rounded) left an open-topped box
   floating between the totals dock and the action bar. It is its own block, so
   it gets its own complete frame. */
html.doc-editor-v2 #edCoach:not(:empty) {
  display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
  margin: 0 0 18px;
  padding: 12px 15px;
  background: var(--inv-card);
  border: 1px solid var(--inv-border);
  border-radius: var(--r-lg, 15px);
}

/* ── Totals dock ─────────────────────────────────────────────────────────
   The running totals become a horizontal bar rather than a block you scroll
   past. Whether it PINS depends on who else is showing the total:

   • header v2 off → this is the only running total, so it sticks to the
     bottom of the viewport and stays with you while you edit lines.
   • header v2 on  → the sticky header at the top already shows the total, and
     keeps it live per keystroke (syncHeaderTotal). A second pinned bar would
     show the same number twice and eat viewport from both ends, so this one
     stays in flow and is simply the breakdown you scroll to when you want it.

   The :not() below is what encodes that — do not "fix" it by making the dock
   unconditionally sticky. */
html.doc-editor-v2:not(.doc-header-v2) .ed-totals {
  position: sticky;
  bottom: var(--sp-3_5, 14px);
  z-index: var(--z-sticky, 20);
  box-shadow: var(--sh-2);
}
/* A DRAFT already pins .ed-actions--sticky (Save / Finalize) to bottom: 0 at
   ≥561px. Both bars pinned to the bottom of the same viewport, at the same
   z-index, land on top of each other — so the dock stacks above the action bar
   rather than sharing its space. --ed-actbar-h is that bar's resting height
   (12px padding × 2 + a 36px control + 1px rule); it only has to be right
   enough to clear it, and the bar wraps downward if its buttons ever do. */
html.doc-editor-v2 { --ed-actbar-h: 61px; }
@media (min-width: 561px) {
  html.doc-editor-v2:not(.doc-header-v2) #editorView:has(.ed-actions--sticky) .ed-totals {
    bottom: calc(var(--ed-actbar-h) + var(--sp-2, 8px));
  }
}
html.doc-editor-v2 .ed-totals {
  width: auto; margin: 18px 0;
  display: flex; align-items: center; gap: 22px; flex-wrap: wrap;
  padding: 12px 16px;
  background: var(--inv-surface);
  border: 1px solid var(--inv-border-2);
  border-radius: var(--r-lg, 15px);
  /* Resting elevation. The pinned variant above lifts to --sh-2, because a bar
     floating over content needs to read as floating; an in-flow card doesn't. */
  box-shadow: var(--sh-1);
}
html.doc-editor-v2 .ed-totals > div {
  display: flex; flex-direction: column; gap: 2px;
  justify-content: flex-start; padding: 0;
  font-size: 14px; font-weight: 600; color: var(--inv-text);
}
html.doc-editor-v2 .ed-totals > div span {
  font-size: 11px; font-weight: 600; letter-spacing: .03em; color: var(--inv-muted);
}
/* The grand total is the one you glance at — pushed to the far end and sized
   for it, so the eye lands there without reading the row. */
html.doc-editor-v2 .ed-totals > div.grand {
  margin-left: auto; text-align: right;
  border-top: 0; padding-top: 0; font-size: 21px; font-weight: 700; letter-spacing: -.02em;
}
html.doc-editor-v2 .ed-totals > div.grand span { color: var(--inv-muted); font-size: 11px; }
/* Two grand rows can co-exist (Total + Amount due); only the last goes big. */
html.doc-editor-v2 .ed-totals > div.grand:not(:last-child) {
  margin-left: auto; font-size: 14px; font-weight: 600;
}

/* ── Details card polish ─────────────────────────────────────────────────── */
html.doc-editor-v2 .ed-grid { border: 1px solid var(--inv-border); border-radius: var(--r-lg, 15px); }

/* ── Narrow ──────────────────────────────────────────────────────────────── */
@media (max-width: 760px) {
  /* The mobile table already collapses to stacked rows (books.css hides thead);
     keep the card framing but let the dock sit above the bottom tab bar. */
  /* Only the pinned variant needs to clear the bottom tab bar. */
  html.doc-editor-v2:not(.doc-header-v2) .ed-totals {
    bottom: calc(var(--books-navh) + 12px + env(safe-area-inset-bottom));
  }
  html.doc-editor-v2 .ed-totals { gap: 14px; padding: 11px 13px; }
  html.doc-editor-v2 .ed-totals > div.grand { font-size: 18px; }
  /* Touch has no hover: reveal the row controls permanently instead. */
  html.doc-editor-v2 table.ed-items td .x { opacity: 1; }
  html.doc-editor-v2 table.ed-items td input,
  html.doc-editor-v2 table.ed-items td textarea { border-color: var(--inv-border); }
}
/* Same reasoning for any pointer that cannot hover, at any width. */
@media (hover: none) {
  html.doc-editor-v2 table.ed-items td .x { opacity: 1; }
  html.doc-editor-v2 table.ed-items .draghandle { opacity: 1; }
  html.doc-editor-v2 table.ed-items td input,
  html.doc-editor-v2 table.ed-items td textarea { border-color: var(--inv-border); }
}

/* ── Native controls ─────────────────────────────────────────────────────────
   The color-scheme declaration that used to sit here has been promoted to the
   app-wide block at the end of this file — it was never a v2-specific problem.

   Spinner arrows earn nothing on a money or quantity field — they step by a
   useless increment, crowd the value, and fight the right-alignment the column
   depends on. Hiding them costs no function: the arrow KEYS still increment.
   Same treatment the catalog-import table already uses. */
html.doc-editor-v2 table.ed-items input[type="number"] {
  -moz-appearance: textfield; appearance: textfield;
}
html.doc-editor-v2 table.ed-items input[type="number"]::-webkit-outer-spin-button,
html.doc-editor-v2 table.ed-items input[type="number"]::-webkit-inner-spin-button {
  -webkit-appearance: none; margin: 0;
}

/* ══════════════════════════════════════════════════════════════════════════
   PHASE 4 — MOBILE. Most of the redesign's small-screen behaviour already fell
   out of the media queries in the three blocks above. What is left is the part
   that does NOT fall out: two places where the v2 rules fight the phone layout
   books.css already had, and one where two of my own flags interact badly.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── The totals dock NEVER pins on a phone ───────────────────────────────────
   Under 561px books.css already pins .ed-actions (Save / Finalize) to the
   bottom, above the bottom tab bar. A pinned dock would be a third fixed bar
   in a phone viewport and would overlap that action bar, whose height varies
   with how its buttons wrap — so there is no reliable offset to dodge it.

   Instead the compact sticky header above carries the running total on a
   phone (kept live per keystroke by syncHeaderTotal), and the dock stays in
   flow as the breakdown. With the header flag off there is no pinned total on
   a phone — which is exactly what the classic editor does today, so it is not
   a regression. */
@media (max-width: 640px) {
  html.doc-editor-v2 .ed-totals { position: static; box-shadow: var(--sh-1); }
  html.doc-editor-v2 .ed-totals > div.grand { margin-left: auto; }
}

/* ── Don't fight the phone card layout ───────────────────────────────────────
   books.css already turns the items table into one card per row under 560px.
   The v2 selectors carry an extra class (html.doc-editor-v2), so they outrank
   those rules and were quietly undoing them — the 14px cell size beat the 13px
   card size, and the 15px amount and the 16px iOS-zoom-guard inputs with it.
   These restore the phone layout's intent under the flag. */
@media (max-width: 560px) {
  html.doc-editor-v2 table.ed-items td { font-size: 13px; padding: 0; }
  html.doc-editor-v2 table.ed-items td.num { font-size: 15px; font-weight: 700; }
  /* 16px is not a style choice — anything smaller makes iOS zoom on focus. */
  html.doc-editor-v2 table.ed-items td input,
  html.doc-editor-v2 table.ed-items textarea.it-desc-input { font-size: 16px; min-height: 42px; }
  /* Inputs keep their boxes on a phone: hover-to-reveal has no meaning here,
     and each row is its own card, so the fields need their own edges. */
  html.doc-editor-v2 table.ed-items td input,
  html.doc-editor-v2 table.ed-items td textarea { border-color: var(--inv-border); }
  html.doc-editor-v2 table.ed-items td input:focus,
  html.doc-editor-v2 table.ed-items td textarea:focus { border-color: var(--inv-accent); }

  /* The card framing (header + table fused into one panel) is a desktop idea.
     Once each ROW is a card, framing the whole table as one too reads as a box
     inside a box, so the head goes back to being a plain heading row. */
  html.doc-editor-v2 .items-head {
    background: transparent; border: 0; border-radius: 0; padding: 0; margin: 22px 0 8px;
  }
  html.doc-editor-v2 table.ed-items { border: 0; border-radius: 0; }
  html.doc-editor-v2 #edCoach:not(:empty) { border-radius: var(--r-lg, 15px); border-top: 1px solid var(--inv-border); }
}

/* ── Section rail (core/ui/docrail.js, gated on html.doc-editor-v2) ──────────
   Sits under the sticky document header and names the sections THIS document
   has. Sticky itself, so it survives the scroll it exists to shortcut. */
.ui-docrail {
  position: sticky;
  top: 0;
  z-index: calc(var(--z-sticky, 20) - 1);   /* under the doc header, over content */
  display: flex; gap: 5px;
  margin: 0 0 16px; padding: 8px 0;
  background: var(--inv-bg);
  overflow-x: auto; scrollbar-width: none;
  scroll-behavior: smooth;
}
.ui-docrail::-webkit-scrollbar { display: none; }
.ui-docrail__seg {
  display: inline-flex; align-items: center; gap: 6px; flex: 0 0 auto;
  height: 30px; padding: 0 12px; border-radius: 999px; white-space: nowrap;
  border: 1px solid var(--inv-border); background: var(--inv-card);
  color: var(--inv-text-2); font-size: 12px; font-weight: 600; cursor: pointer;
  transition: background var(--dur-2, .15s) var(--out, ease), color var(--dur-2, .15s) var(--out, ease), border-color var(--dur-2, .15s) var(--out, ease);
}
.ui-docrail__seg:hover { background: var(--inv-card-hover); color: var(--inv-text); }
.ui-docrail__seg:focus-visible { outline: var(--focus-ring); outline-offset: var(--focus-offset); }
.ui-docrail__seg b { font-size: 11px; font-weight: 700; color: var(--inv-muted); font-variant-numeric: tabular-nums; }
.ui-docrail__seg.is-active {
  background: var(--inv-accent-soft, rgba(51,177,255,.12));
  border-color: var(--inv-accent); color: var(--inv-accent);
}
.ui-docrail__seg.is-active b { color: var(--inv-accent); }
/* The rail's own smooth-scroll is a nicety, not information. */
@media (prefers-reduced-motion: reduce) {
  .ui-docrail { scroll-behavior: auto; }
}

/* ══════════════════════════════════════════════════════════════════════════
   NATIVE CONTROL COLOUR SCHEME — app-wide, NOT flag-gated.
   books.css never declared a color-scheme, so on the dark theme the browser
   drew every native widget in light mode: <input type="date"> pickers, <select>
   chevrons, number-input spinners and inner scrollbars. Declaring it makes the
   UA draw them to match the surface they sit on. This is a fix to shipped UI
   for everyone, not part of the v2 flags — see the flag-scoped copy in the
   editor block above, which this makes redundant but harmless.
   ══════════════════════════════════════════════════════════════════════════ */
:root { color-scheme: dark; }
:root[data-theme="light"],
:root[data-books-theme="light"],
html.sched-light { color-scheme: light; }

/* ══════════════════════════════════════════════════════════════════════════
   CONNECTIONS ROW (html.books-list-v2)

   What was wrong was never the colours — it was that three UNRELATED things
   were interleaved across three rows with no grouping:

     row 1   "6 awaiting payment · $2,082.85"   a fact about the LIST
     row 2   Stripe card          Xero card     facts about INTEGRATIONS
     row 3   "$375.50 clearing"                 a fact about STRIPE, orphaned
                                                two rows from Stripe

   Two changes, per whitespace-balance (group related, separate unrelated):

   1. Row 1 is gone — it repeated the money band verbatim (see
      renderListInsight in invoices-page.js).
   2. Rows 2 and 3 become ONE row of equal cells, each holding a connection
      AND its own figures. The clearing balance now sits with Stripe, because
      it is Stripe's money.

   Result: one row, two peers, each self-contained.
   ══════════════════════════════════════════════════════════════════════════ */
html.books-list-v2 .int-banners {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 10px; align-items: start;
}
/* books.css gives every banner `flex: 1 1 100%` (own row) and flattens the
   connected ones with `display: contents`, which is what let the balance pill
   escape its integration and land on a row of its own. Each banner is a CELL
   again, so its parts stay together. */
html.books-list-v2 .int-banners > div,
html.books-list-v2 .int-banners > div.is-chip {
  display: flex; flex-direction: column; gap: 6px; flex: initial; min-width: 0;
}
html.books-list-v2 .int-banners > div:empty { display: none; }
html.books-list-v2 .sb-chip-wrap { display: block; }

/* Quieter card: green kept only in the left bar, which is enough to read
   "connected" at a glance without outshouting the money above it. */
html.books-list-v2 .sb-card {
  border-color: var(--inv-border);
  border-left-color: var(--doc-accepted);
  background: var(--inv-card);
}
html.books-list-v2 .sb-card .sb-chip { padding: 9px 12px; gap: 10px; }
html.books-list-v2 .sb-card .sb-chip:hover { background: var(--inv-card-hover); }
html.books-list-v2 .sb-card .sb-badge { width: 28px; height: 28px; border-radius: 8px; }
html.books-list-v2 .sb-card-txt b { font-size: 13px; }
/* The note was green — "connected" in words AND colour AND the bar. Once. */
html.books-list-v2 .sb-card-txt small { color: var(--inv-muted); font-size: 12px; }

/* ── The balance belongs INSIDE its connection ───────────────────────────────
   Previously a bordered pill floating below the Stripe card: two objects, so
   the eye had to work out they were related. Now the CELL carries the border
   and the card inside it goes frameless, which makes the connection and its
   figures read as one box with a footer — the money is Stripe's, and it looks
   it. A pill is the wrong container for a figure anyway: it can't align a
   label against a value or give the amount its own weight. */
html.books-list-v2 .int-banners > div:has(.sb-card) {
  gap: 0;
  background: var(--inv-card);
  border: 1px solid var(--inv-border);
  border-left: 3px solid var(--doc-accepted);
  border-radius: var(--r);
  overflow: hidden;
}
/* The card gives up its frame to the cell — one border, not two. */
html.books-list-v2 .int-banners > div:has(.sb-card) .sb-card {
  border: 0; border-radius: 0; background: transparent;
}
/* Footer ledger row: label left, figure right, hairline above. */
html.books-list-v2 .sb-balance-pill {
  display: flex; align-items: center; gap: 16px;
  width: 100%; box-sizing: border-box;
  margin: 0; padding: 8px 12px;
  background: var(--inv-input-bg);
  border: 0; border-top: 1px solid var(--inv-border); border-radius: 0;
  font-size: 12px; font-weight: 500; color: var(--inv-muted);
}
html.books-list-v2 .sb-balance-pill .sbp-item { flex: 1 1 0; min-width: 0; gap: 7px; }
html.books-list-v2 .sb-balance-pill .sbp-item + .sbp-item {
  margin-left: 0; padding-left: 16px; border-left: 1px solid var(--inv-border);
}
html.books-list-v2 .sb-balance-pill i { font-size: 13px; color: #a5a0ff; opacity: 1; flex: none; }
html.books-list-v2 .sb-balance-pill .sbp-lbl {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The figure is the point of the row: pushed right, tabular so a column of
   them lines up, and in full-strength ink against the muted label. */
html.books-list-v2 .sb-balance-pill .sbp-val {
  margin-left: auto; font-size: 13px; font-weight: 700;
  color: var(--inv-text); letter-spacing: -.01em;
}

/* ── The quote nudge ─────────────────────────────────────────────────────────
   "2 quotes awaiting reply · sent >7d" is the one insight the band and the
   rail don't already carry. As a small outlined pill it read as a stray rail
   chip — same shape as the filters directly above it, but not a filter and not
   clickable, which is a false affordance.

   So it becomes what it actually is: a notice attached to the list. Full width,
   sitting on the list's own 24px inset, with the amber carried by a LEFT BAR —
   the same device the connection cards use for status, so the page has one
   vocabulary for "this edge colour tells you the state of the thing". Pill
   shape is dropped precisely because it is not a chip you can press. */
html.books-list-v2 #quoteInsight:not(:empty) { margin: var(--sp-3_5, 14px) 0 0; }
html.books-list-v2 #quoteInsight .insight-strip { display: block; }
html.books-list-v2 #quoteInsight .insight-chip {
  display: flex; align-items: center; gap: 10px;
  width: 100%; box-sizing: border-box;
  min-height: 42px; padding: 0 var(--sp-5, 20px) 0 21px;
  border-radius: var(--r, 11px);
  border: 1px solid var(--inv-border);
  border-left: 3px solid var(--doc-partial);
  background: var(--inv-card);
  font-size: 12.5px; font-weight: 600; color: var(--inv-text-2);
  white-space: normal;
}
html.books-list-v2 #quoteInsight .insight-chip .ti {
  font-size: 16px; color: var(--doc-partial); opacity: 1; flex: none;
}
/* Tone lives in the bar and the icon, not a second fill behind the text. */
html.books-list-v2 #quoteInsight .insight-chip.is-warn,
html.books-list-v2 #quoteInsight .insight-chip.is-good,
html.books-list-v2 #quoteInsight .insight-chip.is-info { background: var(--inv-card); }
html.books-list-v2 #quoteInsight .insight-chip.is-good { border-left-color: var(--doc-accepted); }
html.books-list-v2 #quoteInsight .insight-chip.is-good .ti { color: var(--doc-accepted); }
html.books-list-v2 #quoteInsight .insight-chip.is-info { border-left-color: var(--inv-border-2); }
html.books-list-v2 #quoteInsight .insight-chip.is-info .ti { color: var(--inv-muted); }

/* ── PO “Import from quote” picker (po-page.js) ─────────────────────────────
   Two-step modal: quote list → line select. Rows echo the bk-modal idiom. */
.poqi-list { max-height: 48vh; overflow-y: auto; margin: 0 -6px; padding: 0 6px; }
.poqi-quote { display: flex; align-items: center; gap: 10px; width: 100%; text-align: left;
  background: none; border: none; border-radius: 10px; padding: 9px 10px; cursor: pointer;
  color: var(--inv-text); font: inherit; }
.poqi-quote:hover { background: var(--inv-card-hover); }
.poqi-quote .num { margin-left: auto; color: var(--inv-text-2); font-variant-numeric: tabular-nums; white-space: nowrap; }
.poqi-sub { font-size: 12px; color: var(--inv-muted); }
.poqi-h { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em;
  color: var(--inv-muted); padding: 10px 4px 4px; }
.poqi-row { display: flex; align-items: center; gap: 10px; padding: 7px 6px; border-radius: 8px; cursor: pointer; }
.poqi-row:hover { background: var(--inv-card-hover); }
.poqi-row input { accent-color: var(--inv-accent); width: 16px; height: 16px; flex: none; }
.poqi-row .q { flex: none; width: 58px; font-size: 12px; color: var(--inv-text-2); font-variant-numeric: tabular-nums; }
.poqi-row .d { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; }
.poqi-row .c { flex: none; font-size: 12px; color: var(--inv-text-2); font-variant-numeric: tabular-nums; white-space: nowrap; }
.poqi-row.is-dim { opacity: .55; }
.poqi-tag { flex: none; font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  color: var(--inv-muted); background: var(--inv-input-bg); border: 1px solid var(--inv-border);
  border-radius: 5px; padding: 1px 6px; }

/* The job a PO is for, once one is picked (po-page.js renderJobField). Sized to
   sit in the header grid where the <input> it replaces was, so choosing a job
   doesn't shunt the fields under it. */
.po-joblink { display: flex; align-items: center; gap: 9px; min-height: 38px;
  padding: 7px 11px; border-radius: 9px; font-size: 13px;
  color: var(--inv-text); background: var(--inv-input-bg); border: 1px solid var(--inv-border); }
.po-joblink > i { flex: none; color: var(--inv-accent); }
.po-joblink-m { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.po-joblink-m .t { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
.po-joblink-m .s { font-size: 11.5px; color: var(--inv-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.po-joblink .link { flex: none; font-size: 12px; }

/* ── The rate on the customer header (feature_customer_rate_chip) ───────────
   What you charge this customer, under their name and editable in place. Sits
   between the name and the contact line, so it reads as part of who they are
   rather than as another metric. Absent for a customer on the standard rate —
   a chip repeating the same number on every record would be noise. */
.cx-ratechip:empty { display: none; }
.cx-ratechip { margin: var(--sp-1) 0 0; }
.cx-rate {
  display: inline-flex; align-items: center; gap: var(--sp-1);
  background: none; border: 1px solid transparent; border-radius: var(--r);
  padding: var(--sp-1) var(--sp-2); margin-left: calc(var(--sp-2) * -1);
  color: var(--inv-text); cursor: pointer; font-family: inherit;
}
.cx-rate:hover { background: var(--inv-surface); border-color: var(--inv-border); }
.cx-rate b { font-size: var(--t-md); font-weight: 700; letter-spacing: -0.01em; }
.cx-rate span { font-size: var(--t-sm); color: var(--inv-muted); }
.cx-rate i { font-size: var(--t-sm); color: var(--inv-muted); opacity: 0; transition: opacity var(--dur) var(--ease); }
.cx-rate:hover i, .cx-rate:focus-visible i { opacity: 1; }
.cx-rate.is-unset { color: var(--inv-muted); }
.cx-rate.is-unset span { font-weight: 600; }
/* How this customer pays (feature_pay_behaviour). Reads as a note, not a
   verdict: it sits beside the rate at the same weight as the rest of the
   sub-line, because it is context for a decision rather than a judgement about
   a person. The slow state is carried by the icon and the words as well as the
   colour — "60% on time" says it without any colour at all. */
.cx-pay {
  display: inline-flex; align-items: center; gap: var(--sp-1);
  font-size: var(--t-sm); color: var(--inv-muted); padding: var(--sp-1) var(--sp-2);
}
.cx-pay i { font-size: var(--t-sm); }
.cx-pay.is-slow { color: var(--doc-overdue); font-weight: 600; }
/* Money held for the customer (feature_customer_credit). Accented because it is
   the one fact on this header that is money THEY are owed rather than money you
   charge — and the one an owner needs to remember before quoting the next job. */
.cx-credit {
  display: inline-flex; align-items: center; gap: var(--sp-1);
  font-size: var(--t-sm); font-weight: 600; color: var(--app-accent);
  padding: var(--sp-1) var(--sp-2);
}
.cx-credit i { font-size: var(--t-sm); }
.cx-rate-edit { display: inline-flex; align-items: center; gap: var(--sp-1); flex-wrap: wrap; }
.cx-rate-edit input { width: 7.5em; }
.cx-rate-cur, .cx-rate-unit { font-size: var(--t-sm); color: var(--inv-muted); }

/* ── Schedule of rates (feature_customer_price_list) ────────────────────────
   What this customer pays per catalog item, edited in the same modal shell as
   the statement dialog. A grid rather than a <table>: the price cell is a live
   input and the row wants to stay one line on a phone, where the four columns
   collapse to two. Your price is muted on purpose — it is context for the
   number being typed, not a second number to read. */
.cx-sched-who { font-weight: 500; color: var(--inv-muted); margin-left: var(--sp-1); }
.cx-sched { border: 1px solid var(--inv-border); border-radius: var(--r); overflow: hidden; }
.cx-sched-head, .cx-sched-row {
  display: grid; grid-template-columns: minmax(0, 1fr) 6.5em 11em 2.5em;
  align-items: center; gap: var(--sp-2); padding: var(--sp-2) var(--sp-3);
}
.cx-sched-head { font-size: var(--t-xs); font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: var(--inv-muted); background: var(--inv-surface); }
.cx-sched-row { border-top: 1px solid var(--inv-border); }
.cx-sched-row.is-pending { background: color-mix(in srgb, var(--app-accent) 6%, transparent); }
.cx-sched-item { display: flex; flex-direction: column; min-width: 0; }
.cx-sched-item b { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cx-sched-item small { font-size: var(--t-xs); color: var(--inv-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cx-sched-listed { font-size: var(--t-sm); color: var(--inv-muted); font-variant-numeric: tabular-nums; }
.cx-sched-theirs { display: inline-flex; align-items: center; gap: var(--sp-1); flex-wrap: wrap; }
.cx-sched-theirs input { width: 6.5em; font-variant-numeric: tabular-nums; }
.cx-sched-since { font-size: var(--t-xs); color: var(--inv-muted); flex-basis: 100%; }
.cx-sched-act { text-align: right; }
.cx-sched-x { padding: var(--sp-1); line-height: 1; }
.cx-sched-empty { text-align: center; padding: var(--sp-5) var(--sp-3); color: var(--inv-muted); }
.cx-sched-empty i { font-size: 28px; display: block; margin-bottom: var(--sp-2); opacity: 0.6; }
.cx-sched-empty p { margin: 0; max-width: 34em; margin-inline: auto; }
/* The date bar: "prices as at". Flat while it reads today, accented once it is
   editing a future schedule — that state changes what every save below it
   means, so it has to be visible without being read. */
.cx-sched-when {
  display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
  margin-bottom: var(--sp-3); padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--inv-border); border-radius: var(--r); background: var(--inv-surface);
}
.cx-sched-when label { font-size: var(--t-sm); font-weight: 600; }
.cx-sched-when input { width: 10em; }
.cx-sched-when.is-future { border-color: var(--app-accent); }
.cx-sched-whennote { flex-basis: 100%; font-size: var(--t-sm); color: var(--inv-muted); line-height: 1.45; }
.cx-sched-when.is-future .cx-sched-whennote { color: var(--inv-text); }
.cx-sched-when.is-future .cx-sched-whennote i { color: var(--app-accent); }
/* Labour heads the schedule and is not removable, so its row carries no action. */
.cx-sched-row.is-labour { background: var(--inv-surface); }

/* ── Pasting a builder's schedule: the review table ────────────────────────
   The match quality is the thing being read, so it is words rather than a
   colour alone — "closest guess — check it" says what to do with it. */
.si-list { border: 1px solid var(--inv-border); border-radius: var(--r); max-height: 44vh; overflow-y: auto; }
.si-row {
  display: grid; grid-template-columns: 2em minmax(0, 1fr) minmax(0, 15em);
  align-items: center; gap: var(--sp-2); padding: var(--sp-2) var(--sp-3);
  border-bottom: 1px solid var(--inv-border);
}
.si-row:last-child { border-bottom: 0; }
.si-name { display: flex; flex-direction: column; min-width: 0; }
.si-name b { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.si-name small { font-size: var(--t-xs); color: var(--inv-muted); }
.si-how { margin-left: var(--sp-1); }
.si-how.is-exact { color: var(--inv-ok, var(--app-ok)); }
.si-how.is-close { color: var(--app-warn); }
.si-how.is-none { color: var(--inv-muted); }
.si-pick select { width: 100%; }
.si-tick input { width: auto; margin: 0; }
@media (max-width: 560px) {
  .si-row { grid-template-columns: 2em minmax(0, 1fr); }
  .si-pick { grid-column: 2; }
}
@media (max-width: 560px) {
  .cx-sched-head { display: none; }
  .cx-sched-row { grid-template-columns: minmax(0, 1fr) 2.5em; }
  .cx-sched-listed { grid-column: 1; }
  .cx-sched-listed::before { content: 'Your price '; }
  .cx-sched-theirs { grid-column: 1; }
  .cx-sched-act { grid-column: 2; grid-row: 1; }
}

/* ── Statement of account dialog (customers page) ──────────────────────────
   A preview, not a form: the owner is deciding whether to send this, so the
   dialog shows the same rows, ages and total the PDF will carry. The ageing
   band is the part that earns the dialog — a list of invoices does not tell
   you how old the money is, and that is the whole reason to send a statement
   rather than another reminder. */
.stmt-loading, .stmt-empty { padding: var(--sp-5) var(--sp-3); text-align: center; color: var(--inv-muted); font-size: var(--t-sm); }
.stmt-total { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-3); margin-bottom: var(--sp-3); }
.stmt-total b { font-size: var(--t-2xl); font-weight: 700; color: var(--inv-text); font-variant-numeric: tabular-nums; }
.stmt-total span { font-size: var(--t-sm); color: var(--inv-muted); }
.stmt-age { display: grid; grid-template-columns: repeat(5, 1fr); border: 1px solid var(--inv-border); border-radius: 8px; overflow: hidden; margin-bottom: var(--sp-3); }
.stmt-age div { padding: var(--sp-2) var(--sp-1); text-align: center; border-left: 1px solid var(--inv-border); }
.stmt-age div:first-child { border-left: 0; }
.stmt-age dt { font-size: 10px; letter-spacing: .04em; text-transform: uppercase; color: var(--inv-muted); margin: 0 0 2px; }
.stmt-age dd { margin: 0; font-size: var(--t-sm); font-weight: 700; color: var(--inv-text); font-variant-numeric: tabular-nums; }
.stmt-age dd.zero { color: var(--inv-muted); font-weight: 500; }
.stmt-age dd.late { color: var(--doc-overdue); }
.stmt-rows { list-style: none; margin: 0; padding: 0; max-height: 34vh; overflow: auto; border: 1px solid var(--inv-border); border-radius: 8px; }
.stmt-rows li { display: flex; align-items: baseline; gap: var(--sp-2); padding: var(--sp-2) var(--sp-3); border-top: 1px solid var(--inv-border); font-size: var(--t-sm); }
.stmt-rows li:first-child { border-top: 0; }
.stmt-rows .sr-num { font-weight: 600; color: var(--inv-text); }
.stmt-rows .sr-due { color: var(--inv-muted); flex: 1; }
.stmt-rows .sr-due.late { color: var(--doc-overdue); }
.stmt-rows .sr-bal { font-weight: 600; color: var(--inv-text); font-variant-numeric: tabular-nums; }
/* Where the email is going, said plainly — the owner is about to send this to a
   real person and the address is the one thing they cannot check afterwards. */
.stmt-to { margin: var(--sp-3) 0 0; font-size: var(--t-sm); color: var(--inv-muted); }
.stmt-to b { color: var(--inv-text); font-weight: 600; }
.stmt-to.warn { color: var(--doc-overdue); }
