/* ═══════════════════════════════════════════════════════════════════════════
   books-canon.css — the neuralbooks canon design, gated on :root.books-v4.

   This is a PORT of public/_books-canon-live-rail.html, not an interpretation
   of it. The mockup is the spec: same components, same names, same values. An
   earlier pass tried to layer a light restyle over the v2/v3 structure instead
   — that preserved the old money band and v2 rows and was a reskin of the old
   design, not this one. Every page v4 owns now renders the mockup's own markup
   (.grid/.card/.kpi/.tbl-inv/…) and this file styles it.

   Every rule is scoped to .books-v4, so with the flag off nothing here applies
   and books.css is untouched. Tokens live in tokens.css because stylelint only
   permits colour literals there.

   SPECIFICITY: every selector is :root.books-v4 (0,3,0), not html.books-v4
   (0,2,1). books.css declares its theme values on :root[data-theme=…] .x, which
   is (0,3,0) and outranks the html form no matter what order the files load in
   — that is why the light-theme .btn-ghost kept its old grey. Matching :root
   ties the specificity and lets source order decide, which this file wins.

   .stylelintrc.json exempts this file from the spacing-token and !important
   rules. That is deliberate and narrow: the mockup's exact values ARE the
   approved design, and rounding 6px to 8px to satisfy a lint budget would
   quietly redesign it. The rules still apply to every other stylesheet.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Page-header buttons ───────────────────────────────────────────────────
   The live pages use .btn-ghost in .list-actions; the mockup only ever knew
   .btn. Without this the header buttons fall back to books.css and lose their
   shape — "Export GST summary" rendered as bare text beside a raised "New
   quote". Give .btn-ghost the canon .btn treatment, and make the last one in
   the group the primary, which is what the mockup's header does. */
:root.books-v4 .list-actions { display: flex; align-items: center; gap: var(--sp-2); }
:root.books-v4 .btn-ghost {
  display: inline-flex; align-items: center; gap: 6px;
  height: 34px; padding: 0 var(--sp-3);
  border: 1px solid var(--border); border-radius: var(--r);
  background: var(--card); color: var(--text);
  font-size: var(--t-sm); font-weight: 500; font-family: var(--face);
  white-space: nowrap; text-decoration: none; cursor: pointer;
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
:root.books-v4 .btn-ghost:hover { background: var(--card-2); border-color: var(--border-2); }
:root.books-v4 .btn-ghost i { font-size: var(--t-lg); }
:root.books-v4 .list-actions > :last-child {
  background: var(--accent); border-color: var(--accent);
  color: var(--accent-fg); font-weight: 600;
}
:root.books-v4 .list-actions > :last-child:hover {
  background: var(--accent-h); border-color: var(--accent-h);
}

/* The page title row, matching the mockup's .hd. */
:root.books-v4 .list-head {
  display: flex; align-items: flex-end; gap: var(--sp-3);
  border-bottom: 0; padding-bottom: 0; margin-bottom: var(--sp-5);
}
:root.books-v4 .list-head-lead { margin-right: auto; min-width: 0; }
:root.books-v4 .list-head h2 { font-size: var(--t-2xl); letter-spacing: -.022em; font-weight: 600; }
:root.books-v4 .ov-head-sub { margin-top: 2px; font-size: var(--t-sm); color: var(--muted); }

/* ── The shell ─────────────────────────────────────────────────────────────
   The live pages wrap their content in .wrap, which books.css caps at 920px
   (1200px under v3). The mockup's shell is 1760px — that width is the whole
   reason the 12-column grid can put the chart, donut and aging card in one
   row. Without this the canon layout renders correct but squeezed. */
/* The Books pages do NOT share a container class: .wrap on Overview, Invoices,
   Quotes, Catalog, Expenses and POs; .cx-wrap on Customers; .pf-wrap, .rc-wrap
   and .tx-wrap on Payments, Reconcile and GST; .sp-wrap and .sq-wrap on Stripe
   and Square (each capped at 1120px of their own). Styling only .wrap left four
   pages at their old narrow width, and Stripe/Square were missed entirely —
   they were never linked to this file at all. */
:root.books-v4 .wrap,
:root.books-v4 .cx-wrap,
:root.books-v4 .pf-wrap,
:root.books-v4 .rc-wrap,
:root.books-v4 .tx-wrap,
:root.books-v4 .sp-wrap,
:root.books-v4 .sq-wrap {
  max-width: 1760px;
  margin: 0 auto;
  padding: var(--sp-6) 28px 72px;
}
/* The live-preview split (feature_doc_preview, drafts only) narrows the shell to
   1340px so the preview never squashes the editor. books.css says that as
   `html.books-v3 body.has-doc-preview .wrap`, which is (0,3,2) and BEATS the
   (0,3,0) rule above — so the new-invoice editor was the one canon page still
   rendering at the old width. Adding body to the compound makes this (0,4,1),
   which wins. At 1760 the split gives the editor ~1334px and the preview its
   full 400px, so nothing is squashed — the cap is no longer needed. */
:root.books-v4 body.has-doc-preview .wrap { max-width: 1760px; }
@media (max-width: 900px) {
  :root.books-v4 .wrap,
  :root.books-v4 .cx-wrap,
  :root.books-v4 .pf-wrap,
  :root.books-v4 .rc-wrap,
  :root.books-v4 .tx-wrap,
  :root.books-v4 .sp-wrap,
  :root.books-v4 .sq-wrap { padding: var(--sp-4) var(--sp-3) 72px; }
}

/* ─────────────────────────────────────────────────────────────────────────────
   TOKENS — Airflow, as shipped in public/css/tokens.css. Nothing invented here
   except the chart ramps, which are computed and validated (see the contract).
   ───────────────────────────────────────────────────────────────────────────── */
/* Selectors here match books.css's own :root[data-theme] form. Anything weaker
   (html[data-theme]) silently loses to it despite loading later — that is how
   the --ok override below went missing the first time.
   The mockup no longer owns the palette — it reads the shipped --app-* tokens,
   so whatever tokens.css says is what renders here. Only the chart ramps stay
   literal, because those were computed and validated for these surfaces. */
/* ── Reset + browser surfaces. The parts we did not draw still carry the design. */
:root.books-v4 * { box-sizing: border-box; }
:root.books-v4,
:root.books-v4 body { height: 100%; }
:root.books-v4 body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--face);
  font-size: var(--t-base);
  line-height: 1.45;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
:root.books-v4 ::selection { background: var(--accent-soft); color: var(--text); }
:root.books-v4 :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--r-sm); }
:root.books-v4 * { scrollbar-width: thin; scrollbar-color: var(--border-2) transparent; }
:root.books-v4 *::-webkit-scrollbar { width: 10px; height: 10px; }
:root.books-v4 *::-webkit-scrollbar-track { background: transparent; }
:root.books-v4 *::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: var(--r-pill); border: 3px solid var(--bg); }
:root.books-v4 *::-webkit-scrollbar-thumb:hover { background: var(--muted); }
/* [hidden] must beat the component's own display, or nothing ever hides. */
:root.books-v4 [hidden] { display: none !important; }
/* SCOPED TO .wrap ON PURPOSE. The mockup owned every element on its page, so a
   blanket button/a reset was safe there. Here the top rail is shared chrome
   whose buttons — app switcher, AI credits, notification bell, theme toggle,
   sign out — are injected at runtime and styled by their own single-class
   rules, which a (0,2,1) element reset would outrank and strip bare. The reset
   therefore applies only inside the canon content area. */
   Only font-family is broad enough to force on every control: nothing in this
   file restyles it, so it cannot outrank a component. `font` and `color` are
   NOT — at (0,3,1) they beat .btn (0,3,0), which is what made "Add customer"
   inherit the page ink instead of --accent-fg and drop back to 13px/400. Those
   two go on unclassed controls only, same as the background reset below. */
:root.books-v4 :is(.wrap, .cx-wrap, .pf-wrap, .rc-wrap, .tx-wrap, .sp-wrap, .sq-wrap) :is(button, input, select, textarea) { font-family: inherit; }
:root.books-v4 :is(.wrap, .cx-wrap, .pf-wrap, .rc-wrap, .tx-wrap, .sp-wrap, .sq-wrap) :is(button, input, select, textarea):not([class]) { font: inherit; color: inherit; }
/* :not([class]) matters. Scoping the reset to a container raised it to
   (0,3,1), which OUTRANKS every component rule here — .card is (0,3,0) — so it
   stripped the background and border off any component built on a <button>:
   the KPI tiles, the filter chips, the detail tabs. In the mockup the reset was
   a bare `button` (0,0,1) and lost to .card (0,1,0). Limiting it to buttons
   with no class of their own restores that order: anything carrying a class
   owns its appearance. */
:root.books-v4 :is(.wrap, .cx-wrap, .pf-wrap, .rc-wrap, .tx-wrap, .sp-wrap, .sq-wrap) button { cursor: pointer; }
:root.books-v4 :is(.wrap, .cx-wrap, .pf-wrap, .rc-wrap, .tx-wrap, .sp-wrap, .sq-wrap) button:not([class]) { background: none; border: 0; padding: 0; }
/* The flip side of that: a few canon components ARE deliberately chrome-less
   and were relying on the blanket reset for it. They have to say so themselves
   now, or the UA's raised grey button shows through — which is what happened to
   the customer detail tabs. */
/* Every one of these is a component that happens to be built on a <button>.
   The scoped reset above only strips UA chrome from buttons with NO class, so a
   classed one keeps the grey box, the border and the centred UA font unless it
   says otherwise here. .dl-i and .age-r joined this list when they became
   buttons for the chart hover — the legend rows grew grey boxes the moment
   they stopped being spans. */
:root.books-v4 :is(.tab, .ctab, .crow, .age-r, .dl-i) { background: none; border-style: none; font: inherit; color: inherit; }
:root.books-v4 :is(.wrap, .cx-wrap, .pf-wrap, .rc-wrap, .tx-wrap, .sp-wrap, .sq-wrap) a { color: inherit; text-decoration: none; }
:root.books-v4 :is(.wrap, .cx-wrap, .pf-wrap, .rc-wrap, .tx-wrap, .sp-wrap, .sq-wrap) :is(h1, h2, h3) { margin: 0; font-weight: 600; letter-spacing: -.01em; text-wrap: balance; }
/* Every figure in the app sits in a fixed tabular slot so a column is one
   vertical read and a value change is a digit changing, not a re-layout. */
:root.books-v4 .num { font-family: var(--num); font-variant-numeric: tabular-nums; font-feature-settings: "tnum" 1; letter-spacing: -.01em; }
/* ─────────────────────────────────────────────────────────────────────────────
   SHELL — sidebar + header + scrolling content. The arrangement an owner
   already knows how to read on day one.
   ───────────────────────────────────────────────────────────────────────────── */
:root.books-v4 .app { display: flex; flex-direction: column; height: 100vh; height: 100dvh; }
/* .topbar / .books-back / .books-brand / .books-tabs / .books-tab all come from
   the real books.css. Only the pieces books-bar.js injects at runtime, and the
   More menu the rail has no markup for, are styled here. */
:root.books-v4 .bar-sp { margin-left: auto; }
:root.books-v4 .books-badge { font-family: var(--num); font-variant-numeric: tabular-nums; }
:root.books-v4 .books-badge:not(.hot) { background: var(--card-2); color: var(--text-2); }
:root.books-v4 .books-badge.hot { background: var(--danger); color: var(--danger-fg); }
:root.books-v4 .more-w { position: relative; display: inline-flex; }
:root.books-v4 .more-menu {
  position: absolute; top: calc(100% + 6px); right: 0; z-index: 50;
  min-width: 210px; padding: var(--sp-1);
  background: var(--surface); border: 1px solid var(--border-2);
  border-radius: var(--r-lg); box-shadow: var(--shadow);
}
:root.books-v4 .mi-b {
  display: flex; align-items: center; gap: var(--sp-3); width: 100%;
  padding: var(--sp-2) var(--sp-3); border-radius: var(--r); text-align: left;
  color: var(--text-2); font-size: var(--t-md); background: none; border: 0; cursor: pointer;
}
:root.books-v4 .mi-b i { font-size: var(--t-lg); width: 18px; opacity: .85; }
:root.books-v4 .mi-b:hover { background: var(--card-2); color: var(--text); }
:root.books-v4 .mi-b.on { color: var(--accent); background: var(--accent-soft); }
:root.books-v4 .av {
  width: 30px; height: 30px; border-radius: var(--r); display: grid; place-items: center;
  background: var(--card-2); color: var(--text-2); font-size: var(--t-xs); font-weight: 600;
  border: 1px solid var(--border); flex: none;
}
:root.books-v4 .srch {
  display: flex; align-items: center; gap: var(--sp-2);
  height: 34px; padding: 0 var(--sp-3); width: 230px; flex: none;
  border: 1px solid var(--border); border-radius: 8px;
  background: var(--sunken); color: var(--muted); font-size: var(--t-sm);
}
:root.books-v4 .srch kbd { margin-left: auto; font-family: var(--num); font-size: var(--t-2xs); border: 1px solid var(--border-2); border-radius: var(--r-sm); padding: 1px 5px; }
:root.books-v4 .main { min-width: 0; display: flex; flex-direction: column; min-height: 0; }
:root.books-v4 .hd {
  display: flex; align-items: flex-end; gap: var(--sp-3);
  margin-bottom: var(--sp-5);
}
:root.books-v4 .hd-t { min-width: 0; margin-right: auto; }
:root.books-v4 .hd-t h1 { font-size: var(--t-2xl); letter-spacing: -.022em; }
:root.books-v4 .hd-t p { margin: 2px 0 0; font-size: var(--t-sm); color: var(--muted); }
:root.books-v4 .srch {
  display: flex; align-items: center; gap: var(--sp-2);
  height: 34px; padding: 0 var(--sp-3); width: 250px;
  border: 1px solid var(--border); border-radius: var(--r);
  background: var(--sunken); color: var(--placeholder); font-size: var(--t-sm);
  transition: border-color var(--dur) var(--ease);
}
:root.books-v4 .srch:hover { border-color: var(--border-2); }
:root.books-v4 .srch i { font-size: var(--t-lg); }
:root.books-v4 .srch kbd {
  margin-left: auto; font-family: var(--num); font-size: var(--t-2xs);
  border: 1px solid var(--border-2); border-radius: var(--r-sm);
  padding: 1px 5px; color: var(--muted);
}
:root.books-v4 .btn {
  display: inline-flex; align-items: center; gap: 6px; height: 34px; padding: 0 var(--sp-3);
  border: 1px solid var(--border); border-radius: var(--r);
  background: var(--card); color: var(--text); font-size: var(--t-sm); font-weight: 500;
  white-space: nowrap;
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
:root.books-v4 .btn:hover { background: var(--card-2); border-color: var(--border-2); }
:root.books-v4 .btn i { font-size: var(--t-lg); }
:root.books-v4 .btn.pri { background: var(--accent); border-color: var(--accent); color: var(--accent-fg); font-weight: 600; }
:root.books-v4 .btn.pri:hover { background: var(--accent-h); border-color: var(--accent-h); }
:root.books-v4 .btn:disabled { opacity: .45; cursor: not-allowed; }
:root.books-v4 .btn.gh { background: transparent; border-color: transparent; color: var(--text-2); }
:root.books-v4 .btn.gh:hover { background: var(--card-2); color: var(--text); }
/* The mockup never drew a destructive button — the live pages need one. Same
   .btn shape, danger ink, no fill: it stays the quietest thing in the row until
   it is hovered, which is the right weight for "Delete this customer". */
:root.books-v4 .btn.dgr { color: var(--danger); }
:root.books-v4 .btn.dgr:hover { background: var(--danger-soft); border-color: var(--danger); }
/* books.css .btn-primary (a raised blue pill with a glow) predates the canon and
   still appears in page headers and modals. Give it the .btn.pri treatment so a
   primary action looks the same wherever it is. */
:root.books-v4 .btn-primary,
:root.books-v4 .btn-brand {
  display: inline-flex; align-items: center; gap: 6px; height: 34px; padding: 0 var(--sp-3);
  border: 1px solid var(--accent); border-radius: var(--r);
  background: var(--accent); color: var(--accent-fg);
  font-family: var(--face); font-size: var(--t-sm); font-weight: 600;
  white-space: nowrap; box-shadow: none;
}
:root.books-v4 .btn-primary:hover,
:root.books-v4 .btn-brand:hover { background: var(--accent-h); border-color: var(--accent-h); }
:root.books-v4 .btn-primary i { font-size: var(--t-lg); }
:root.books-v4 .btn-danger-ghost {
  display: inline-flex; align-items: center; gap: 6px; height: 34px; padding: 0 var(--sp-3);
  border: 1px solid var(--border); border-radius: var(--r);
  background: var(--card); color: var(--danger);
  font-family: var(--face); font-size: var(--t-sm); font-weight: 500; white-space: nowrap;
}
:root.books-v4 .btn-danger-ghost:hover { background: var(--danger-soft); border-color: var(--danger); }

/* FIELDS TAKE THE CONTROL RADIUS, like every button they sit beside.
   books.css's v2 layer rounds every books input/select/textarea to var(--r-xs),
   which read as 7px when it was written. This file re-declares --r-xs as 3px —
   it is the canon's hairline radius, for progress tracks and swatches — so the
   whole Books form family silently went square while the .btn and .srch next to
   it stayed at var(--r). Search boxes were the visible casualty: a 3px box in a
   row of 6px buttons. Checkboxes/radios/ranges are excluded; their shape is the
   browser's business, not the control scale's. */
:root.books-v4 :is(input:not([type="checkbox"], [type="radio"], [type="range"]), textarea, select, .inv-search),
/* books.css's v3 list toolbar re-declares the same radius at (0,4,2), which the
   selector above cannot reach — so the list search, the most-looked-at box on
   the page, is named again here. */
:root.books-v4 .dl-toolbar input[type="search"].inv-search {
  border-radius: var(--r);
}
:root.books-v4 .content { overflow-y: auto; padding: var(--sp-6) 28px; min-height: 0; }
:root.books-v4 .scr { display: none; max-width: 1760px; margin: 0 auto; }
:root.books-v4 .scr.on { display: block; }
:root.books-v4 .hd { max-width: 1760px; margin-left: auto; margin-right: auto; }
/* ─────────────────────────────────────────────────────────────────────────────
   CARDS + GRID
   ───────────────────────────────────────────────────────────────────────────── */
:root.books-v4 .grid { display: grid; gap: var(--sp-4); grid-template-columns: repeat(12, 1fr); }
:root.books-v4 .grid + .grid { margin-top: var(--sp-4); }
:root.books-v4 .c3 { grid-column: span 3; }
:root.books-v4 .c4 { grid-column: span 4; }
:root.books-v4 .c6 { grid-column: span 6; }
:root.books-v4 .c8 { grid-column: span 8; }
:root.books-v4 .c12 { grid-column: span 12; }
:root.books-v4 .card {
  background: var(--card); border: 1px solid var(--border); border-radius: var(--r-lg);
  box-shadow: var(--shadow);
  min-width: 0; display: flex; flex-direction: column;
}
:root.books-v4 .card-h {
  display: flex; align-items: baseline; gap: var(--sp-3);
  padding: var(--sp-4) var(--sp-5) 0;
}
:root.books-v4 .card-h h2 { font-size: var(--t-md); font-weight: 600; letter-spacing: -.005em; }
/* On the board a card can be one column wide, and a flex item shrinks before
   the line wraps — so "Money in transit" broke into "Money / in transit" while
   its subtitle sat comfortably beside it. The TITLE is the last thing that
   should yield: it refuses to shrink, so the subtitle wraps to its own line
   first, and the heading stays one phrase at every width. */
:root.books-v4 .card-h { flex-wrap: wrap; }
:root.books-v4 .card-h h2 { flex: 0 0 auto; }
:root.books-v4 .card-h p { margin: 0; font-size: var(--t-sm); color: var(--muted); }
:root.books-v4 .card-h .sp { margin-left: auto; }
:root.books-v4 .card-b { padding: var(--sp-4) var(--sp-5) var(--sp-5); }
/* Cards sharing a row stretch to the tallest. A card that would leave dead
   space pushes its summary line to the bottom edge instead. */
:root.books-v4 .card-b.fill { flex: 1; display: flex; flex-direction: column; }
:root.books-v4 .card-b.fill .age-foot { margin-top: auto; }
/* ── KPI tiles ─────────────────────────────────────────────────────────────── */
:root.books-v4 .kpi { padding: var(--sp-4) var(--sp-5); display: block; text-align: left; width: 100%; }
:root.books-v4 .kpi:hover { border-color: var(--border-2); }
:root.books-v4 .kpi-l {
  display: flex; align-items: center; gap: 6px;
  font-size: var(--t-sm); font-weight: 500; color: var(--text-2);
}
:root.books-v4 .kpi-l i { font-size: var(--t-lg); color: var(--muted); }
:root.books-v4 .kpi-v {
  display: flex; align-items: baseline; gap: var(--sp-2); margin: var(--sp-3) 0 0;
  font-family: var(--num); font-size: var(--t-fig); font-weight: 600;
  letter-spacing: -.03em; font-variant-numeric: tabular-nums;
}
:root.books-v4 .kpi-v small { font-family: var(--face); font-size: var(--t-sm); font-weight: 400; color: var(--muted); letter-spacing: 0; }
:root.books-v4 .kpi-v.bad { color: var(--danger); }
:root.books-v4 .kpi-f { display: flex; align-items: center; gap: var(--sp-2); margin-top: var(--sp-3); min-height: 22px; flex-wrap: nowrap; }
:root.books-v4 .delta {
  display: inline-flex; align-items: center; gap: 3px;
  font-family: var(--num); font-size: var(--t-xs); font-weight: 600;
  padding: 2px 6px; border-radius: var(--r-sm); white-space: nowrap; flex: none;
}
:root.books-v4 .delta i { font-size: 13px; }
:root.books-v4 .delta.up { background: color-mix(in srgb, var(--ok) 14%, transparent); color: var(--ok); }
:root.books-v4 .delta.dn { background: var(--danger-soft); color: var(--danger); }
:root.books-v4 .delta.flat { background: var(--card-2); color: var(--text-2); }
:root.books-v4 .kpi-f > span { font-size: var(--t-xs); color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
:root.books-v4 .spark { margin-left: auto; flex: none; display: block; }
/* ── Chart chrome ──────────────────────────────────────────────────────────── */
:root.books-v4 .chart-box { position: relative; flex: 1; min-height: 250px; }
:root.books-v4 .chart { position: absolute; inset: 0; width: 100%; height: 100%; display: block; overflow: visible; }
:root.books-v4 .chart text { font-family: var(--num); fill: var(--muted); font-size: 10.5px; font-variant-numeric: tabular-nums; }
:root.books-v4 .chart .gl { stroke: var(--grid); stroke-width: 1; shape-rendering: crispEdges; }
:root.books-v4 .chart .ax { stroke: var(--axis); stroke-width: 1; shape-rendering: crispEdges; }
:root.books-v4 .bar { transition: opacity var(--dur) var(--ease); }
:root.books-v4 .chart.dim .bar:not(.hi) { opacity: .35; }
:root.books-v4 .hitbox { fill: transparent; cursor: pointer; }
:root.books-v4 .lgd { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4); margin-top: var(--sp-4); }
:root.books-v4 .lgd-i { display: flex; align-items: center; gap: 7px; font-size: var(--t-sm); color: var(--text-2); }
:root.books-v4 .sw { width: 9px; height: 9px; border-radius: var(--r-2xs); flex: none; }
/* Donut legend doubles as the table view — value + count are always present, so
   identity never rests on colour. */
:root.books-v4 .dl { display: flex; flex-direction: column; gap: 2px; margin-top: var(--sp-3); }
:root.books-v4 .dl-i {
  display: grid; grid-template-columns: 9px 1fr auto auto; align-items: center;
  gap: var(--sp-3); padding: 7px var(--sp-2); border-radius: var(--r);
  width: 100%; text-align: left; font-size: var(--t-sm);
  transition: background var(--dur) var(--ease);
}
:root.books-v4 .dl-i:hover,
:root.books-v4 .dl-i.hi { background: var(--card-2); }
:root.books-v4 .dl-i .sw { width: 9px; height: 9px; }
/* Draft is a neutral, so its swatch carries the same hatch as its slice —
   identity never rests on colour in either the chart or the legend. */
:root.books-v4 .sw.hatch {
  background: repeating-linear-gradient(45deg,
    var(--c-draft) 0 2px, color-mix(in srgb, var(--c-draft) 35%, transparent) 2px 4px);
}
:root.books-v4 .dl-i .nm { color: var(--text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
:root.books-v4 .dl-i .ct { font-family: var(--num); font-size: var(--t-xs); color: var(--muted); font-variant-numeric: tabular-nums; }
:root.books-v4 .dl-i .vl { font-family: var(--num); font-weight: 600; color: var(--text); font-variant-numeric: tabular-nums; }
:root.books-v4 .note {
  margin: var(--sp-3) 0 0; padding-top: var(--sp-3); border-top: 1px solid var(--border);
  font-size: var(--t-xs); color: var(--muted); line-height: 1.5;
}
:root.books-v4 .note b { color: var(--text-2); font-weight: 600; }
:root.books-v4 .donut-wrap { position: relative; display: grid; place-items: center; }
:root.books-v4 .donut-c { position: absolute; text-align: center; pointer-events: none; }
:root.books-v4 .donut-c b { display: block; font-family: var(--num); font-size: var(--t-2xl); font-weight: 600; letter-spacing: -.03em; font-variant-numeric: tabular-nums; }
:root.books-v4 .donut-c span { font-size: var(--t-xs); color: var(--muted); }
:root.books-v4 .seg { transition: opacity var(--dur) var(--ease); cursor: pointer; }
:root.books-v4 .donut.dim .seg:not(.hi) { opacity: .3; }
/* ── Aging rows ────────────────────────────────────────────────────────────── */
:root.books-v4 .age { display: flex; flex-direction: column; gap: var(--sp-3); }
:root.books-v4 .card-b.fill .age { flex: 1; justify-content: space-evenly; }
:root.books-v4 .age-r { display: grid; grid-template-columns: 88px 1fr 92px; align-items: center; gap: var(--sp-3); width: 100%; text-align: left; }
:root.books-v4 .age-l { font-size: var(--t-sm); color: var(--text-2); }
/* display:block matters — width and height do not apply to inline spans. */
:root.books-v4 .age-t { display: block; height: 10px; border-radius: var(--r-xs); background: var(--sunken); overflow: hidden; }
:root.books-v4 .age-f { display: block; height: 100%; border-radius: var(--r-xs); transition: filter var(--dur) var(--ease); }
:root.books-v4 .age-r:hover .age-f { filter: brightness(1.12); }
:root.books-v4 .age-v { font-family: var(--num); font-size: var(--t-sm); font-weight: 600; text-align: right; font-variant-numeric: tabular-nums; }
:root.books-v4 .age-foot {
  display: flex; justify-content: space-between; align-items: baseline;
  margin-top: var(--sp-4); padding-top: var(--sp-3); border-top: 1px solid var(--border);
  font-size: var(--t-sm); color: var(--muted);
}
:root.books-v4 .age-foot b { font-family: var(--num); font-size: var(--t-md); color: var(--text); font-variant-numeric: tabular-nums; }
/* ── Tables ────────────────────────────────────────────────────────────────── */
:root.books-v4 .tbl-wrap { overflow-x: auto; }
:root.books-v4 table { width: 100%; border-collapse: collapse; }
:root.books-v4 th {
  text-align: left; font-size: var(--t-2xs); font-weight: 600; letter-spacing: .08em;
  text-transform: uppercase; color: var(--muted); white-space: nowrap;
  padding: var(--sp-2) var(--sp-3); border-bottom: 1px solid var(--border);
  position: sticky; top: 0; background: var(--card); z-index: 1;
}
:root.books-v4 th.r,
:root.books-v4 td.r { text-align: right; }
:root.books-v4 th.srt { cursor: pointer; user-select: none; }
:root.books-v4 th.srt:hover { color: var(--text-2); }
:root.books-v4 th.srt i { font-size: var(--t-sm); vertical-align: -1px; opacity: 0; }
:root.books-v4 th.srt.on { color: var(--text); }
:root.books-v4 th.srt.on i { opacity: 1; }
:root.books-v4 td {
  padding: 10px var(--sp-3); border-bottom: 1px solid var(--border);
  font-size: var(--t-md); vertical-align: middle;
}
:root.books-v4 tbody tr { transition: background var(--dur) var(--ease); }
:root.books-v4 tbody tr:hover { background: var(--card-2); }
:root.books-v4 tbody tr:last-child td { border-bottom: 0; }
:root.books-v4 td.num,
:root.books-v4 th.num { font-family: var(--num); font-variant-numeric: tabular-nums; }
:root.books-v4 .cust { font-weight: 500; }
:root.books-v4 .sub { display: block; font-size: var(--t-sm); color: var(--muted); font-weight: 400; }
:root.books-v4 .docn { font-family: var(--num); font-size: var(--t-sm); color: var(--text-2); white-space: nowrap; }
:root.books-v4 .amt { font-weight: 600; white-space: nowrap; }
:root.books-v4 .amt small { display: block; font-weight: 400; font-size: var(--t-xs); color: var(--muted); }
/* Status is a recorded fact: a dot AND a word, never colour on its own. */
:root.books-v4 .pill {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 9px 3px 7px; border-radius: var(--r-pill);
  font-size: var(--t-xs); font-weight: 500; white-space: nowrap;
  border: 1px solid var(--border); background: var(--sunken); color: var(--text-2);
}
:root.books-v4 .pill::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: none; }
:root.books-v4 .pill.paid { color: var(--ok);     border-color: color-mix(in srgb, var(--ok) 32%, transparent);     background: color-mix(in srgb, var(--ok) 10%, transparent); }
:root.books-v4 .pill.over { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 36%, transparent); background: var(--danger-soft); }
:root.books-v4 .pill.await { color: var(--accent); border-color: var(--accent-edge);   background: var(--accent-soft); }
:root.books-v4[data-theme="light"] .pill.await { color: var(--accent-h); }
:root.books-v4 .pill.draft { color: var(--muted); }
:root.books-v4 .pill.part { color: var(--warn);   border-color: color-mix(in srgb, var(--warn) 34%, transparent);   background: color-mix(in srgb, var(--warn) 10%, transparent); }
:root.books-v4 .ck { width: 15px; height: 15px; accent-color: var(--accent); cursor: pointer; vertical-align: -2px; }
/* The live lists emit ndBadge markup (.ui-badge[data-status]), not the mockup's
   hand-written .pill classes. Rather than rewrite a component three pages share,
   the status keys map onto the pill palette here. Same rule, same dot-and-word. */
:root.books-v4 .ui-badge {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 9px 3px 7px; border-radius: var(--r-pill);
  font-size: var(--t-xs); font-weight: 500; white-space: nowrap; line-height: 1.5;
  border: 1px solid var(--border); background: var(--sunken); color: var(--text-2);
}
:root.books-v4 .ui-badge::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: none; }
:root.books-v4 .ui-badge__dot { display: none; }
:root.books-v4 .ui-badge[data-status="paid"],
:root.books-v4 .ui-badge[data-status="accepted"] { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 32%, transparent); background: color-mix(in srgb, var(--ok) 10%, transparent); }
:root.books-v4 .ui-badge[data-status="overdue"],
:root.books-v4 .ui-badge[data-status="declined"],
:root.books-v4 .ui-badge[data-status="bounced"] { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 36%, transparent); background: var(--danger-soft); }
:root.books-v4 .ui-badge[data-status="open"],
:root.books-v4 .ui-badge[data-status="sent"],
:root.books-v4 .ui-badge[data-status="viewed"] { color: var(--accent); border-color: var(--accent-edge); background: var(--accent-soft); }
:root.books-v4[data-theme="light"] .ui-badge[data-status="open"],
:root.books-v4[data-theme="light"] .ui-badge[data-status="sent"],
:root.books-v4[data-theme="light"] .ui-badge[data-status="viewed"] { color: var(--accent-h); }
:root.books-v4 .ui-badge[data-status="partially_paid"],
:root.books-v4 .ui-badge[data-status="expired"],
:root.books-v4 .ui-badge[data-status="partially_received"],
:root.books-v4 .ui-badge[data-status="submitted"] { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 34%, transparent); background: color-mix(in srgb, var(--warn) 10%, transparent); }
/* Purchase-order and expense lifecycles land on the same three meanings:
   done (ok), in flight (accent), gone wrong (danger). Anything unlisted keeps
   the neutral pill, which is the right answer for draft and closed. */
:root.books-v4 .ui-badge[data-status="received"],
:root.books-v4 .ui-badge[data-status="reimbursed"],
:root.books-v4 .ui-badge[data-status="approved"] { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 32%, transparent); background: color-mix(in srgb, var(--ok) 10%, transparent); }
:root.books-v4 .ui-badge[data-status="issued"] { color: var(--accent); border-color: var(--accent-edge); background: var(--accent-soft); }
:root.books-v4[data-theme="light"] .ui-badge[data-status="issued"] { color: var(--accent-h); }
:root.books-v4 .ui-badge[data-status="cancelled"] { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 36%, transparent); background: var(--danger-soft); }
/* An icon badge ("viewed", "Not sent", delivery) already carries its own mark —
   a second dot in front of it is noise. */
:root.books-v4 .ui-badge:has(> i)::before { display: none; }
/* Footnotes to the status, not rivals to it: same words, same tooltips, but
   stripped of the chip so the column reads as one pill per row. */
:root.books-v4 .t-notes { display: flex; flex-wrap: wrap; gap: var(--sp-1) var(--sp-3); margin-top: 5px; }
:root.books-v4 .t-notes .ui-badge {
  padding: 0; border: 0; border-radius: 0; background: none;
  color: var(--muted); font-size: var(--t-2xs); font-weight: 400; height: auto;
}
:root.books-v4 .t-notes .ui-badge::before { display: none; }
:root.books-v4 .t-notes .ui-badge i { font-size: var(--t-sm); }
/* ── Payments / Reconcile / GST & tax / Stripe / Square ────────────────────
   These pages ship their own page-scoped design system in a <style> block —
   .pf-*, .rc-*, .tx-*, .sp-* (Stripe), .sq-* (Square) — each with its own KPI
   band, cards, buttons and table.
   Structurally that is already what the mockup asks for, so the canon does not
   rewrite three bespoke renderers: it re-skins the primitives onto the canon
   tokens and type scale, so these pages sit at the same weight as the four
   that were ported markup-first.

   Every selector here is a single class inside the page's own <style>, which is
   (0,1,0); these are (0,3,0) and win on specificity regardless of source order. */
:root.books-v4 :is(.pf-card, .rc-card, .tx-card, .tx-hero, .sp-card, .sq-card) {
  background: var(--card); border: 1px solid var(--border);
  border-radius: var(--r-lg); box-shadow: var(--shadow); margin-bottom: var(--sp-4);
}
:root.books-v4 :is(.pf-cardhead, .rc-cardhead, .tx-cardhead, .sp-cardhead, .sq-cardhead) {
  padding: var(--sp-4) var(--sp-5); border-bottom: 1px solid var(--border);
}
:root.books-v4 :is(.pf-cardhead, .rc-cardhead, .tx-cardhead, .sp-cardhead, .sq-cardhead) h2 {
  font-size: var(--t-lg); font-weight: 600; letter-spacing: -.01em;
}
/* KPI bands → the canon tile. Sentence-case label, one big tabular figure, a
   quiet sub — the same three-line rhythm every other page's band has. */
:root.books-v4 :is(.pf-band, .rc-band, .sp-band, .sq-band) {
  display: grid; grid-template-columns: repeat(4, 1fr);
  gap: var(--sp-4); margin-bottom: var(--sp-4);
}
:root.books-v4 :is(.pf-kpi, .rc-kpi, .sp-kpi, .sq-kpi) {
  background: var(--card); border: 1px solid var(--border);
  border-radius: var(--r-lg); box-shadow: var(--shadow); padding: var(--sp-4) var(--sp-5);
}
:root.books-v4 :is(.pf-kpi, .rc-kpi, .sp-kpi, .sq-kpi) .lab {
  font-size: var(--t-2xs); font-weight: 600; letter-spacing: .09em;
  text-transform: uppercase; color: var(--muted); margin-bottom: var(--sp-2);
}
:root.books-v4 :is(.pf-kpi, .rc-kpi, .sp-kpi, .sq-kpi) .val {
  font-family: var(--num); font-size: var(--t-fig); font-weight: 600;
  letter-spacing: -.02em; font-variant-numeric: tabular-nums; color: var(--text);
}
:root.books-v4 :is(.pf-kpi, .rc-kpi, .sp-kpi, .sq-kpi) .val:is(.pf-ok, .sp-ok, .sq-ok, .ok) { color: var(--ok); }
:root.books-v4 :is(.pf-kpi, .rc-kpi, .sp-kpi, .sq-kpi) .val:is(.pf-warn, .sp-warn, .sq-warn, .warn) { color: var(--warn); }
:root.books-v4 :is(.pf-kpi, .rc-kpi, .sp-kpi, .sq-kpi) .val:is(.pf-bad, .sp-bad, .sq-bad, .bad) { color: var(--danger); }
:root.books-v4 :is(.pf-kpi, .rc-kpi, .sp-kpi, .sq-kpi) .sub {
  font-size: var(--t-xs); color: var(--muted); margin-top: var(--sp-1);
}
/* Page buttons → the canon .btn. */
:root.books-v4 :is(.pf-btn, .rc-btn, .tx-btn, .sp-btn, .sq-btn) {
  display: inline-flex; align-items: center; gap: 6px; height: 34px; padding: 0 var(--sp-3);
  border: 1px solid var(--border); border-radius: var(--r);
  background: var(--card); color: var(--text);
  font-family: var(--face); font-size: var(--t-sm); font-weight: 500; white-space: nowrap;
}
:root.books-v4 :is(.pf-btn, .rc-btn, .tx-btn, .sp-btn, .sq-btn):hover { background: var(--card-2); border-color: var(--border-2); }
:root.books-v4 :is(.pf-btn, .rc-btn, .tx-btn, .sp-btn, .sq-btn).ghost { background: transparent; border-color: transparent; color: var(--text-2); }
/* Chips and pills → the canon status pill: dot and word, never colour alone. */
:root.books-v4 :is(.pf-chip, .rc-tag, .pf-pill, .rc-pill, .sp-pill, .sq-pill) {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 9px; border-radius: var(--r-pill);
  border: 1px solid var(--border); background: var(--sunken);
  font-size: var(--t-xs); font-weight: 500; color: var(--text-2); white-space: nowrap;
}
:root.books-v4 :is(.pf-chip, .rc-tag, .sp-chip, .sq-chip).is-on,
:root.books-v4 :is(.pf-chip, .rc-tag, .sp-chip, .sq-chip).sel {
  border-color: var(--accent-edge); background: var(--accent-soft); color: var(--accent);
}
/* Page titles at the canon step, so a back-office page does not shout louder
   than the Overview it hangs off. */
:root.books-v4 :is(.pf-head, .rc-head, .tx-head, .sp-head, .sq-head) h1 {
  font-size: var(--t-2xl); font-weight: 600; letter-spacing: -.022em;
}
:root.books-v4 :is(.pf-head, .rc-head, .tx-head, .sp-head, .sq-head) p { font-size: var(--t-md); color: var(--text-2); }
/* The tax hero keeps its scale — it IS the page's one figure — but takes the
   canon's figure face and tracking. */
:root.books-v4 .tx-hero .big {
  font-family: var(--num); font-weight: 700; letter-spacing: -.03em;
  font-variant-numeric: tabular-nums;
}
:root.books-v4 .tx-hero .lab {
  font-size: var(--t-2xs); font-weight: 600; letter-spacing: .09em; color: var(--muted);
}
/* Expenses renders its table INTO the card-grid host. Left as a grid, the
   whole table becomes one grid item and is squeezed to a single card column —
   the last three money columns fell off the side. */
:root.books-v4 #expGrid,
:root.books-v4 #poList { display: block; }
/* The list-insight strip ("3 quotes awaiting reply") sits between the tabs and
   the table. books.css gives it a top margin only, which left it welded to the
   card below — every other block in the canon carries the same --sp-4 gap, so
   it does too. The ID makes these (1,3,0), which clears the (1,2,0) rule. */
:root.books-v4 #quoteInsight:not(:empty),
:root.books-v4 #invInsight:not(:empty) { margin: var(--sp-4) 0; }
/* ── The canon list row ────────────────────────────────────────────────────
   A real <tr>, so the columns size to their content the way the mockup's
   table does rather than to widths guessed in a grid template. */
:root.books-v4 .row--v4 { cursor: pointer; }
:root.books-v4 .row--v4 .t-ck { width: 34px; cursor: default; }
:root.books-v4 .row--v4 .t-job { color: var(--text-2); max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
:root.books-v4 .row--v4 .t-st { white-space: nowrap; }
:root.books-v4 .row--v4 .t-due { font-size: var(--t-sm); color: var(--text-2); }
/* Lateness is the one thing in this table you act on, so it is the one thing
   that carries colour outside its pill. */
:root.books-v4 .row--v4 .t-due.is-late { color: var(--danger); }
:root.books-v4 .row--v4 .t-due .sub { font-size: var(--t-xs); }
:root.books-v4 .row--v4 .t-act { width: 1%; white-space: nowrap; }
:root.books-v4 .row--v4 .row-paybar,
:root.books-v4 .row--v4 .row-insight { display: none; }
/* ── Filter tabs + toolbar ─────────────────────────────────────────────────── */
:root.books-v4 .tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--border); margin-bottom: var(--sp-4); overflow-x: auto; }
:root.books-v4 .tab {
  padding: var(--sp-2) var(--sp-3) 10px; font-size: var(--t-md); color: var(--muted);
  border-bottom: 2px solid transparent; margin-bottom: -1px; white-space: nowrap;
  transition: color var(--dur) var(--ease);
}
:root.books-v4 .tab:hover { color: var(--text-2); }
:root.books-v4 .tab.on { color: var(--text); border-bottom-color: var(--accent); font-weight: 500; }
:root.books-v4 .tab b { font-family: var(--num); font-weight: 600; font-size: var(--t-sm); color: var(--muted); margin-left: 5px; }
:root.books-v4 .tab.on b { color: var(--accent); }
:root.books-v4 .tools { display: flex; align-items: center; gap: var(--sp-2); margin-bottom: var(--sp-4); flex-wrap: wrap; }
:root.books-v4 .tools .sp { margin-left: auto; }
/* ── Empty + loading states ────────────────────────────────────────────────── */
:root.books-v4 .empty { padding: var(--sp-10) var(--sp-5); text-align: center; }
:root.books-v4 .empty i { font-size: var(--icon-lg); color: var(--border-2); }
:root.books-v4 .empty h3 { margin: var(--sp-3) 0 var(--sp-1); font-size: var(--t-lg); }
:root.books-v4 .empty p { margin: 0 auto var(--sp-4); max-width: 42ch; color: var(--muted); font-size: var(--t-md); }
:root.books-v4 .empty-a { display: flex; gap: var(--sp-2); justify-content: center; flex-wrap: wrap; }
/* ── Table footer: range, paging, and the bulk bar a selection earns ───────── */
:root.books-v4 .tfoot {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: 10px var(--sp-3) 10px var(--sp-4); border-top: 1px solid var(--border);
  font-size: var(--t-sm); color: var(--muted);
}
:root.books-v4 .tfoot .rng b { color: var(--text-2); font-weight: 600; }
:root.books-v4 .pg { margin-left: auto; display: flex; align-items: center; gap: var(--sp-2); }
:root.books-v4 .pg .btn { height: 26px; padding: 0 6px; }
:root.books-v4 .pg span { color: var(--text-2); }
:root.books-v4 .bulk {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: 10px var(--sp-4); border-top: 1px solid var(--accent-edge);
  background: var(--accent-soft); font-size: var(--t-sm); color: var(--text-2);
  border-radius: 0 0 var(--r-lg) var(--r-lg);
}
:root.books-v4 .bulk b { color: var(--text); }
:root.books-v4 .bulk .sp { margin-left: auto; }
/* The `@keyframes pulse` that belongs here was destroyed by the wholesale port:
   the scoping pass prefixed the keyframe SELECTORS (`:root.books-v4 0%`), which
   is not valid inside @keyframes — so .sk referenced an animation that did not
   exist and never pulsed. Keyframe stops are global by name; they take no
   scope, which is why the port had nothing to prefix. */
:root.books-v4 .sk { background: var(--card-2); border-radius: var(--r-sm); animation: pulse 1.4s var(--ease) infinite; }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: .45; } }
/* ── Phone: list screens ───────────────────────────────────────────────────
   Nothing here scrolls sideways. Four stacked stat cards buried the list a
   screen and a half down; a swipe rail fixed that but hid three of the four
   behind a gesture, and the filter strip hid its own tail the same way. So
   the band becomes a 2x2 of compact tiles and the filters become wrapping
   chips: everything visible, one axis of scrolling on the whole page. */
@media (max-width: 640px) {
  /* AND IT HAS TO NAME THE MARKUP THAT SHIPS. `.scr` is the mockup's screen
     wrapper; the live list pages render the band as `.inv-summary > .grid`, so
     for every build since this block was written it matched nothing and the
     four cards went on stacking exactly as the comment above says they must
     not. Measured on invoices at 390px: the first invoice sat 978px down — a
     screen and a half, which is the very number that comment was written
     about. Both selectors are listed so the mockup keeps rendering too. */
  /* NAMED BY WHAT IT HOLDS, NOT BY WHERE IT SITS. This began as
     `.scr > .grid:first-child` — the mockup's wrapper, which ships nowhere —
     then grew `.inv-summary > .grid`, then `.cx-kpis > .grid`, and the catalog
     still stacked four tiles 412px deep because its band is a bare `.grid`
     inside an unclassed div. A stat band IS a grid with .kpi children; saying
     that is what stops this recurring a sixth time. */
  :root.books-v4 .scr > .grid:first-child,
  :root.books-v4 .grid:has(> .kpi) {
    display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-2);
    margin-inline: 0; padding-inline: 0; overflow: visible;
  }
  :root.books-v4 .scr > .grid:first-child > .kpi,
  :root.books-v4 .grid > .kpi { grid-column: auto; padding: var(--sp-3); min-width: 0; }
  /* TWO TILES, NOT FOUR. The band answers one question on a phone — how much am
     I owed, and how much of it is late. The third tile (awaiting payment) is
     the difference between the first two, and the fourth is a desk metric that
     reads "—" on any account without enough paid history. Both are filters, and
     the status chips directly below already filter the same way, so nothing
     actionable goes with them. nth-child rather than a name, because the tiles
     are already ordered by importance and a new one should inherit the rule. */
  :root.books-v4 .inv-summary > .grid > .kpi:nth-child(n+3) { display: none; }

  /* THE COMMAND ROW WAS FOUR FULL-WIDTH ROWS — 200px of buttons before a single
     invoice. books.css lays it out as a two-column grid below 720px, but the
     canon re-declares .list-actions as flex OUTSIDE any media query and at
     heavier specificity, so on a phone that grid never applied and every
     control took a line of its own. Same shape of miss as the band above.
     Search leads on its own row — it is what you came to the list to use — and
     the three controls share the one below it. */
  /* The canon aligns the head's two halves to flex-end so the actions sit on
     the title's baseline in a desktop ROW. books.css turns that row into a
     column below 720px — at which point flex-end stops meaning "bottom" and
     starts meaning "right", and the page title was hard against the right
     edge. Fourth rule in this file to reach a phone it was not written for. */
  /* ── Customers, same three problems ──────────────────────────────────────
     Measured at 390px: the first customer sat 796px down, which on an 809px
     screen means you arrive at a page of statistics and have to scroll to find
     out it has a list at all. Its band is a fourth grid this file had never
     been pointed at, and its head stacked title, search and + Add on three
     rows of their own. */
  /* TITLE AND ACTION SHARE THE TOP LINE; SEARCH TAKES THE WHOLE ONE BELOW.
     Sharing a row with the button left the field 231px against a 215px
     placeholder that needs 24px of padding around it — "Search name, email or
     phor". Shrinking the field's text is not available: 16px is the floor that
     stops iOS zooming the page on focus. So the button moves up beside the
     title, which has the room, and search gets the full width it wants. */
  :root.books-v4 .cx-head { flex-wrap: wrap; gap: var(--sp-2); align-items: center; }
  :root.books-v4 .cx-title { flex: 1 1 0; min-width: 0; order: 0; }
  :root.books-v4 .cx-head .btn-primary {
    flex: 0 0 auto; order: 1; margin: 0; padding-inline: var(--sp-3); font-size: var(--t-sm);
  }
  :root.books-v4 .cx-head .inv-search { flex: 1 1 100%; order: 2; min-width: 0; width: auto; margin: 0; }

  /* (.cx-rows is un-capped further down — it has to come after the 1180px
     block that re-caps it. See "A SCROLLBOX INSIDE A SCROLLING PAGE".) */

  /* ── 16px, OR iOS ZOOMS THE PAGE ────────────────────────────────────────
     books.css states this floor and enforces it inside the invoice editor's
     item table, where it was noticed. Every OTHER field on these pages was
     left below it: the list search at 14px, the customers sort select at 11px
     and the rail's own search well at 12.5px — which is the most-tapped
     control in the app. Focus any of them on an iPhone and Safari zooms the
     viewport, so you finish typing a customer's name on a page you now have to
     pinch back out of.
     Specificity, not order: rail-canon.css is injected at runtime and lands
     after this file, so :root.books-v4 is what carries the rail's well. */
  :root.books-v4 .inv-search,
  :root.books-v4 .list-actions input,
  :root.books-v4 .list-actions select,
  :root.books-v4 .nr-find input,
  :root.books-v4 :is(.pf-search, .pf-sel, .rc-sel, .sp-sel, .sq-sel, .dw-sel),
  :root.books-v4 .cat-facet,
  :root.books-v4 .cat-bulk-markup input { font-size: 16px; }
  /* The sort select carries no class, so the container reset at the top of this
     file (:is(...) :is(button,input,select,textarea):not([class]) { font:
     inherit }) claims it at (0,4,1) and hands it the 11px of the uppercase
     label it sits in. Matching :not([class]) is what gets back above it. */
  :root.books-v4 .cx-sortlbl select:not([class]) { font-size: 16px; }

  /* AND THE FULL BLEED WAS 4px WIDE OF THE VIEWPORT. books-v3 makes this a
     sticky banner with `margin: 0 -16px; padding: 14px 16px`, which assumes its
     PARENT carries the 16px gutter. On these pages the gutter is on .wrap, two
     levels up, so the -16px overshot and every list page scrolled sideways by
     4px. The 50%/50vw form measures against the viewport instead, so it is
     exact whatever the parent's padding turns out to be. */
  /* ── Catalog: searching is its own screen ────────────────────────────────
     The field is at the top and the results are under a stat band, category
     chips, supplier facets and a count — 999px of page before the first item.
     Raise the keyboard and the visible window is about 420px, so the number of
     results on screen WHILE TYPING measured zero. You were searching a list you
     could not see.

     A query collapses the page to the two things that matter. Everything hidden
     here is a way of BROWSING the catalog, and a search is the statement that
     you are done browsing. .list-head is already sticky (books-v3), so the
     field stays put and the results run under it.

     The intro paragraph goes on a phone either way: it is read once, and it is
     57px in front of the thing it describes, every visit. */
  /* ── Integration banners get one line, not four ──────────────────────────
     "Accept card payments on your invoices — connect Stripe (no monthly fee)"
     wraps to four lines at 390px and the banner stands 129px tall, directly
     between the filter chips and the first invoice. It is a real prompt and it
     stays — this is not the screen to hide the thing that gets you paid — but
     it says its piece in two lines and gives the rest back. The full sentence
     is still there for anyone who taps through. */
  :root.books-v4 .sb .sb-lead { align-items: center; gap: var(--sp-2_5); }
  :root.books-v4 .sb .sb-lead > span:last-child,
  :root.books-v4 .sb .sb-lead > div:last-child {
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
    overflow: hidden; font-size: var(--t-sm); line-height: 1.35;
  }
  :root.books-v4 .sb { padding: var(--sp-2_5) var(--sp-3); gap: var(--sp-2_5); }
  :root.books-v4 .sb .sb-badge { padding: var(--sp-1) var(--sp-1_5); }

  :root.books-v4 .cat-intro { display: none; }
  :root.books-v4 .wrap.is-searching :is(.grid:has(> .kpi), .cat-cats, .cat-facets, .cat-count) {
    display: none;
  }
  /* The command row goes too, except the field itself. Sort, the view toggle
     and five import buttons are all ways of getting items INTO the catalog or
     ordering the whole of it — none of them is what you are doing with a query
     half-typed and a keyboard covering the screen. Clearing the field brings
     them all back. */
  :root.books-v4 .wrap.is-searching .list-head h2,
  :root.books-v4 .wrap.is-searching .list-actions > *:not(#catSearch) { display: none; }

  /* ── The catalog row was 1010px of grid in a 364px row ────────────────────
     books.css folds this table to four columns below 700px, but the canon
     re-declares the same selector at :root.books-v4 with the full nine-track
     desktop grid — 34 38 180 104 104 88 80 96 286 — and wins. The tracks the
     phone hides still get allocated, so the sell price was laid out at x=313
     in a row that ends at 377 and the margin cell at 429 was off the screen
     entirely. The price is the thing a person opens a price catalog to read.
     Sixth rule in this file to override a phone rule it was not written for. */
  /* (the four-track grid itself is set further down — it has to come after the
     canon's own nine-track declaration, which ties on specificity at (0,3,0)) */
  :root.books-v4 :is(.catv2-head, .catv2-row) >
    :is(.catv2-c-part, .catv2-c-sup, .catv2-c-rrp, .catv2-c-cost) { display: none; }
  /* Margin is a desk metric, and it is the cell that pushes the price off. */
  :root.books-v4 :is(.catv2-head, .catv2-row) > :last-child { display: none; }

  :root.books-v4 .list-head {
    align-items: stretch;
    margin-inline: calc(50% - 50vw); padding-inline: var(--sp-4);
  }
  :root.books-v4 .list-actions { flex-wrap: wrap; gap: var(--sp-2); }
  :root.books-v4 .list-actions > * { flex: 1 1 0; min-width: 0; }
  :root.books-v4 .list-actions .inv-search { flex: 1 1 100%; order: 1; }
  :root.books-v4 .list-actions .lt-dd { order: 2; }
  :root.books-v4 .list-actions .btn-primary { order: 3; }
  :root.books-v4 .list-actions .lt-dd__trigger,
  :root.books-v4 .list-actions .btn-primary {
    width: 100%; justify-content: center; padding-inline: var(--sp-2); font-size: var(--t-sm);
  }
  :root.books-v4 .kpi-l { font-size: var(--t-xs); gap: 5px; }
  :root.books-v4 .kpi-l i { font-size: var(--t-md); }
  :root.books-v4 .kpi-v { margin-top: 6px; font-size: var(--t-2xl); }
  :root.books-v4 .kpi-v small { display: none; }
  :root.books-v4 .kpi-f { margin-top: 6px; min-height: 0; }
  :root.books-v4 .kpi-f > span:not(.delta) { display: none; }
   /* the figure carries it */
  
  :root.books-v4 .delta { font-size: var(--t-2xs); }
  /* The customer detail's own tabs wrap for the same reason. */
  
  :root.books-v4 .ctabs { flex-wrap: wrap; overflow: visible; padding: 0 var(--sp-4); gap: var(--sp-1); }
  :root.books-v4 .ctab { padding: 10px var(--sp-2) var(--sp-2); font-size: var(--t-sm); }
  /* Filters wrap as chips instead of hiding their tail behind a scrub. */
  
  :root.books-v4 .tabs {
    flex-wrap: wrap; overflow: visible; gap: 6px;
    border-bottom: 0; margin: 0 0 var(--sp-3);
    -webkit-mask-image: none; mask-image: none;
  }
  :root.books-v4 .tabs::after { display: none; }
  :root.books-v4 .tab {
    padding: 6px 11px; font-size: var(--t-sm);
    border: 1px solid var(--border); border-radius: var(--r-pill);
    margin-bottom: 0; white-space: nowrap;
  }
  :root.books-v4 .tab.on {
    background: var(--accent-soft); border-color: var(--accent-edge);
    color: var(--accent); border-bottom-color: var(--accent-edge);
  }
  :root.books-v4 .tab b { color: inherit; margin-left: var(--sp-1); }
  /* One tidy block instead of a ragged wrap. */
  
  :root.books-v4 .tools,
:root.books-v4 .cat-tools { gap: var(--sp-2); }
  :root.books-v4 .tools .sp,
:root.books-v4 .cat-tools .sp { display: none; }
  :root.books-v4 .tools .srch,
:root.books-v4 .cat-tools .srch { flex: 1 0 100%; width: auto; }
  :root.books-v4 .tools .btn,
:root.books-v4 .cat-tools .btn { flex: 1 1 calc(50% - 4px); justify-content: center; }
  :root.books-v4 .tools .btn.pri,
:root.books-v4 .cat-tools .btn.pri { flex: 1 0 100%; }
  /* No money table scrolls sideways on a phone. */
  
  :root.books-v4 #scr-quotes table,
:root.books-v4 #scr-customers table { min-width: 0; }
  :root.books-v4 .cat-cost { display: none; }
  :root.books-v4 .flag-t { display: none; }
          /* the icon still says it */
  
  :root.books-v4 .pay-hide { display: none; }
        /* net is what actually landed */
  
  :root.books-v4 .po-hide { display: none; }
         /* status already implies the date */
  
  :root.books-v4 .item b { white-space: normal; }
  :root.books-v4 .mcell { gap: var(--sp-2); }
  :root.books-v4 .mpc { min-width: 0; }
  :root.books-v4 .item .thumb { width: 28px; height: 28px; font-size: var(--t-md); }
  :root.books-v4 .item b { font-size: var(--t-sm); }
  /* These two keep real columns rather than stacking, so they are forced to
     the viewport and long part codes are allowed to break. */
  
  :root.books-v4 #scr-catalog table,
:root.books-v4 #scr-pos table { table-layout: fixed; width: 100%; }
  :root.books-v4 #scr-catalog .item span,
:root.books-v4 #scr-pos .docn { overflow-wrap: anywhere; }
  :root.books-v4 #scr-catalog .amt,
:root.books-v4 #scr-pos .amt { white-space: normal; }
}
/* ── Back office ───────────────────────────────────────────────────────────
   Five pages that share the list contract: stat band, tabs, one primary
   action, stated range. Only Reconcile breaks it, because matching is a
   two-sided job and a table cannot show both sides. */
:root.books-v4 .prog { height: 10px; border-radius: var(--r-xs); background: var(--sunken); overflow: hidden; display: block; }
:root.books-v4 .prog i { display: block; height: 100%; border-radius: var(--r-xs); background: var(--accent); }
:root.books-v4 .prog i.short { background: var(--warn); }
:root.books-v4 .gst-wrap { display: grid; grid-template-columns: minmax(0,1fr) 380px; gap: var(--sp-4); align-items: start; }
:root.books-v4 .gst-b { padding: var(--sp-5); }
:root.books-v4 .gst-fig { display: flex; align-items: baseline; gap: var(--sp-3); margin-bottom: var(--sp-3); }
:root.books-v4 .gst-fig b { font-family: var(--num); font-size: var(--t-fig-lg); font-weight: 600; letter-spacing: -.03em; font-variant-numeric: tabular-nums; }
:root.books-v4 .gst-fig span { font-size: var(--t-md); color: var(--muted); }
:root.books-v4 .gst-legend { display: flex; justify-content: space-between; margin-top: var(--sp-2); font-size: var(--t-sm); color: var(--muted); }
:root.books-v4 .gst-legend b { font-family: var(--num); color: var(--text-2); font-weight: 600; }
:root.books-v4 .advice {
  display: flex; gap: var(--sp-3); align-items: flex-start;
  margin-top: var(--sp-4); padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--accent-edge); background: var(--accent-soft);
  border-radius: var(--r); font-size: var(--t-sm); line-height: 1.5;
}
:root.books-v4 .advice i { color: var(--accent); font-size: var(--t-lg); flex: none; margin-top: 1px; }
:root.books-v4 .advice b { font-family: var(--num); font-weight: 600; }
/* Reconcile: a bank line on the left, what it probably is on the right. */
:root.books-v4 .rec { display: flex; flex-direction: column; }
:root.books-v4 .rec-i {
  display: grid; grid-template-columns: minmax(0,1fr) 34px minmax(0,1fr) auto;
  gap: var(--sp-4); align-items: center;
  padding: var(--sp-4); border-bottom: 1px solid var(--border);
}
:root.books-v4 .rec-i:last-child { border-bottom: 0; }
:root.books-v4 .rec-i:hover { background: var(--card-2); }
:root.books-v4 .bank b { display: block; font-size: var(--t-md); font-weight: 500; }
:root.books-v4 .bank span { display: block; font-size: var(--t-sm); color: var(--muted); }
:root.books-v4 .bank .amt-in { font-family: var(--num); font-weight: 600; color: var(--ok); font-variant-numeric: tabular-nums; }
:root.books-v4 .bank .amt-out { font-family: var(--num); font-weight: 600; color: var(--text-2); font-variant-numeric: tabular-nums; }
:root.books-v4 .rec-arrow { display: grid; place-items: center; color: var(--border-2); font-size: var(--t-xl); }
:root.books-v4 .match {
  padding: var(--sp-3); border: 1px solid var(--border); border-radius: var(--r);
  background: var(--sunken);
}
:root.books-v4 .match b { display: block; font-size: var(--t-md); font-weight: 500; }
:root.books-v4 .match span { display: block; font-size: var(--t-sm); color: var(--muted); }
:root.books-v4 .conf {
  display: inline-flex; align-items: center; gap: 5px; margin-top: 6px;
  font-size: var(--t-xs); font-weight: 500;
}
:root.books-v4 .conf i { width: 6px; height: 6px; border-radius: 50%; display: block; }
:root.books-v4 .conf.hi { color: var(--ok); }
:root.books-v4 .conf.mid { color: var(--warn); }
:root.books-v4 .rec-acts { display: flex; flex-direction: column; gap: 6px; }
@media (max-width: 1180px) {
  :root.books-v4 .gst-wrap { grid-template-columns: minmax(0,1fr); }
}
@media (max-width: 900px) {
  :root.books-v4 .rec-i { grid-template-columns: minmax(0,1fr); gap: var(--sp-3); }
  :root.books-v4 .rec-arrow { transform: rotate(90deg); justify-self: start; }
  :root.books-v4 .rec-acts { flex-direction: row; }
}
/* ── Catalog ───────────────────────────────────────────────────────────────
   A price list is read to answer one question: am I charging enough? So margin
   is drawn against the business's own target, not just printed. */
:root.books-v4 .cat-tools { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; margin-bottom: var(--sp-4); }
:root.books-v4 .cat-tools .sp { margin-left: auto; }
:root.books-v4 .thumb {
  display: grid; place-items: center; flex: none;
  width: 38px; height: 38px; border-radius: var(--r); font-size: var(--t-xl);
}
:root.books-v4 .item { display: flex; align-items: center; gap: var(--sp-3); min-width: 0; }
:root.books-v4 .item b { display: block; font-size: var(--t-md); font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
:root.books-v4 .item span { display: block; font-family: var(--num); font-size: var(--t-xs); color: var(--muted); }
/* Margin: a magnitude, so it takes the neutral accent — never the lateness red.
   The target tick is the only reference on the bar. */
:root.books-v4 .mcell { display: flex; align-items: center; gap: var(--sp-3); justify-content: flex-end; }
:root.books-v4 .mbar { position: relative; width: 84px; height: 8px; border-radius: var(--r-xs); background: var(--sunken); overflow: hidden; flex: none; }
:root.books-v4 .mfill { position: absolute; inset: 0 auto 0 0; border-radius: var(--r-xs); background: var(--accent); }
:root.books-v4 .mfill.low { background: var(--warn); }
:root.books-v4 .mtick { position: absolute; top: -3px; bottom: -3px; width: 2px; background: var(--text-2); border-radius: 1px; }
:root.books-v4 .mpc { font-family: var(--num); font-weight: 600; font-size: var(--t-base); font-variant-numeric: tabular-nums; min-width: 48px; text-align: right; }
:root.books-v4 .mpc.low { color: var(--warn); }
:root.books-v4 .flag {
  display: inline-flex; align-items: center; gap: 5px; padding: 2px var(--sp-2);
  border-radius: var(--r-pill); font-size: var(--t-xs); font-weight: 500;
  color: var(--warn); border: 1px solid color-mix(in srgb, var(--warn) 34%, transparent);
  background: color-mix(in srgb, var(--warn) 10%, transparent); white-space: nowrap;
}
:root.books-v4 .supp { font-size: var(--t-sm); color: var(--text-2); white-space: nowrap; }
:root.books-v4 .supp em { font-style: normal; color: var(--muted); }
/* ── Catalog rows ──────────────────────────────────────────────────────────
   The live catalog's row view (catalog-rows.js) is a VIRTUALISED div-grid, not
   a table: it measures a fixed row height and paints a window. Rebuilding it as
   a <tr> table the way Invoices and Quotes were would break that maths, so the
   canon look is applied to the grid it already has. The one cell that gets new
   markup is Margin — a bar against a target tick is not a restyle of a chip. */
:root.books-v4 .catv2-list {
  border: 1px solid var(--border); border-radius: var(--r-lg);
  background: var(--card); box-shadow: var(--shadow);
}
/* The margin cell is a flag + an 84px bar + a figure, so its column has to be
   wide enough to hold all three; the pre-canon chip needed 84px. */
:root.books-v4 .catv2-head,
:root.books-v4 .catv2-row {
  grid-template-columns: 34px 38px minmax(180px, 1fr) 104px 104px 88px 80px 96px 286px;
  gap: var(--sp-3); padding: 0 var(--sp-3);
}
/* FOUR TRACKS ON A PHONE, and it has to be written HERE. books.css folds this
   table to four columns below 700px, but the rule above re-declares the same
   selector at :root.books-v4 and ties it on specificity (0,3,0) — so the later
   one wins and every hidden column still got its track allocated. The sell
   price was therefore laid out at x=313 in a row that ends at 377, and the
   margin cell at 429 was off the screen: a price catalog whose price you could
   not read. */
@media (max-width: 640px) {
  :root.books-v4 .catv2-head,
  :root.books-v4 .catv2-row {
    grid-template-columns: 34px 38px minmax(0, 1fr) auto;
    gap: var(--sp-2); padding: 0 var(--sp-3);
  }
}
/* A grid column does not clip: at 232px the flag + bar + figure overflowed
   LEFT and painted over the Sell column. The column is now sized to the cell's
   real width, and the cell is told not to grow past it. */
:root.books-v4 .catv2-row .mcell { max-width: 100%; overflow: hidden; }
:root.books-v4 .catv2-row .flag { min-width: 0; overflow: hidden; }
:root.books-v4 .catv2-head {
  height: 34px; background: var(--card); border-bottom: 1px solid var(--border);
  border-radius: var(--r-lg) var(--r-lg) 0 0;
  font-size: var(--t-2xs); font-weight: 600; letter-spacing: .08em; color: var(--muted);
}
:root.books-v4 .catv2-head button[aria-sort="ascending"],
:root.books-v4 .catv2-head button[aria-sort="descending"] { color: var(--text); }
:root.books-v4 .catv2-row { min-height: 56px; border-bottom: 1px solid var(--border); }
:root.books-v4 .catv2-row:hover { background: var(--card-2); }
:root.books-v4 .catv2-row.is-sel { background: var(--accent-soft); }
:root.books-v4 .catv2-thumb {
  width: 38px; height: 38px; border-radius: var(--r);
  border: 1px solid var(--border); background: var(--sunken); font-size: var(--t-xl);
}
:root.books-v4 .catv2-name { font-size: var(--t-md); font-weight: 500; }
:root.books-v4 .catv2-spec { font-family: var(--num); font-size: var(--t-xs); color: var(--muted); }
:root.books-v4 .catv2-cell { font-size: var(--t-sm); color: var(--text-2); }
:root.books-v4 .catv2-n { font-family: var(--num); font-size: var(--t-base); font-variant-numeric: tabular-nums; }
:root.books-v4 .catv2-sell { font-weight: 600; font-size: var(--t-md); color: var(--text); }
:root.books-v4 .catv2-sell small { font-size: var(--t-2xs); font-weight: 400; color: var(--muted); }
/* justify-content, not text-align: .mcell is a flex row, so the parent's
   right-alignment does not reach the bar inside it. */
:root.books-v4 .catv2-row .mcell { justify-content: flex-end; }
@media (max-width: 1180px) {
  :root.books-v4 .cat-hide { display: none; }
}
@media (max-width: 640px) {
  :root.books-v4 .mbar { display: none; }
  :root.books-v4 .thumb { width: 32px; height: 32px; font-size: var(--t-lg); }
}
/* ── Customers: master–detail ──────────────────────────────────────────────
   A customer list is a list of people, not a spreadsheet. Identity carries a
   colour so the eye can find a row it has seen before; every avatar fill/ink
   pair was measured and clears AA for its initials. */
:root.books-v4 .cust-layout { display: grid; grid-template-columns: 392px minmax(0, 1fr); gap: var(--sp-4); align-items: start; }
:root.books-v4 .avatar {
  display: grid; place-items: center; flex: none;
  width: 38px; height: 38px; border-radius: var(--r-lg);
  font-size: var(--t-base); font-weight: 700; letter-spacing: .01em;
}
:root.books-v4 .chips { display: flex; flex-wrap: wrap; gap: 6px; padding: var(--sp-4) var(--sp-4) var(--sp-3); }
:root.books-v4 .fchip {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 5px 11px; border-radius: var(--r-pill);
  border: 1px solid var(--border); background: var(--card);
  font-size: var(--t-sm); color: var(--text-2); white-space: nowrap;
}
:root.books-v4 .fchip:hover { border-color: var(--border-2); color: var(--text); }
:root.books-v4 .fchip.on { border-color: var(--accent-edge); background: var(--accent-soft); color: var(--accent); font-weight: 500; }
:root.books-v4 .fchip i { width: 7px; height: 7px; border-radius: 50%; display: block; }
:root.books-v4 .fchip b { font-family: var(--num); font-weight: 600; font-variant-numeric: tabular-nums; }
:root.books-v4 .clist-h {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: 9px var(--sp-4); border-block: 1px solid var(--border);
  font-size: var(--t-2xs); font-weight: 600; letter-spacing: .09em;
  text-transform: uppercase; color: var(--muted);
}
:root.books-v4 .clist-h .sp { margin-left: auto; }
:root.books-v4 .clist { max-height: 620px; overflow-y: auto; }
:root.books-v4 .crow {
  display: grid; grid-template-columns: 38px minmax(0,1fr) auto; gap: var(--sp-3);
  align-items: center; width: 100%; text-align: left;
  padding: 10px var(--sp-4); border-bottom: 1px solid var(--border);
  border-left: 2px solid transparent;
  transition: background var(--dur) var(--ease);
}
:root.books-v4 .crow:last-child { border-bottom: 0; }
:root.books-v4 .crow:hover { background: var(--card-2); }
/* .is-sel is what customers-page.js toggles; .on is the mockup's own name. The
   ::before is books.css's own inset accent pill — the mockup marks the selected
   row with a full-height left border instead, and two of them read as a stutter. */
:root.books-v4 .crow.on,
:root.books-v4 .crow.is-sel { background: var(--accent-soft); border-left-color: var(--accent); border-radius: 0; }
:root.books-v4 .crow.is-sel::before { display: none; }
:root.books-v4 .crow .nm { display: block; font-size: var(--t-md); font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
:root.books-v4 .crow .ct { display: block; font-size: var(--t-sm); color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
:root.books-v4 .crow .rt { text-align: right; }
:root.books-v4 .crow .owe { font-family: var(--num); font-weight: 600; font-size: var(--t-base); font-variant-numeric: tabular-nums; display: block; }
:root.books-v4 .crow .owe.bad { color: var(--danger); }
:root.books-v4 .crow .paid { font-size: var(--t-sm); font-weight: 500; color: var(--ok); display: block; }
:root.books-v4 .crow .ago { font-size: var(--t-2xs); color: var(--muted); display: block; margin-top: 2px; }
:root.books-v4 .cd-h { padding: var(--sp-5); border-bottom: 1px solid var(--border); }
:root.books-v4 .cd-top { display: flex; gap: var(--sp-4); align-items: flex-start; }
:root.books-v4 .cd-top .avatar { width: 56px; height: 56px; border-radius: 14px; font-size: var(--t-2xl); }
/* An <h2> in the live markup, so the UA/books.css heading defaults get reset. */
:root.books-v4 .cd-name { font-size: var(--t-2xl); letter-spacing: -.02em; font-weight: 600; margin: 0; line-height: 1.25; }
:root.books-v4 .cd-meta { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-5); margin-top: 7px; font-size: var(--t-base); color: var(--text-2); }
:root.books-v4 .cd-meta span { display: inline-flex; align-items: center; gap: 7px; }
:root.books-v4 .cd-meta i { color: var(--muted); font-size: var(--t-lg); }
:root.books-v4 .cd-acts { display: flex; gap: var(--sp-2); margin-top: var(--sp-4); flex-wrap: wrap; }
:root.books-v4 .cstats { display: grid; grid-template-columns: repeat(4, 1fr); border-bottom: 1px solid var(--border); }
:root.books-v4 .cstat { padding: var(--sp-4) var(--sp-5); border-left: 1px solid var(--border); }
:root.books-v4 .cstat:first-child { border-left: 0; }
/* The mockup carried these on the label span inline; same declarations. */
:root.books-v4 .cstat-l {
  font-size: var(--t-2xs); font-weight: 600; letter-spacing: .09em;
  text-transform: uppercase; color: var(--muted);
}
/* "Next visit" is a date, not a figure — it needs a step down to fit. */
:root.books-v4 .cstat b.sm { font-size: var(--t-lg); }
:root.books-v4 .cstat b {
  display: block; margin-top: 6px; font-family: var(--num);
  font-size: 22px; font-weight: 600; letter-spacing: -.02em; font-variant-numeric: tabular-nums;
}
:root.books-v4 .cstat b.bad { color: var(--danger); }
:root.books-v4 .cstat b.accent { color: var(--accent); }
:root.books-v4 .cstat em { display: block; margin-top: 3px; font-style: normal; font-size: var(--t-xs); color: var(--muted); }
:root.books-v4 .ctabs { display: flex; gap: 2px; padding: 0 var(--sp-5); border-bottom: 1px solid var(--border); overflow-x: auto; }
:root.books-v4 .ctab {
  padding: var(--sp-3) var(--sp-3) 10px; font-size: var(--t-md); color: var(--muted);
  border-bottom: 2px solid transparent; margin-bottom: -1px; white-space: nowrap;
}
:root.books-v4 .ctab:hover { color: var(--text-2); }
:root.books-v4 .ctab.on { color: var(--text); border-bottom-color: var(--accent); font-weight: 500; }
:root.books-v4 .cbody { padding: var(--sp-5); }
@media (max-width: 1180px) {
  :root.books-v4 .cust-layout { grid-template-columns: minmax(0, 1fr); }
  :root.books-v4 .clist { max-height: 360px; }
}
@media (max-width: 640px) {
  :root.books-v4 .cstats { grid-template-columns: repeat(2, 1fr); }
  :root.books-v4 .cstat:nth-child(3),
:root.books-v4 .cstat:nth-child(4) { border-top: 1px solid var(--border); }
  :root.books-v4 .cstat:nth-child(odd) { border-left: 0; }
  :root.books-v4 .cd-meta { gap: var(--sp-1); }
  :root.books-v4 .cd-meta span { width: 100%; }
}
/* ── Document + context rail ───────────────────────────────────────────────
   A document on a wide screen is a narrow column of lines with dead space
   beside it. The rail fills that with the things an owner opens an overdue
   invoice to do: what is owed, what has already been tried, who owes it, and
   which job it came from. */
:root.books-v4 .doc-layout {
  display: grid; grid-template-columns: minmax(0, 1fr) 366px;
  gap: var(--sp-4); align-items: start;
}
:root.books-v4 .doc { max-width: none; }
:root.books-v4 .side { position: sticky; top: var(--sp-4); display: flex; flex-direction: column; gap: var(--sp-4); }
:root.books-v4 .panel { background: var(--card); border: 1px solid var(--border); border-radius: var(--r-lg); box-shadow: var(--shadow); }
:root.books-v4 .panel-h {
  display: flex; align-items: baseline; gap: var(--sp-3);
  padding: var(--sp-4) var(--sp-5) 0;
}
:root.books-v4 .panel-h h3 { font-size: var(--t-md); font-weight: 600; }
:root.books-v4 .panel-h .sp { margin-left: auto; }
:root.books-v4 .panel-h a { font-size: var(--t-sm); color: var(--accent); }
:root.books-v4 .panel-b { padding: var(--sp-3) var(--sp-5) var(--sp-5); }
:root.books-v4 .due-big {
  font-family: var(--num); font-size: var(--t-fig-lg); font-weight: 600;
  letter-spacing: -.03em; font-variant-numeric: tabular-nums; display: block;
}
/* The mockup's only example was overdue, so .due-sub was hard-coded to danger.
   A settled or not-yet-due invoice must not print its date in red. */
:root.books-v4 .due-sub { display: block; margin-top: 5px; font-size: var(--t-sm); color: var(--muted); font-weight: 500; }
:root.books-v4 .due-big.bad + .due-sub { color: var(--danger); }
:root.books-v4 .cap {
  display: block; font-size: var(--t-2xs); font-weight: 600; letter-spacing: .09em;
  text-transform: uppercase; color: var(--muted);
}
:root.books-v4 .due-big.bad { color: var(--danger); }
/* ── The document + its rail ───────────────────────────────────────────────
   layout-v3.js already wraps the editor into .bk3-main + .bk3-rail and moves
   the context blocks across. The canon does not build a second rail; it gives
   that one the mockup's proportions and panel chrome. Every selector is
   :root.books-v4 (0,3,0) against books.css's html.books-v3 (0,2,1), so these
   win without !important. */
:root.books-v4 #editorView.bk3-has-rail {
  grid-template-columns: minmax(0, 1fr) 366px; gap: var(--sp-4); align-items: start;
}
/* The document reads as one sheet, not a stack of loose sections. */
:root.books-v4 #editorView.bk3-has-rail > .bk3-main {
  background: var(--card); border: 1px solid var(--border);
  border-radius: var(--r-lg); box-shadow: var(--shadow);
  padding: var(--sp-5); display: flex; flex-direction: column; gap: var(--sp-4);
}
:root.books-v4 #editorView.bk3-has-rail > .bk3-rail {
  display: flex; flex-direction: column; gap: var(--sp-4);
  position: sticky; top: var(--sp-4);
}
/* Each rail block takes the panel's chrome — including the ones layout-v3 moved
   across, which arrive as bare divs. */
:root.books-v4 #editorView.bk3-has-rail > .bk3-rail > * {
  background: var(--card); border: 1px solid var(--border);
  border-radius: var(--r-lg); box-shadow: var(--shadow);
}
:root.books-v4 .doc-due .panel-b { padding: var(--sp-5); }
/* ── Tooltip ───────────────────────────────────────────────────────────────── */
:root.books-v4 .tip {
  position: fixed; z-index: 90; pointer-events: none; opacity: 0;
  transform: translate(-50%, -100%) translateY(-9px);
  background: var(--surface); border: 1px solid var(--border-2); border-radius: var(--r);
  box-shadow: var(--shadow); padding: 9px 11px; min-width: 132px;
  transition: opacity .12s var(--ease);
}
:root.books-v4 .tip.on { opacity: 1; }
:root.books-v4 .tip b { display: block; font-size: var(--t-sm); font-weight: 600; margin-bottom: 5px; }
:root.books-v4 .tip-r { display: flex; align-items: center; gap: 7px; font-size: var(--t-sm); color: var(--text-2); white-space: nowrap; }
:root.books-v4 .tip-r .vl { margin-left: auto; font-family: var(--num); font-weight: 600; color: var(--text); font-variant-numeric: tabular-nums; }
/* Blocks moved into the rail keep their own ids and handlers but take the
   panel's chrome, so they read as panels rather than as sections that lost
   their page. */
:root.books-v4 .side > :is(.ed-activity, .ed-payments, .ed-actions) { padding: var(--sp-4) var(--sp-5); }
:root.books-v4 .side > .ed-actions { display: flex; flex-wrap: wrap; gap: var(--sp-2); position: static; }
:root.books-v4 .side > .ed-actions > * { flex: 1 1 auto; }
/* The document column is one card, so the blocks inside it lose the borders and
   backgrounds they carried as standalone sections. */
:root.books-v4 .doc > :is(.ed-grid, .items-head, .ed-totals, .ed-coach) { padding: 0 var(--sp-5); }
:root.books-v4 .doc > .ed-items { width: calc(100% - var(--sp-5) * 2); margin: 0 var(--sp-5); }
:root.books-v4 .doc > :last-child { margin-bottom: var(--sp-5); }
:root.books-v4 .due-acts { display: flex; flex-direction: column; gap: var(--sp-2); margin-top: var(--sp-4); }
:root.books-v4 .due-acts .btn { width: 100%; justify-content: center; height: 36px; }
:root.books-v4 .due-row { display: flex; gap: var(--sp-2); }
:root.books-v4 .due-row .btn { flex: 1; }
/* Activity: what has already been tried, so nobody chases twice. */
:root.books-v4 .tl { display: flex; flex-direction: column; }
:root.books-v4 .tl-i { display: grid; grid-template-columns: 15px 1fr; gap: var(--sp-3); padding-bottom: var(--sp-4); position: relative; }
:root.books-v4 .tl-i:last-child { padding-bottom: 0; }
:root.books-v4 .tl-i::before {
  content: ""; position: absolute; left: 7px; top: 15px; bottom: 0; width: 1px; background: var(--border);
}
:root.books-v4 .tl-i:last-child::before { display: none; }
:root.books-v4 .tl-d {
  width: 9px; height: 9px; border-radius: 50%; margin-top: var(--sp-1); margin-left: 3px;
  background: var(--border-2); position: relative; z-index: 1;
  box-shadow: 0 0 0 3px var(--card);
}
:root.books-v4 .tl-d.on { background: var(--accent); }
:root.books-v4 .tl-d.bad { background: var(--danger); }
:root.books-v4 .tl-t { font-size: var(--t-sm); line-height: 1.4; }
:root.books-v4 .tl-t b { font-weight: 500; }
:root.books-v4 .tl-t span { display: block; color: var(--muted); font-size: var(--t-xs); margin-top: 1px; }
:root.books-v4 .kv { display: flex; justify-content: space-between; gap: var(--sp-3); padding: 6px 0; font-size: var(--t-sm); }
:root.books-v4 .kv span { color: var(--muted); }
:root.books-v4 .kv b { font-family: var(--num); font-weight: 600; font-variant-numeric: tabular-nums; }
:root.books-v4 .kv b.plain { font-family: var(--face); font-weight: 500; }
:root.books-v4 .who-row { display: flex; align-items: center; gap: var(--sp-3); margin-bottom: var(--sp-3); }
:root.books-v4 .who-row .av { width: 34px; height: 34px; }
:root.books-v4 .who-row b { display: block; font-size: var(--t-md); font-weight: 500; }
:root.books-v4 .who-row span { font-size: var(--t-xs); color: var(--muted); }
:root.books-v4 .job-link {
  display: flex; align-items: center; gap: var(--sp-3); width: 100%;
  padding: var(--sp-3); border: 1px solid var(--border); border-radius: var(--r);
  background: var(--sunken); text-align: left;
}
:root.books-v4 .job-link:hover { border-color: var(--border-2); }
:root.books-v4 .job-link i { font-size: var(--t-xl); color: var(--muted); flex: none; }
:root.books-v4 .job-link b { display: block; font-size: var(--t-sm); font-weight: 500; }
:root.books-v4 .job-link span { font-size: var(--t-xs); color: var(--muted); }
@media (max-width: 1180px) {
  :root.books-v4 .doc-layout { grid-template-columns: minmax(0, 1fr); }
  :root.books-v4 .side { position: static; }
}
/* On a phone the rail must not land under the line items — the amount due and
   its actions are why the invoice was opened. The rail dissolves so its panels
   sit in the page order that answers that: what is owed, the document, then
   who owes it. */
@media (max-width: 900px) {
  /* align-items:start is right for the two-column grid, where it stops cards
     stretching to the tallest. Once this is a flex COLUMN that same property
     controls width, so it has to go back to stretch or every panel shrinks to
     its own content. */
  
  :root.books-v4 .doc-layout { display: flex; flex-direction: column; align-items: stretch; }
  :root.books-v4 .side { display: contents; }
  :root.books-v4 .doc-layout > .doc { order: 2; }
  :root.books-v4 .side > .panel { order: 3; }
  :root.books-v4 .side > .panel:first-child { order: 1; }
}
@media (max-width: 640px) {
  :root.books-v4 .doc-gst { display: none; }
  :root.books-v4 .lines th,
:root.books-v4 .lines td { padding-left: var(--sp-2); padding-right: var(--sp-2); }
  :root.books-v4 .lines th:first-child,
:root.books-v4 .lines td:first-child { padding-left: var(--sp-4); }
  :root.books-v4 .lines th:last-child,
:root.books-v4 .lines td:last-child { padding-right: var(--sp-4); }
  :root.books-v4 .doc-meta { gap: var(--sp-2) var(--sp-4); }
  :root.books-v4 .tot-w { padding: var(--sp-4); }
  :root.books-v4 .tot-t { width: 100%; }
}
/* ── Document editor ───────────────────────────────────────────────────────── */
:root.books-v4 .doc { }
:root.books-v4 .doc-h { display: flex; align-items: flex-start; gap: var(--sp-4); padding: var(--sp-5); border-bottom: 1px solid var(--border); }
:root.books-v4 .doc-h h2 { font-size: var(--t-xl); }
:root.books-v4 .doc-meta { display: flex; gap: var(--sp-5); margin-top: var(--sp-3); flex-wrap: wrap; }
:root.books-v4 .mi { font-size: var(--t-sm); }
:root.books-v4 .mi span { display: block; color: var(--muted); font-size: var(--t-xs); margin-bottom: 1px; }
:root.books-v4 .mi b { font-weight: 500; font-family: var(--num); font-variant-numeric: tabular-nums; }
:root.books-v4 .tot { padding: var(--sp-4) var(--sp-5); display: flex; justify-content: flex-end; }
:root.books-v4 .tot-t { width: 260px; }
:root.books-v4 .tr { display: flex; justify-content: space-between; padding: 5px 0; font-size: var(--t-md); color: var(--text-2); }
:root.books-v4 .tr .num { color: var(--text); }
:root.books-v4 .tr.big { margin-top: var(--sp-2); padding-top: var(--sp-3); border-top: 1px solid var(--border); font-size: var(--t-lg); font-weight: 600; color: var(--text); }
:root.books-v4 .tr.big .num { font-size: var(--t-xl); }
/* ── Dev bar (mockup chrome, not part of the design) ───────────────────────── */
:root.books-v4 .content { padding-bottom: 56px; }
/* ─────────────────────────────────────────────────────────────────────────────
   RESPONSIVE — the canon layout folds; it never reflows a money column.
   ───────────────────────────────────────────────────────────────────────────── */
/* Below full desktop the four-across KPI row squeezes its figures, and the
   8/4 split starves the chart. Both go to halves, then to full width. */
@media (max-width: 1320px) {
  :root.books-v4 .c3 { grid-column: span 6; }
  :root.books-v4 .c4,
:root.books-v4 .c6,
:root.books-v4 .c8 { grid-column: span 12; }
}
@media (max-width: 900px) {
  :root.books-v4 .topbar { padding: 9px var(--sp-3); gap: var(--sp-2); }
  :root.books-v4 .books-back { display: none; }
  :root.books-v4 .books-brand span:last-child { display: none; }
  :root.books-v4 .hd { flex-wrap: wrap; }
  :root.books-v4 .hd-t { width: 100%; margin-right: 0; }
  :root.books-v4 .srch { flex: 1; width: auto; min-width: 0; }
  :root.books-v4 .content { padding: var(--sp-4) var(--sp-3) 150px; }
  /* books.css orders .books-brand to 1, so the controls books-bar.js injects at
     runtime need an order too or they jump ahead of the logo. */
  
  :root.books-v4 .bar-sp,
:root.books-v4 .srch,
:root.books-v4 #newBtn,
:root.books-v4 .av { order: 2; }
  :root.books-v4 .more-menu { position: fixed; left: var(--sp-3); right: var(--sp-3); bottom: 112px; top: auto; }
  :root.books-v4 .c3,
:root.books-v4 .c4,
:root.books-v4 .c6,
:root.books-v4 .c8 { grid-column: span 6; }
  :root.books-v4 .c12 { grid-column: span 12; }
  :root.books-v4 .kpi-v { font-size: var(--t-2xl); }
  :root.books-v4 .kpi-v small { display: none; }
   /* the count returns on the tile it belongs to */
  
  :root.books-v4 .spark { display: none; }
}
/* A money table cannot be crushed into a phone — below this width the invoice
   rows stop being a grid of columns and become a list of documents: who, how
   much, what state, when. Job and bulk-select are desktop concerns. */
@media (max-width: 640px) {
  /* A card title and its subtitle stop competing for one line. */
  
  :root.books-v4 .card-h { flex-wrap: wrap; }
  :root.books-v4 .card-h p { width: 100%; }
  :root.books-v4 .tbl-inv thead { display: none; }
  :root.books-v4 .tbl-inv,
:root.books-v4 .tbl-inv tbody,
:root.books-v4 .tbl-inv tr,
:root.books-v4 .tbl-inv td { display: block; }
  /* TWO LINES, NOT THREE, AND THE SAME TWO ON EVERY ROW. The first cut put the
     due cell on a third line of its own and let the sent/opened footnotes wrap
     under the status pill, which made row height depend on how much had
     happened to the invoice: an overdue-and-opened row ran 230px while a paid
     one ran 120px, and a list you cannot scan by rhythm is a list you read
     twice. Three columns across two rows fits who/how-much above
     which/when/what-state, and every row is the same height.

       Kiama RSL                          $2,480.00
       INV-1042   12 days late             Overdue  */
  :root.books-v4 .tbl-inv tr {
    display: grid; grid-template-columns: auto 1fr auto;
    gap: var(--sp-1) var(--sp-2_5);
    align-items: center;
    padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--border);
  }
  :root.books-v4 .tbl-inv td { border: 0; padding: 0; }
  :root.books-v4 .tbl-inv .t-ck,
:root.books-v4 .tbl-inv .t-job,
:root.books-v4 .tbl-inv .t-act { display: none; }
  :root.books-v4 .tbl-inv .t-cust {
    grid-area: 1 / 1 / 2 / 3; font-size: var(--t-md); min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  :root.books-v4 .tbl-inv .t-amt { grid-area: 1 / 3; text-align: right; white-space: nowrap; }
  :root.books-v4 .tbl-inv .t-no { grid-area: 2 / 1; }
  :root.books-v4 .tbl-inv .t-due { grid-area: 2 / 2; text-align: left !important; }
  /* ROW-REVERSE so the pill keeps the right edge and the footnote glyph sits
     inside it. Left to source order the eye pushed the pill off the margin, and
     one row's status no longer lined up with the rest — the same failure of
     rhythm this block exists to fix, just 20px of it. */
  :root.books-v4 .tbl-inv .t-st {
    grid-area: 2 / 3; justify-self: end;
    display: flex; flex-direction: row-reverse; align-items: center; gap: var(--sp-1_5);
  }
  /* An unnumbered document has no number to show, so .t-no prints the word
     "Draft" — beside a pill already saying it. */
  :root.books-v4 .tbl-inv tr[data-st="draft"] .t-no { display: none; }

  /* A ROW MUST NOT SAY THE SAME WORD TWICE. dueCell answers "how late?", and
     for a settled or unissued document there is no such question — so it falls
     back to the lifecycle word, which is exactly what the pill beside it
     already says. On the desktop table that column has to hold something; here
     it is a repeat, and the draft row managed to print "Draft" three times.
     data-st is on the row for this (core/ui/doclist.js rowV4). */
  :root.books-v4 .tbl-inv tr[data-st="paid"] .t-due,
:root.books-v4 .tbl-inv tr[data-st="draft"] .t-due,
:root.books-v4 .tbl-inv tr[data-st="void"] .t-due { display: none; }

  /* The footnotes stay — "they opened it and still haven't paid" is worth
     knowing — but as their glyph rather than their sentence, on the line they
     annotate. Same font-size:0 idiom books.css uses to fold the install button
     on a phone. */
  :root.books-v4 .tbl-inv .t-notes {
    display: inline-flex; gap: var(--sp-1); margin: 0; vertical-align: middle;
  }
  :root.books-v4 .tbl-inv .t-notes .ui-badge { font-size: 0; gap: 0; padding: 0; border: 0; background: none; }
  :root.books-v4 .tbl-inv .t-notes .ui-badge i { font-size: var(--t-sm); }
}
/* books.css moves the tabs to the bottom bar at 560px — match that number, not
   this mockup's own 900px content breakpoint. */
@media (max-width: 560px) {
  /* Production ships exactly five tabs and the bar is sized for five. A sixth
     crowds the labels at 390px, so More stands down here and back office is
     reached from the Overview cards instead. */
  
  :root.books-v4 .more-w { display: none; }
  /* Mockup chrome only — lifts the nav above the dev bar. Do NOT port. */
  
  :root.books-v4 .books-tabs { bottom: 46px; }
  :root.books-v4 body { padding-bottom: 0; }
  :root.books-v4 .hd-t h1 { font-size: var(--t-xl); }
  :root.books-v4 .srch kbd { display: none; }
  :root.books-v4 .c3,
:root.books-v4 .c4,
:root.books-v4 .c6,
:root.books-v4 .c8 { grid-column: span 12; }
  :root.books-v4 .age-r { grid-template-columns: 74px 1fr 78px; }
  :root.books-v4 td,
:root.books-v4 th { padding-left: var(--sp-2); padding-right: var(--sp-2); }
}
/* The blanket reduced-motion kill that was here (every duration → .01ms) also
   removed the colour and opacity feedback that tells you a control responded,
   which is the opposite of what the preference asks for. The MOTION section at
   the end of this file handles it properly: spatial animation off, state
   transitions kept. Only the indefinite skeleton pulse is stopped here. */
@media (prefers-reduced-motion: reduce) {
  :root.books-v4 .sk { animation: none; }
}


/* ═══ CUSTOMERS ════════════════════════════════════════════════════════════
   The live page already has the mockup's bones — a filter rail, a list of
   people, and a detail pane beside it — under .cx-* names. So this maps those
   components onto the canon look rather than rewriting 1,400 lines of renderer
   to change class names. The avatars carry fill/ink pairs (customers-page.js
   _avatarStyle), which is the one thing CSS alone could not do. */
:root.books-v4 .cx-av {
  border-radius: var(--r-lg);
  font-weight: 700;
  letter-spacing: .01em;
}
:root.books-v4 .cx-row {
  border-left: 2px solid transparent;
  border-bottom: 1px solid var(--border);
  padding: 10px var(--sp-4);
  transition: background var(--dur) var(--ease);
}
:root.books-v4 .cx-row:hover { background: var(--card-2); }
:root.books-v4 .cx-row.is-sel {
  background: var(--accent-soft);
  border-left-color: var(--accent);
}
:root.books-v4 .cx-rname .nm { font-size: var(--t-md); font-weight: 500; }
:root.books-v4 .cx-rsub { font-size: var(--t-sm); color: var(--muted); }
:root.books-v4 .cx-bal {
  font-family: var(--num);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  font-size: var(--t-base);
}
:root.books-v4 .cx-ractive { font-size: var(--t-2xs); color: var(--muted); margin-top: 2px; }

/* Filter chips, matching the mockup's .fchip. */
:root.books-v4 .cx-seg {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  padding: 5px 11px; border-radius: var(--r-pill);
  border: 1px solid var(--border); background: var(--card);
  font-size: var(--t-sm); color: var(--text-2); white-space: nowrap;
}
:root.books-v4 .cx-seg:hover { border-color: var(--border-2); color: var(--text); }
:root.books-v4 .cx-seg.is-active {
  border-color: var(--accent-edge); background: var(--accent-soft);
  color: var(--accent); font-weight: 500;
}
:root.books-v4 .cx-seg-n { font-family: var(--num); font-weight: 600; font-variant-numeric: tabular-nums; }

/* Detail pane. */
:root.books-v4 .cx-pcard {
  background: var(--card); border: 1px solid var(--border);
  border-radius: var(--r-lg); box-shadow: var(--shadow);
}
:root.books-v4 .cx-hero { padding: var(--sp-5); border-bottom: 1px solid var(--border); }
:root.books-v4 .cx-hname { font-size: var(--t-2xl); letter-spacing: -.02em; font-weight: 600; }
:root.books-v4 .cx-hcontact { font-size: var(--t-base); color: var(--text-2); }
:root.books-v4 .cx-hero .cx-av { width: 56px; height: 56px; border-radius: 14px; font-size: var(--t-2xl); }
:root.books-v4 .cx-metrics {
  display: grid; grid-template-columns: repeat(4, 1fr);
  border-bottom: 1px solid var(--border);
}
:root.books-v4 .cx-metrics > * { padding: var(--sp-4) var(--sp-5); border-left: 1px solid var(--border); }
:root.books-v4 .cx-metrics > :first-child { border-left: 0; }
:root.books-v4 .cx-metrics b {
  display: block; margin-top: 6px; font-family: var(--num);
  font-size: 22px; font-weight: 600; letter-spacing: -.02em; font-variant-numeric: tabular-nums;
}
:root.books-v4 .cx-tabs {
  display: flex; gap: 2px; padding: 0 var(--sp-5);
  border-bottom: 1px solid var(--border);
}
:root.books-v4 .cx-tabs > * {
  padding: var(--sp-3) var(--sp-3) 10px; font-size: var(--t-md); color: var(--muted);
  border-bottom: 2px solid transparent; margin-bottom: -1px; white-space: nowrap;
}
:root.books-v4 .cx-tabs > .is-active {
  color: var(--text); border-bottom-color: var(--accent); font-weight: 500;
}
:root.books-v4 .cx-tabbody { padding: var(--sp-5); }

@media (max-width: 640px) {
  :root.books-v4 .cx-metrics { grid-template-columns: 1fr 1fr; }
  :root.books-v4 .cx-metrics > :nth-child(3),
  :root.books-v4 .cx-metrics > :nth-child(4) { border-top: 1px solid var(--border); }
  :root.books-v4 .cx-metrics > :nth-child(odd) { border-left: 0; }
  :root.books-v4 .cx-tabs { flex-wrap: wrap; overflow: visible; }
}


/* ── Filter rail ───────────────────────────────────────────────────────────
   Invoices, Quotes and Customers all render their filters as .ui-rail__seg
   under list-v2, so this is the canon .fchip treatment applied once for all
   three. It wraps rather than scrolling: a strip that hides its own tail is a
   filter nobody knows is there. */
:root.books-v4 .ui-rail {
  display: flex; flex-wrap: wrap; gap: 6px;
  overflow: visible; padding-bottom: 0;
}
:root.books-v4 .ui-rail__seg {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  padding: 5px 11px; border-radius: var(--r-pill);
  border: 1px solid var(--border); background: var(--card);
  font-size: var(--t-sm); color: var(--text-2); white-space: nowrap;
  transition: border-color var(--dur) var(--ease), color var(--dur) var(--ease);
}
:root.books-v4 .ui-rail__seg:hover { border-color: var(--border-2); color: var(--text); }
:root.books-v4 .ui-rail__seg.is-active {
  border-color: var(--accent-edge); background: var(--accent-soft);
  color: var(--accent); font-weight: 500;
}
:root.books-v4 .ui-rail__seg.is-active b { color: var(--accent); }
:root.books-v4 .ui-rail__seg b {
  font-family: var(--num); font-weight: 600; font-size: var(--t-xs);
  font-variant-numeric: tabular-nums; color: var(--muted);
}
:root.books-v4 .ui-rail__seg.is-hot { border-color: color-mix(in srgb, var(--danger) 45%, transparent); color: var(--danger); }
:root.books-v4 .ui-rail__seg.is-hot b { color: var(--danger); }
:root.books-v4 .ui-rail__dot { width: 7px; height: 7px; border-radius: 50%; flex: none; }


/* ── Customers: page shell to match the mockup ─────────────────────────────
   #custKpis hosts the four-tile grid; the list and detail sit in the mockup's
   392px + 1fr split. */
:root.books-v4 #custKpis { margin-bottom: var(--sp-4); }
:root.books-v4 #custKpis .grid { margin: 0; }
:root.books-v4 .cx-split {
  display: grid;
  grid-template-columns: 392px minmax(0, 1fr);
  gap: var(--sp-4);
  align-items: start;
}
/* The list is a card in the mockup, with the chips and the shown/sort row
   inside it rather than floating above. */
:root.books-v4 .cx-list {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow);
  overflow: hidden;
}
:root.books-v4 .cx-segs { display: flex; flex-wrap: wrap; gap: 6px; padding: var(--sp-4) var(--sp-4) var(--sp-3); }
:root.books-v4 .cx-tools {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: 9px var(--sp-4); border-block: 1px solid var(--border);
  font-size: var(--t-2xs); font-weight: 600; letter-spacing: .09em;
  text-transform: uppercase; color: var(--muted);
}
:root.books-v4 .cx-showing { margin-right: auto; }
:root.books-v4 .cx-sortlbl { text-transform: none; letter-spacing: 0; font-weight: 400; display: inline-flex; align-items: center; gap: var(--sp-2); }
:root.books-v4 .cx-rows { margin: 0; padding: 0; list-style: none; max-height: 620px; overflow-y: auto; }
:root.books-v4 .cx-rows .cx-row:last-child { border-bottom: 0; }
/* Detail-pane actions read as the mockup's button row, primary last. */
:root.books-v4 .cx-hactions { display: flex; gap: var(--sp-2); flex-wrap: wrap; margin-top: var(--sp-4); }
:root.books-v4 .cx-hactions a,
:root.books-v4 .cx-hactions button { text-decoration: none; }
:root.books-v4 .cx-hero .cx-hbody { display: flex; gap: var(--sp-4); align-items: flex-start; }
:root.books-v4 .cx-hcontact { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-5); margin-top: 7px; }
:root.books-v4 .cx-hcontact i { color: var(--muted); }

@media (max-width: 1180px) {
  :root.books-v4 .cx-split { grid-template-columns: minmax(0, 1fr); }
  :root.books-v4 .cx-rows { max-height: 360px; }
}

/* A SCROLLBOX INSIDE A SCROLLING PAGE. The cap above is right for the narrow
   DESKTOP split, where the list column still has to sit beside the profile and
   stay reachable. On a phone the split has already stacked, so all the cap does
   is trap the gesture: flick on the list and the list moves, flick ten pixels
   to the side and the page moves, and the customer you were looking for is in
   neither. Let it flow with the page.

   THIS HAS TO LIVE HERE, not up in the phone block with the rest of the
   customers work. Both media queries match at 390px and both selectors are
   (0,2,0), so the later one wins — and the block above is later. Written up
   there it read correctly and did nothing, which is the same way three other
   phone rules in this file have failed. */
@media (max-width: 640px) {
  :root.books-v4 .cx-rows { max-height: none; overflow-y: visible; }

  /* FOUR COLUMNS OF MONEY DO NOT FIT IN 366px. repeat(4, 1fr) will not shrink a
     track below its content, so the payments band measured 405px of columns in
     a 366px page and took the whole document sideways with it — on the one
     screen whose entire job is reporting cash. minmax(0, …) is what lets a
     track actually give. */
  :root.books-v4 :is(.pf-band, .rc-band, .sp-band, .sq-band) {
    grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-2);
  }
  :root.books-v4 :is(.pf-kpi, .rc-kpi, .sp-kpi, .sq-kpi) { padding: var(--sp-3); min-width: 0; }

  /* ── The directory is a DRILL-DOWN here, not a split ─────────────────────
     Two panes side by side is what makes master–detail work; stacked on a
     phone it is just a very long page — the list runs the full height of the
     directory and the profile sits under all of it, so choosing a second
     customer means scrolling back past everything you already read.
     Selecting replaces the directory with the profile (customers-page.js
     cxDetail), and the back control returns to the list at the scroll position
     it was left at. Above 640px nothing here applies and the split stands. */
  :root.books-v4 .cx-split.is-detail .cx-list { display: none; }
  :root.books-v4 .cx-split:not(.is-detail) .cx-profile { display: none; }
  :root.books-v4 .cx-split.is-detail .cx-back { display: inline-flex; }

  /* The directory chrome belongs to the directory. On the detail screen the
     search, the + Add and the stat band are all answering a question the
     reader has already left. */
  :root.books-v4 .cx-wrap:has(.cx-split.is-detail) .cx-head,
  :root.books-v4 .cx-wrap:has(.cx-split.is-detail) .cx-kpis { display: none; }
}

/* The back control is drawn only on the phone detail screen (rule above); it is
   in the markup at every width, so it is hidden by default rather than by a
   media query it would otherwise escape. */
:root.books-v4 .cx-back {
  display: none; align-items: center; gap: var(--sp-2);
  margin: 0 0 var(--sp-3); padding: var(--sp-2_5) var(--sp-3);
  min-height: 44px; border-radius: var(--r-lg, 12px);
  background: var(--card); border: 1px solid var(--border); color: var(--text);
  font: 600 var(--t-sm) var(--face, inherit); cursor: pointer;
}
:root.books-v4 .cx-back:active { background: var(--card-hover, var(--sunken)); }
:root.books-v4 .cx-back i { font-size: var(--t-lg); }


/* ═══ MOTION ═══════════════════════════════════════════════════════════════
   Books is a surface people work in, so motion here explains feedback, state
   and continuity — it is never scenery. Three jobs only:

     1. The Overview's figures draw themselves ONCE on arrival. That screen is
        about the numbers, and a chart that grows from its baseline reads as
        something computed from your data rather than printed onto the page.
     2. The customer detail pane cross-fades when you pick a different row, so
        it reads as one pane with new contents rather than a new pane.
     3. Anything you can press acknowledges the press.

   No page-load choreography, no scroll reveals, no hover lift. Everything is
   transform or opacity, so nothing here triggers layout.
   ═════════════════════════════════════════════════════════════════════════ */

/* 1 · The figures ────────────────────────────────────────────────────────── */
/* Bars grow from the baseline they are measured against, not from nothing.
   transform-box: fill-box makes transform-origin resolve against the rect's own
   box rather than the SVG viewport, which is what makes "bottom" mean bottom. */
@keyframes cn-rise { from { transform: scaleY(0); } to { transform: scaleY(1); } }
@keyframes cn-grow { from { transform: scaleX(0); } to { transform: scaleX(1); } }
:root.books-v4 .cn-draw .bar {
  transform-box: fill-box; transform-origin: 50% 100%;
  animation: cn-rise .42s cubic-bezier(.16, 1, .3, 1) backwards;
  /* Left to right, but capped: 12 months must not become a 2-second wait. */
  animation-delay: calc(var(--i, 0) * 22ms);
}
:root.books-v4 .cn-draw .age-f {
  transform-origin: 0 50%;
  animation: cn-grow .42s cubic-bezier(.16, 1, .3, 1) backwards;
  animation-delay: calc(var(--i, 0) * 40ms);
}
/* The donut is drawn by the Web Animations API instead (overview-page.js): its
   segments are positioned WITH stroke-dasharray, so the sweep has to
   interpolate the same property the layout depends on — CSS cannot express
   "from zero to whatever the attribute happens to say". */

/* 2 · Continuity ─────────────────────────────────────────────────────────── */
/* Selecting a different customer replaces the pane's contents. 140ms and 4px is
   enough to say "this changed" without making the click feel slow. */
@keyframes cn-pane { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
:root.books-v4 .cx-profile.is-swap > * { animation: cn-pane .14s var(--ease) backwards; }
/* Tab bodies swap inside the pane, so they get the same treatment one level in. */
:root.books-v4 .cbody.is-swap { animation: cn-pane .14s var(--ease) backwards; }

/* 3 · Press feedback ─────────────────────────────────────────────────────── */
/* A press is the one moment the interface should feel physical. Small, fast,
   and on the way DOWN only — the release is the result appearing. */
:root.books-v4 :is(.btn, .btn-primary, .card.kpi, .crow, .fchip, .ui-rail__seg):active {
  transform: translateY(.5px) scale(.995);
}
:root.books-v4 :is(.btn, .btn-primary, .card.kpi, .crow, .fchip, .ui-rail__seg) {
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease),
              transform .08s var(--ease);
}
/* The selected row's accent edge grows into place rather than appearing, which
   makes it read as the selection moving down the list. */
:root.books-v4 .crow { transition: background var(--dur) var(--ease), border-left-color .12s var(--ease); }

/* Reduced motion ─────────────────────────────────────────────────────────── */
/* NOT a blanket kill. The earlier rule flattened every duration to .01ms, which
   also removed the colour and opacity feedback that tells you a control
   responded. Reduced motion means less MOVEMENT: the spatial animations stop,
   the state transitions stay. */
@media (prefers-reduced-motion: reduce) {
  :root.books-v4 .cn-draw .bar,
  :root.books-v4 .cn-draw .age-f,
  :root.books-v4 .cx-profile.is-swap > *,
  :root.books-v4 .cbody.is-swap { animation: none; }
  :root.books-v4 :is(.btn, .btn-primary, .card.kpi, .crow, .fchip, .ui-rail__seg):active { transform: none; }
}

/* Announced to screen readers, invisible to everyone else — the skeleton is
   aria-hidden, so without this a non-sighted user gets silence while the page
   loads instead of the "Loading…" they used to get. */
:root.books-v4 .nd-sr-only {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
/* The skeleton's own bars stagger their pulse, so the block reads as one
   surface breathing rather than a grid of independent flashing rectangles. */
:root.books-v4 .card.kpi .sk:nth-child(2) { animation-delay: .12s; }
:root.books-v4 .card.kpi .sk:nth-child(3) { animation-delay: .24s; }

/* A card whose body IS a list of rows carries no padding of its own — .crow
   brings its own, and doubling them insets the hover band from the card edge
   so the row looks like it is floating rather than filling. */
:root.books-v4 .card-b.cn-rows { padding: 0; }
/* The 38px lead slot of a .crow, holding an icon instead of an avatar. Same
   footprint as .avatar so the two row kinds line up when stacked. */
:root.books-v4 .cn-ico {
  width: 34px; height: 34px; border-radius: var(--r-sm);
  display: grid; place-items: center;
  background: var(--accent-soft); border: 1px solid var(--accent-edge);
  color: var(--accent); font-size: var(--t-lg);
}
/* The setup card's steps are links until they are done; a completed step is a
   statement, not somewhere to go. */
:root.books-v4 .tl-t a { color: inherit; text-decoration: none; border-bottom: 1px solid var(--accent-edge); }
:root.books-v4 .tl-t a:hover { color: var(--accent); border-bottom-color: var(--accent); }
/* A finished step recedes. The card's job is to get the REMAINING ones done, so
   the accent dot marks what is left rather than congratulating what is not —
   otherwise the only row you can act on is the faintest thing on the card. */
:root.books-v4 .tl-i.is-done .tl-t b { color: var(--muted); font-weight: 400; }
:root.books-v4 .tl-i.is-done .tl-t span { color: var(--muted); opacity: .7; }

/* .advice carries the accent by default — right for a suggestion, wrong for a
   warning and wrong for good news. The tints follow the same recipe the status
   pills use, so a warning here and a warning on a pill are the same colour.

   Named adv-*, NOT .warn: books.css already owns a global `.warn` — an
   unrelated banner component with its own colour, padding and margin-bottom.
   At (0,1,0) it lost the background and the border to this rule but kept the
   text colour, so warn notes came out with amber PROSE while good and info
   notes did not. The tint and the icon carry the tone; the words stay in
   reading ink, the same as every other sentence on the page. */
:root.books-v4 .advice.adv-warn {
  border-color: color-mix(in srgb, var(--warn) 34%, transparent);
  background: color-mix(in srgb, var(--warn) 10%, transparent);
}
:root.books-v4 .advice.adv-warn i { color: var(--warn); }
:root.books-v4 .advice.adv-good {
  border-color: color-mix(in srgb, var(--ok) 34%, transparent);
  background: color-mix(in srgb, var(--ok) 10%, transparent);
}
:root.books-v4 .advice.adv-good i { color: var(--ok); }
/* The retry sits on a tinted note, where a ghost button's edge disappears. */
:root.books-v4 .advice .btn { border-color: var(--border-2); background: var(--card); }
/* Stacked, .advice's own top margin is the gap between notes — but the first
   one sits directly under the card head, which already has its padding. */
:root.books-v4 .card-b > .advice:first-child { margin-top: 0; }
/* Waiting on the model. Three bars of falling width read as "sentences are
   coming"; a spinner reads as "something is stuck". */
:root.books-v4 .ai-sk { display: flex; flex-direction: column; gap: var(--sp-3); padding: var(--sp-2) 0; }
:root.books-v4 .ai-sk .sk { height: 11px; }

/* ═══ THE OVERVIEW AS WIDGETS (feature_books_widgets) ══════════════════════
   The widget host wraps every widget in its own tile: a bordered card with an
   icon-and-title header. The canon's cards already ARE that — so mounted as-is
   you get two frames and the title twice ("Owed to you" above "Owed to you").

   The canon card wins. The host's tile becomes a transparent positioning box,
   and its header — which is also Gridstack's drag handle — only appears while
   you are customising, which is the only time you need something to grab. */
/* overflow HIDDEN, not visible. Visible let a card whose content is taller than
   its cell spill over the tile below, so the rows overlapped instead of
   clipping — which reads as a broken grid rather than a too-small widget. The
   row unit is now sized so nothing needs to spill; this is the backstop for a
   user who drags a widget smaller than its content. */
:root.books-v4 #booksWidgetHost .wgt-tile {
  background: none; border: 0; box-shadow: none; padding: 0; overflow: hidden;
  /* Each tile is its own layout and paint boundary, so relaying out one chart
     cannot invalidate the other seven while the grid is reflowing around a
     drag. This is the difference between the browser re-costing one card and
     re-costing the whole page on every frame of a gesture. */
  contain: layout paint;
}
/* The tile clips on the SAME curve as the card it holds. --radius is a
   dashboard token that Books never defines, so the tile's radius resolved to 0
   — a square clip box around a 10px-rounded card. Harmless now the tile is
   transparent, wrong the moment anything paints on it again. */
:root.books-v4 #booksWidgetHost .wgt-tile { border-radius: var(--r-lg); }
:root.books-v4 #booksWidgetHost .wgt-body { padding: 0; height: 100%; }
/* The card is the visible surface here, so the per-tile background the
   Settings sheet writes belongs on the card, not on the box behind it. */
:root.books-v4 #booksWidgetHost .wgt-body > .card {
  height: 100%; background: var(--wgt-bg, var(--card));
}
:root.books-v4 #booksWidgetHost .wgt-head { display: none; }
/* Customising: the handle comes back, sitting ON the card rather than above it,
   so nothing reflows when you enter and leave the mode. */
/* NOT the Gridstack tile. Gridstack lays a tile out as `position:absolute` with
   top/bottom pinned to the cell, which is what makes a card fill the space it
   occupies. Flipping it to relative drops it back into normal flow at
   height:auto, so every card shrank to its own content the instant you pressed
   Customize — the donut lost 82px, Needs attention lost 154px — while the
   dashed cell stayed the size it really was. You could no longer see what size
   you were dragging. An absolute tile is already a containing block, so the
   header below anchors to it with no help from here; only the no-Gridstack
   fallback, whose tiles are static, needs this. */
:root.books-v4 #booksWidgetHost.wgt-editing .wgt-tile:not(.grid-stack-item-content) { position: relative; }
:root.books-v4 #booksWidgetHost.wgt-editing .wgt-head {
  display: flex; position: absolute; inset: 0 0 auto; z-index: 2;
  align-items: center; gap: var(--sp-2); padding: 6px var(--sp-3);
  background: color-mix(in srgb, var(--accent) 16%, var(--card));
  border: 1px solid var(--accent-edge);
  border-radius: var(--r-lg) var(--r-lg) 0 0;
  font-size: var(--t-xs); cursor: grab;
  /* One line, always. The handle sits ON the card, so any second line is height
     it takes from the figure underneath: on a one-column tile "Collected this
     month" wrapped, the seven controls fell to a second row, and the $0 it
     names ended up half-covered by its own drag handle. The title yields
     first — it is a grab label, and the card it sits on says the same thing. */
  flex-wrap: nowrap;
}
/* inline-BLOCK, not the host's inline-flex: text-overflow only applies to a
   block container's own inline content, and a flex container's text is an
   anonymous item it cannot reach. As inline-flex the label clipped mid-word
   with no ellipsis, so "Collected this month" read as "Collected this mon". */
:root.books-v4 #booksWidgetHost.wgt-editing .wgt-title {
  display: inline-block; min-width: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
:root.books-v4 #booksWidgetHost.wgt-editing .wgt-title i { vertical-align: -0.09em; }
:root.books-v4 #booksWidgetHost.wgt-editing .wgt-ctrls { flex: 0 0 auto; flex-wrap: nowrap; }
:root.books-v4 #booksWidgetHost.wgt-editing .wgt-head:active { cursor: grabbing; }
/* A dashed edge is the standard "this is now movable" signal, and it does not
   move anything: the border replaces the card's own, same width. */
:root.books-v4 #booksWidgetHost.wgt-editing .wgt-body > .card {
  border-style: dashed; border-color: var(--accent-edge);
}


/* ══ TOUCH TARGETS ═════════════════════════════════════════════════════════
   books.css already states this rule and applies it to the catalog's chips and
   the shared list toolbar: "a target too small to hit is a bug, not a
   redesign", coarse pointers only so desktop density is untouched. Every
   control the CANON introduced was left out of it, and an audit of all
   fourteen Books pages at 390px found them at 24-34px: the money sub-nav on
   five pages, the status tabs on invoices and quotes, the customer segments,
   every .*-btn and .*-chip on payments, reconcile, Stripe, Square, drawings
   and tax.

   The rules live HERE rather than beside their cousins in books.css because
   the canon sizes these components — .pf-btn is `height: 34px` at
   :root.books-v4, which beats an unscoped min-height in the earlier file. That
   fixed height is also why `height: auto` has to come first: a min-height
   alone cannot grow a box that has been told exactly how tall to be. ══ */
@media (hover: none) {
  :root.books-v4 :is(.pf-btn, .rc-btn, .tx-btn, .sp-btn, .sq-btn, .dw-btn) {
    height: auto; min-height: 44px;
  }
  :root.books-v4 :is(.pf-chip, .rc-tag, .sp-chip, .sq-chip, .dw-chip,
    .pf-pill, .rc-pill, .sp-pill, .sq-pill) { min-height: 44px; }
  :root.books-v4 :is(.pf-sel, .pf-search, .rc-sel, .sp-sel, .sq-sel, .dw-sel) { min-height: 44px; }
  :root.books-v4 .mn-sub a { min-height: 44px; }
  :root.books-v4 .tabs .tab { min-height: 44px; }
  :root.books-v4 .ui-rail__seg { min-height: 44px; }
  :root.books-v4 .cx-head .btn-primary { min-height: 44px; }
  :root.books-v4 .cx-sortlbl select:not([class]) { min-height: 44px; }
  :root.books-v4 .cat-bulk-markup input { min-height: 44px; }
  :root.books-v4 .cat-facet { min-height: 44px; }
  /* THE HIT AREA, NOT THE BOX. The catalog's select tick is 18px, and a 44px
     checkbox is a 44px checkbox — visually absurd on a dense row. The target
     grows underneath it instead, which is the whole reason ::before exists. */
  :root.books-v4 .catv2-tickwrap { position: relative; }
  :root.books-v4 .catv2-tick { position: relative; }
  :root.books-v4 .catv2-tick::before {
    content: ''; position: absolute; inset: -13px; border-radius: 8px;
  }
  /* The integration banners' own CTA — "Set up payments", "Connect Xero". */
  :root.books-v4 :is(.btn-brand, .sb .btn-primary, .sb .btn-ghost) { min-height: 44px; }
}
