/* ===========================================================================
   App shell + Home — ported from the approved R13 prototype
   (design-review/v2/r13-merge/prototype/final-candidate-r13.html).

   Loaded AFTER style.css, which still owns shared form/table primitives. Every
   rule here is scoped to `.shell` (chrome) or to one page scope — `.home`,
   `.invoices`, `.reports`, `.templates`, `.entity` — so a rule can never reach
   markup it was not written for. Since m1-reports-polish every authenticated
   page is on one of those scopes: the transitional `.main.legacy` white-paper
   surface is gone, and both palettes reach the whole app.

   z-index comes only from the token scale below (behavior standard §11).
   =========================================================================== */

:root {
  /* the preview mark: a token, but a single-value one — see `.doc-watermark`
     for why it must not flip with the theme */
  --doc-watermark: rgba(31, 41, 38, 0.13);
  --bg: #ecebe6;
  --paper: #ffffff;
  --paper-2: #f8f7f3;
  --paper-3: #f1f0ea;
  --ink: #1c2b27;
  --muted: #64706c;
  /* Was #929b97, verbatim from the prototype. It carries the captions that
     name every stat, every column header and every field hint, and it measured
     2.66–2.85:1 on the three surfaces it lands on (white, --paper-2,
     --green-softer) — a hard WCAG AA failure at 11–11.5px. Darkened to the
     lightest grey that clears 4.5:1 on all three (4.89 / 4.56 / 4.62). It now
     sits a hair lighter than --muted rather than a tier below it: two greys
     that both have to be legible at 11px cannot be far apart. A prototype-level
     correction, flagged to the owner rather than charged to any one screen. */
  --faint: #6a736f;
  --line: #ddd9d0;
  --line-2: #ece9e2;
  /* The boundary that *identifies* a control, distinct from the rule that
     separates content. --line on --paper measures 1.41:1; SC 1.4.11 asks 3:1
     of a component boundary. Used on inputs/selects/combos only. */
  --line-control: #8b938f;
  /* The rim a secondary button or a Home tile takes on hover — a step darker
     than --line, and until item 17a a hex painted straight into six rules,
     which meant one fixed grey in BOTH palettes. */
  --line-hover: #aeb8b3;
  --green: #08785a;
  --green-dark: #056146;
  --green-soft: #e4f2ec;
  /* the flash toast's ARRIVAL colour — a saturated step above the
     resting tint, which it settles back to over a second (owner
     2026-08-15). Chosen to keep the text above 4.5:1 for the whole
     transit: #056146 on #cdeade measures 5.8:1. */
  --flash-peak-bg: #cdeade;
  --bar-issued: #9fd1c0; /* the charts' Issued series AND its legend swatch */
  /* The mint RIM — the create card, the switch-tabs track, the appearance
     card. Stronger than --green-soft (a fill) and quieter than --green (a
     mark); it was a hex in three rules, so dark wore the light mint. */
  --green-rim: #8fc6b5;
  --green-softer: #f3faf7;
  /* The label a primary button paints on --green. White on the light --green is
     5.46:1; white on the DARK --green (#48bd95) measured 2.34:1 — the app's most
     important control was its least legible one in one of two shipped themes.
     Dark flips the label to the deep ink instead of losing the mint: 7.34:1. */
  --on-green: #ffffff;
  --blue: #315f78;
  --blue-soft: #e7f0f5;
  /* The pressed step of --blue. Added with item 17a because the legacy
     preview chrome it replaces HAD a hover (`#1d4ed8` under `#2563eb`), and a
     control that loses its pressed state to a tokenisation has been made
     worse, not tidier. */
  --blue-strong: #25485c;
  /* The template-preview chrome sits on a FIXED-light stage (`#f4f3ef` and
     the white A4 page), so its ink must not invert with the palette — an
     inverting --blue erased the enlarge control and the watermark in dark
     (2026-08-21). Same value in BOTH palettes IS the design here: the
     surface never changes, so neither may the ink. */
  --blue-fixed: #315f78;
  --blue-fixed-strong: #25485c;
  /* A tooltip is a surface of its own, so it carries its own pair. Light is
     the near-black box it always was; dark INVERTS rather than darkening,
     which is the visible half of item 17a — `#15241f` against a `--paper` of
     `#1a2723` was a box the reader could not see. */
  --tooltip-bg: #15241f;
  --tooltip-ink: #ffffff;
  --amber: #a65a0b;
  --amber-soft: #fff1df;
  /* WINE, owner decision R1 (2026-08-09), chosen from rendered candidates: the
     prototype's brick #bd3c35 was never the bordeaux the product means, and
     this token is the one reading for overdue everywhere (Home's tile, the
     Invoices badge, Reports' tile and column) plus the nine danger surfaces.
     8.25:1 on the light paper against the brick's 5.44:1. The DARK palette
     keeps its own light red below — wine measures 1.4–1.7:1 on those
     surfaces, so one hex for both palettes would be unreadable. */
  --red: #8c1c2b;
  --red-soft: #fdebea;
  --violet: #6d4fa3;
  --violet-soft: #efe9f8;
  --shadow: 0 22px 70px rgba(33, 48, 43, 0.13);
  --backdrop: rgba(18, 30, 26, 0.45);
  --radius: 15px;
  --radius-sm: 10px;
  /* One control scale, two steps (the panel measured SEVEN heights). `md` is
     every form field, toolbar control and footer button; `sm` is a control that
     lives INSIDE a table row, where 44px would double the row. Touch raises
     `sm` to `md` — see the `pointer: coarse` block at the end of this file. */
  --ctl: 44px;
  --ctl-sm: 36px;
  /* the entity tables' actions column: two `sm` buttons and a kebab, stated
     once so the three tables share it and a list cap can align to it.
     200 since round 4: the kebab became a 36px square (+17). */
  --rowacts-w: 200px;
  --font-ui: -apple-system, "Helvetica Neue", Helvetica, Arial, sans-serif;
  --sidebar: 220px;
  --cap: 1580px;
  --z-topbar: 20;
  --z-nav: 70;
  --z-modal: 100;
  --z-drawer: 120;
  --z-modal-high: 140;
  --z-modal-top: 170;
  --z-confirm: 190;
  /* the flash toast: above every bar and card, below an open popover — a
     message about what just happened must not cover the menu the reader
     opened next (R4-1) */
  --z-toast: 200;
  --z-popover: 240;
  --z-toast: 260;
}

html[data-theme="dark"] {
  --bg: #111b18;
  --paper: #1a2723;
  --paper-2: #20302b;
  --paper-3: #253630;
  --ink: #f2f5f3;
  --muted: #bdc7c2;
  /* dark was marginal rather than failing (4.45:1 on --paper-2); nudged to 5.04 */
  --faint: #939f99;
  --line: #35443f;
  --line-2: #2b3934;
  --line-control: #6a746f;
  /* a hover rim has to LIFT off the dark surfaces, where the light grey it
     used to share sat flat against neither */
  --line-hover: #4d5f59;
  --green: #48bd95;
  --green-dark: #64d0aa;
  --green-soft: #173d31;
  /* dark's peak, same rule: #64d0aa on #1a4a3a measures 5.2:1 */
  --flash-peak-bg: #1a4a3a;
  --green-rim: #2f7a63;
  --green-softer: #153229;
  --on-green: #0b1f19;
  --blue: #8ab6cb;
  --blue-soft: #1b3541;
  /* dark's blue is a LIGHT one, so its pressed step is lighter still */
  --blue-strong: #a8cbdd;
  /* deliberately the LIGHT palette's pair: the preview stage is fixed-light
     in both themes, see the light block's note */
  --blue-fixed: #315f78;
  --blue-fixed-strong: #25485c;
  /* inverted on purpose: a near-white chip is unmistakable over a dark chart,
     where a near-black one was invisible */
  --tooltip-bg: #e8efec;
  --tooltip-ink: #12211c;
  --amber: #e6a354;
  --amber-soft: #3a2b1b;
  --red: #f08a83;
  --red-soft: #402321;
  --violet: #b8a1e0;
  --violet-soft: #322847;
  --shadow: 0 22px 70px rgba(0, 0, 0, 0.32);
  /* A dialog surface at rgb(26,39,35) over a page dimmed to rgb(18,24,21)
     measured 1.16:1 — the modal had no visible edge in dark. */
  --backdrop: rgba(4, 10, 8, 0.62);
}

/* Native widgets (date, month, file, the select chevron, scrollbars) are the
   UA's to paint, and it paints them light unless told. Three separate white
   slabs in the dark app traced to this one missing declaration. Scoped to the
   app body: the public auth pages are a light-only surface (style.css) and
   must not inherit a palette they were never drawn for. */
body:not(.public) { color-scheme: light; }
html[data-theme="dark"] body:not(.public) { color-scheme: dark; }

/* the legacy flex body layout belonged to the old sidebar shell */
body:not(.public) {
  display: block;
  background: var(--bg);
  color: var(--ink);
}

/* --- shell frame --------------------------------------------------------- */

.shell {
  width: min(calc(100% - 28px), var(--cap));
  margin: 14px auto;
  min-height: calc(100dvh - 28px);
  display: grid;
  grid-template-columns: var(--sidebar) minmax(0, 1fr);
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 18px;
  /* `clip`, not `hidden`: both round off the corners, but `hidden` makes this a
     SCROLL CONTAINER whose scrollTop is permanently 0 — so every
     `position: sticky` descendant silently stopped sticking, which is why the
     company editor's commit bar rendered at document y=2735 instead of pinned
     to the viewport (design critique CO-01). `clip` clips without scrolling. */
  overflow: clip;
  box-shadow: var(--shadow);
}

/* The navigation STAYS (owner, 2026-08-08, on a tablet held upright): it was a
   plain grid item, so the shell stretched it to the page's full height and
   every link scrolled off the top with the content — leaving a blank column
   where the menu belongs and no way to leave the screen without scrolling back
   up. The topbar was already sticky, so the chrome half-stayed, which is what
   made it read as a broken layout rather than a long page.

   `align-self: start` stops the stretch, the height is the window's, and
   `top` matches the shell's own 14px margin so it pins flush under it. The
   sidebar is a flex column already, so its footer keeps sitting at the bottom.
   Below the phone breakpoint the nav becomes a bottom bar with its own rules
   further down; this only governs the desktop column. */
.shell .sidebar {
  min-width: 0;
  padding: 22px 12px 16px;
  border-right: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  background: var(--paper);
}

/* …and only where it IS a column: below the phone breakpoint the same element
   is a fixed BOTTOM bar, and an unscoped `top` moved it to the top of the
   window, over the burger it shares the topbar with (caught by the shell
   walk, which clicks that burger at 900). */
@media (min-width: 901px) {
  .shell .sidebar {
    position: sticky;
    top: 14px;
    align-self: start;
    height: calc(100dvh - 28px);
    overflow-y: auto;
  }
}

.shell .brand {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 0 10px 21px;
  font-weight: 850;
  color: var(--ink);
  text-decoration: none;
}

.shell .brandmark {
  width: 31px;
  height: 31px;
  border-radius: 9px;
  background: var(--green);
  display: grid;
  place-items: center;
  color: var(--on-green);
  font-size: 13px;
  flex: none;
}

.shell .nav {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.shell .nav a {
  border: 0;
  background: transparent;
  color: var(--muted);
  padding: 11px 13px;
  border-radius: 10px;
  text-align: left;
  font-weight: 680;
  font-size: 14px;
  text-decoration: none;
  display: flex;
  align-items: center;
  gap: 10px;
}

.shell .nav a:hover,
.shell .nav a.active {
  background: var(--green-soft);
  color: var(--green);
}

.shell .nav .icon {
  width: 18px;
  text-align: center;
  color: var(--faint);
  flex: none;
}

.shell .nav a.active .icon {
  color: var(--green);
}

.shell .sidebar-footer {
  margin-top: auto;
  border-top: 1px dashed var(--line);
  padding: 15px 10px 0;
  color: var(--faint);
  font-size: 12px;
  line-height: 1.55;
}

.shell .sidebar-footer .account-email {
  display: block;
  color: var(--muted);
  text-decoration: none;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.shell .sidebar-footer .account-email:hover {
  color: var(--green);
}

.shell .sidebar-footer .logout-form {
  margin: 8px 0 12px;
}

.shell .sidebar-footer .logout-btn {
  width: 100%;
  border: 1px solid var(--line);
  background: var(--paper);
  color: var(--muted);
  border-radius: 9px;
  padding: 6px 10px;
  font-size: 12px;
  cursor: pointer;
}

.shell .sidebar-footer .logout-btn:hover {
  color: var(--ink);
  border-color: var(--faint);
}

.shell .workspace {
  min-width: 0;
  display: flex;
  flex-direction: column;
}

/* --- topbar -------------------------------------------------------------- */

.shell .topbar {
  height: 62px;
  border-bottom: 1px solid var(--line);
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 0 28px;
  background: var(--paper);
  position: sticky;
  top: 0;
  z-index: var(--z-topbar);
}

.shell .topbar .spacer {
  flex: 1;
}

.shell .topbar .btn {
  border: 1px solid var(--line);
  background: var(--paper);
  color: var(--ink);
  padding: 8px 12px;
  border-radius: 10px;
  font: inherit;
  font-weight: 730;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  white-space: nowrap;
  cursor: pointer;
}

.shell .topbar .btn:hover {
  border-color: var(--faint);
  background: var(--paper-2);
}

.shell .topbar .iconbtn {
  width: var(--ctl-sm);
  height: var(--ctl-sm);
  padding: 0;
  font-size: 17px;
  line-height: 1;
}

.shell .topbar .ghost {
  background: transparent;
}

/* Inline icons follow their type: sized in `em`, stroked in `currentColor`,
   so a hover, a palette or a font-size change reaches them like a glyph. */
.shell .ic,
.entity .ic {
  display: block;
  flex: none;
}

/* `.company-pill` / `.avatar` deliberately absent: the topbar's company pill is
   gone (owner 2026-08-05) and nothing else in the app renders a monogram — the
   invoice document's own monogram is inside the self-contained invoice
   templates and never reads this file. */

/* --- content region ------------------------------------------------------ */

.shell .main {
  padding: 28px clamp(22px, 3vw, 42px) 50px;
  min-width: 0;
  max-width: none;
  margin: 0;
  flex: 1;
}

/* --- R13 page primitives (shared by every redesigned page) ---------------
   `.r13` is set on <main> together with the page's own scope class
   (`r13 home`, `r13 invoices`), so a redesigned page opts out of the
   legacy white-paper surface and into these tokens in both themes. */

.r13 {
  font-size: 14px;
}

.r13 .pagehead {
  display: flex;
  align-items: flex-start;
  gap: 16px;
}

.r13 .pagehead .spacer {
  flex: 1;
}

.r13 h1 {
  font-size: 30px;
  line-height: 1.12;
  margin: 0;
  letter-spacing: -0.025em;
  font-weight: 800;
  color: var(--ink);
}

.r13 h2 {
  font-size: 21px;
  line-height: 1.2;
  margin: 0;
  font-weight: 780;
  color: var(--ink);
}

.r13 .sub {
  color: var(--muted);
  font-size: 14px;
  margin-top: 6px;
}

.r13 .eyebrow {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--faint);
  font-weight: 800;
}

.r13 .row {
  display: flex;
  align-items: center;
  gap: 10px;
}

.r13 .wrap {
  flex-wrap: wrap;
}

.r13 .spacer {
  flex: 1;
}

.r13 .num,
.r13 .money,
.r13 .mono {
  /* the r13 numeral face — every money/count figure is monospace + tabular
     (prototype law); Reports relied on this and rendered sans without it */
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-variant-numeric: tabular-nums;
}

/* `.num` carries two jobs: right-align the column (which a header must share —
   the project invariant) and set the numeral face (which a header must NOT: a
   header is prose). `LINES TOTAL USED BY` rendered monospace beside
   `PRODUCT CURRENCY ACTIONS` in the UI face — two typefaces in one header row,
   on Products, Clients, Companies and Reports at once. The alignment stays;
   the face goes back to the UI stack for th only. */
.r13 th.num,
.r13 .thead .num,
.r13 .line-head .num,
.r13 .run-grid-head .num,
.r13 td.num::before,
.r13 th.num::before {
  font-family: var(--font-ui);
}

.r13 .card,
.r13 .tile {
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

.r13 .card .card-head {
  padding: 14px 17px;
  border-bottom: 1px solid var(--line-2);
  display: flex;
  align-items: center;
  gap: 10px;
  font-weight: 780;
}

.r13 .card .card-body {
  padding: 17px;
}

.r13 .btn {
  border: 1px solid var(--line);
  background: var(--paper);
  color: var(--ink);
  padding: 10px 14px;
  border-radius: 10px;
  font: inherit;
  font-weight: 730;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  white-space: nowrap;
  cursor: pointer;
}

.r13 .btn:hover {
  border-color: var(--line-hover);
  background: var(--paper);
}

/* A blocked action has to LOOK blocked. `Issue invoices` carries `disabled`
   whenever the run has a blocker — and rendered identically to a live button,
   so the only sign was the sentence beside it. Nothing here changes which
   buttons are disabled; it changes whether that is visible. */
.r13 .btn:disabled,
.r13 .btn[disabled] {
  opacity: 0.45;
  cursor: not-allowed;
}

.r13 .btn:disabled:hover,
.r13 .btn[disabled]:hover {
  border-color: var(--line);
}

.r13 .btn.primary:disabled:hover {
  background: var(--green);
}


.r13 .btn.primary {
  background: var(--green);
  border-color: var(--green);
  color: var(--on-green);
}

.r13 .btn.primary:hover {
  background: var(--green-dark);
}

/* An icon button is square, and its side is the COMPACT step of the control
   scale — the same box as every `.btn.sm` in a row, because that is the rank it
   holds: an auxiliary control with a glyph instead of a label. It was 38px,
   which belonged to no scale at all and made the topbar's two buttons and every
   dialog's × a third height on every screen in the app (design critique
   2026-08-06 §4.1). A thumb still gets 44px — see the `pointer: coarse` block
   at the end of this file. */
.r13 .btn.iconbtn,
.entity .btn.iconbtn {
  width: var(--ctl-sm);
  height: var(--ctl-sm);
  min-height: 0;
  padding: 0;
  font-size: 17px;
  line-height: 1;
  flex: none;
}

.r13 .btn.ghost {
  background: transparent;
}

.r13 .btn.sm {
  padding: 7px 10px;
  font-size: 13px;
}

/* `margin: 0` is not tidiness — it is the whole of the Invoices row pitch.
   `style.css` still carries the legacy STATUS-MESSAGE rule
   `.ok { … margin: 0 0 var(--sp-4) }` — a success banner. The R13 payment badge
   is `class="badge ok"`, so it matched that rule too and every Paid badge in
   the app carried a 16px bottom margin nobody had written for it. That is why
   `.trow` measured 68px on a Paid row and 63px on every other one: the status
   cell went 28.6px → 44.6px and became the tallest cell in the row.

   Measured, not reasoned: swapping the badge's class from `ok` to `blue` in the
   live DOM collapsed the cell 44.59 → 28.59, while swapping its TEXT changed
   nothing. (The published diagnosis — a stray whitespace text node from the
   Jinja `{% elif %}` chain — is wrong; removing every text node from the cell
   left it at 44.59.) One declaration, and the table gains a constant pitch. */
.r13 .badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 5px 8px;
  border-radius: 999px;
  background: var(--paper-3);
  font-size: 12px;
  font-weight: 720;
  color: var(--muted);
  margin: 0;
}

/* --- The filter band ------------------------------------------------------
   ONE pattern for every screen that narrows a list — Invoices (all three
   tabs), Clients, Products, Companies and Reports. It used to be four:
   `.invoices`, `.entity` and `.reports` each carried a near-copy of these
   rules with different numbers (search 715 / 340 / 300 px, input padding
   10/9/8 px, type 14/13.5/13.5 px, chip row 14 / 0 px, and on `.entity` no
   chip rule at all, so the chips fell through to legacy style.css and painted
   violet). Owner 2026-08-06: "the filter arrangement itself is untidy… all
   filters and searches must share one style, across all screens".

   Two rows, and only two:

     .toolbar   what narrows the list — search, then the selects, then the
                list's own actions pushed to the far end by `.listacts`
     .chiprow   what is currently narrowing it — one chip per active filter,
                and the single `Reset` that clears them all

   `Reset` lives on the chip row, not in the toolbar: it undoes the chips, so
   it belongs beside them, and the rule for drawing it is then the same rule
   on every screen — a chip exists, therefore Reset does.

   Spacing is the prototype's own (`final-candidate-r13.html`, `.toolbar
   { margin: 18px 0 }`): the band owns the gap ABOVE it. That 18px was the one
   thing the port lost, which is why the page's explanatory `.sub` sat 0px off
   the search box on every entity screen. Adjacent margins collapse, so the
   18px is shared with whatever precedes it rather than added to it. */

.r13 .toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  /* stated, not defaulted: whatever else defines `.toolbar`, the controls sit
     next to each other and the gap between them is the gap, not the leftover */
  justify-content: flex-start;
  gap: 9px;
  margin: 18px 0 14px;
}

/* A search box is sized to what people type, never to the row it sits in.
   Measured in the app's own face (14px, -apple-system) with canvas
   `measureText`, the way `verify_walk_qty_rate.check_fits` measures a select:
   the widest placeholder in the product — `Search number, client, product or
   payer…` — is 262px of text, so 288px of box with the 26px of padding and
   border; the longest whole value anyone types into these fields is a billing
   email at 24 characters (`billing@meridian.example`, 159px → 185px of box).
   300px holds that placeholder with 10px to spare (measured in the box, 261.7
   of 272px of text room), and it is 300 rather than
   320 because those 20px are load-bearing: `flex-wrap` wraps on the sum of the
   flex BASES before it shrinks anything, so the search box's basis sets the
   band's wrap point directly, and 320 put Invoices at 1040px — sixteen pixels
   the wrong side of a 1024 window. The Invoices box had grown to 715px on
   `flex: 1 1 240px` — room for 88
   characters, i.e. 395px of white the eye crosses to reach the first filter.
   THE BASIS IS 184, THE RESTING WIDTH IS STILL 300 (owner round 2b, item 11).
   The paragraph above used to argue for a 300px BASIS, and it argued itself
   into the defect: because wrapping is decided on the sum of the bases, the
   300px basis was also the band's wrap point, so the row broke at 998px on
   Invoices and 1014px on Clients — two rows on the owner's screen — while the
   search kept all 300 of its pixels. His law reverses the trade: the search
   shrinks, the row holds. `max-width` keeps the resting width at exactly the
   measured 300px wherever there is room, so the "one pattern" argument
   survives intact; 184 rather than 190 because at 901 the Clients band's three
   combos and gaps need 441 of 627px, and 190 still wrapped in the 901–904
   sliver. The cap is scoped to `.toolbar` — the Review matrix's own search is
   not in a band and must keep its full width. */
.r13 .search {
  flex: 1 1 184px;
  min-width: 0;
}

.r13 .toolbar .search {
  max-width: 300px;
}

/* THE CONTROL SCALE, stated once for the whole band.
   `--ctl` is the only height a band control has. It used to be a height the
   padding happened to add up to, and it added up differently per scope: the
   identical `<select class="btn">` measured 41px on `/invoices` and 44px on
   `/clients`, because `.entity input, .entity select` pins `min-height` and the
   `.invoices` scope does not (design critique 2026-08-06 §4.2). Six heights —
   36 / 38 / 40 / 41 / 42 / 44 — shipped against a declared two.

   So the shared rule pins it here, where the band's controls are defined, and
   the `.entity` form rule keeps its own copy of the SAME token for the fields
   on the entity forms. `padding` is scoped to `.toolbar` for the same reason:
   `.entity input[type="search"]` outranks `.r13 .search-input`, so the one
   search box was 10px 12px on three screens and 10px 13px on two. */
.r13 .search-input,
.r13 select.btn,
.r13 .toolbar .combo > .combo-input {
  width: 100%;
  min-height: var(--ctl);
  border: 1px solid var(--line-control);
  background: var(--paper);
  color: var(--ink);
  border-radius: 10px;
  padding: 10px 13px;
  font: inherit;
}

.r13 .toolbar .search-input {
  padding: 10px 13px;
}

/* Two kinds of picker, and the difference is real rather than cosmetic.

   A CLOSED VOCABULARY the domain owns — payment status, currency, reporting
   period — is a native `<select>`. Its options are short, few and fixed
   forever, so it sizes itself honestly and can never surprise the layout.

   A LIST THE BOOK OWNS — companies, countries — is the app's combobox
   (`data-combo-source`, below). A `<select>` sizes itself to its WIDEST option,
   so one long legal name in the book decides the width of a filter on every
   screen that lists it: the demo's `Cloudpine Studio & Digital Infrastructure
   Holdings s.r.o.` pinned the issuer picker to its full 240px cap to show a
   resting label of 94px, and those 146px of slack are what wrapped the Invoices
   band onto a second row below a 1385px viewport — every 1366 laptop, and every
   1440 window that is not maximised. The cap survives for the native selects
   that still need it. */
.r13 select.btn {
  width: auto;
  max-width: 240px;
  text-overflow: ellipsis;
  font-weight: 730;
}

/* A band picker is sized to its LABEL, not to the book. `All companies` is 94px
   of the band's own face (measured with canvas `measureText` at 14px/730) and
   `All countries` 83px; 140px holds either with 97px of text box, and it is one
   number for every band picker on every screen rather than a per-screen guess.

   Nothing is lost by not fitting a 300px legal name inside it: the picker is
   the CONTROL, and the chip row directly below is the STATEMENT — a narrowed
   list already prints `Issuer: <the whole name>` as a removable chip. Nothing
   does double duty. */
.r13 .toolbar .combo {
  flex: 0 0 auto;
  width: 138px;
}

.r13 .toolbar .combo > .combo-input {
  font-weight: 730;
  text-overflow: ellipsis;
  /* the caret is 11px of glyph sitting 11px from the edge, so 25px clears it;
     the 3px this returns over the default 28 is 3px of the band's wrap point */
  padding-right: 25px;
}

/* …and the SAME width discipline for the native selects beside them, because
   the band is one component: `All statuses` is 78px of the band's face, `All
   currencies` 91px and `Last 6 months` 95px, so 146px holds every band label
   with ~5px to spare once the UA chevron is paid for. The 240px cap survives
   for the selects inside the Reports filter PANEL, which is a stacked form with
   a row to itself and no wrap point to protect. */
.r13 .toolbar select.btn {
  max-width: 146px;
}

/* a caret on a band control means the same thing it means on a combobox: this
   opens something */
.r13 .toolbar .btn-caret {
  font-size: 10px;
  color: var(--faint);
  margin-left: 5px;
  transition: transform 0.12s ease;
  display: inline-block;
}

.r13 .toolbar [aria-expanded="true"] .btn-caret {
  transform: rotate(180deg);
}

.r13 .search-input::placeholder {
  color: var(--faint);
}

/* Things you do to this list, as one group — never scattered, and never a bar
   of its own under the chips (owner 2026-08-06: "the exports can be grouped,
   moved higher"). The group is one component; where it RIDES is the two
   rules below it, because the two screens that carry it have different rows to
   spare. */
.r13 .listacts {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}

/* Riding inside a filter band (Reports, whose group is the single
   `Export CSV`). `margin-left: auto` makes the group wrap as a group, still
   right-aligned, when the row runs out.

   `align-self: stretch` is what makes the band ONE row rather than a row of
   things that happen to be near each other: the group takes the band's own
   height and centres its compact buttons inside it, so every child of the band
   shares a top edge and a bottom edge. Centred `sm` buttons in a 44px row
   otherwise start 4px lower than the search box beside them — invisible until
   you measure it, and then it is the reason the row never reads as a row. */
.r13 .toolbar .listacts {
  align-self: stretch;
  margin-left: auto;
}

/* The selection count is hidden at zero (`shell.js`): "0 selected" is a label
   for a mechanism the reader has not touched, and it sat in the row every
   second of every visit. */
.r13 .selcount {
  color: var(--muted);
  font-size: 13px;
  white-space: nowrap;
}

/* …and below 960px the readout keeps its NUMBER and drops its word.

   The tab strip's group is five controls now (task 13's verbs joined the two
   downloads and the export), and it must hold one row at 901px. It needed
   368px of the 359px the row leaves — and the miss is not a fixed nine
   pixels, because this chip's width is DATA: `50 selected` is wider than
   `3 selected`, so a strip tuned by shaving gaps would fit the fixture and
   wrap on a real book. The word is the part that carries no information a
   ticked list does not already show; the count is the part that does, and it
   stays. (The whole chip still leaves at 900 with the selection mechanism
   itself — the phone cards have no row checkbox.) */
@media (max-width: 960px) {
  .r13 .selcount-word {
    display: none;
  }
}

/* …and the two buttons that act on a selection are LOCKED, not hidden.

   `Download PDFs` and `Download ZIP` act on the TICKED rows, and with nothing
   ticked they were a promise the app did not keep — the action sweep pressed
   both on an untouched list and measured no download, no message, no disabled
   state (`docs/process/2026-08-06-action-sweep.md`, item 5). Two commits then
   fixed the same lie in opposite directions: `632d810` disabled the pair in
   `shell.js`, and a rule here hid it to reclaim 206px of the band's resting
   width. The owner settled it (2026-08-06): «comma-separated; the export together with the PDFs and
   the rest can live in their own group anywhere» — locked, and the group may live
   wherever it works. A capability the reader never sees is a capability they
   never learn they have; a disabled button with a title that says what to tick
   teaches it.

   So the hiding rule is gone and `shell.js` is the whole mechanism. The 206px
   are paid for by moving the group off the band entirely and onto the tab
   strip's own row (`.invoices .tabrow`), which costs no height at all. What
   the band needs now, measured at 1440: search 300 + combo 138 + status 126 +
   two 9px gaps = 582px of row, beside 334px of nav and padding. The gate's
   bisection finds no wrap at or above its 900px floor, against 954px with the
   group still in the row — and the earlier note here recorded ~1224px for the
   configuration where the pair was drawn INSIDE the band, which is the one
   thing this must not go back to. */

/* The tab strip and the export group share one 44px row: tabs at the left,
   the group riding its far end. A `role="tablist"` may only contain tabs, so
   the strip keeps its own box and this is their common parent — which is also
   what carries the rule the active tab underlines against. */
.invoices .tabrow {
  display: flex;
  align-items: center;
  gap: 16px;
  border-bottom: 1px solid var(--line);
  margin-bottom: 18px;
}

.invoices .tabrow .listacts {
  margin-left: auto;
}

/* THE LEDGER CAP — what a narrowed list adds up to (owner 2026-08-06: "what
   is shown is what is summed").

   It is the table's cap, not a paragraph above the table: it takes the table's
   own box, its own edges and — where the table is a CSS grid — its own columns,
   so the sum right-aligns on the column it sums and the scope that names it
   right-aligns against the sum. Before this it was 148px of unboxed text in six
   type styles with `THIS PAGE` starting at x=555, a coordinate no column on the
   screen shares, while the four-line currency stack spent 98px of height and
   130px of width inside a 1106px band (design critique 2026-08-06 §2.3).

   There is no `TOTAL` / `OUTSTANDING` label: the column header directly above
   the figure is already that word, and the two figures are told apart by their
   own counts — `951 invoices` against `This page · 50 invoices`. That is one
   whole type token removed rather than restyled.

   The cap is the one place on these screens where the type is allowed to be
   loud. Everything else in the band above it is deliberately quiet. */
.r13 .listtotal {
  display: flex;
  align-items: baseline;
  gap: 16px;
  margin: 0;
  padding: 9px 15px;
  font-size: 13px;
  background: var(--paper-2);
  border: 1px solid var(--line);
  border-bottom: 0;
  border-radius: var(--radius) var(--radius) 0 0;
}

/* A line of the cap is a scope and its sum, and they have to READ as a pair —
   two USD figures that both said only "USD" would be two numbers with no way to
   tell them apart. `display: contents` is how the pair survives being placed on
   the table's columns: the wrapper vanishes from layout and its two cells
   become items of the table's own grid. On a phone (below) the wrapper comes
   back as a flex row, because there are no columns there to land on. */
.r13 .listtotal .lt-line {
  display: contents;
}

.r13 .listtotal .lt-scope {
  flex: 1 1 auto;
  min-width: 0;
  text-align: right;
  /* the scope names the figure and stands right next to it; without this the
     two run together — `238 open documents166,545.77 USD` */
  padding-right: 14px;
}

.r13 .listtotal .lt-figs {
  flex: 0 0 auto;
  white-space: nowrap;
  text-align: right;
}

.r13 .listtotal .lt-count {
  color: var(--muted);
  font-size: 12.5px;
}

/* the sentence a screen adds when what it LISTS and what it SUMS differ; it
   rides on the count's own line, because it explains that count */
.r13 .listtotal .lt-excluded {
  color: var(--faint);
}

.r13 .listtotal .lt-figs b {
  font-size: 15px;
  font-weight: 780;
  color: var(--ink);
}

/* the page figure is the secondary of the two, and says so */
.r13 .listtotal .lt-page .lt-scope,
.r13 .listtotal .lt-page .lt-figs {
  font-size: 12px;
  color: var(--muted);
}

.r13 .listtotal .lt-check {
  flex: 0 0 auto;
  min-width: 0;
}

/* THE CHECK — the composition in the original currencies, one component for the
   two list caps and the Reports header. Its own rule decides how it draws: a
   run of text beside the figure while the domain keeps the note on one line
   (≤ 2 currencies), a disclosure once the domain takes it vertical. The
   disclosure changes what is VISIBLE, never what is said — opened it prints the
   same parts, in the same USD→EUR→GBP→THB order, reconciling exactly. */
.usd-check {
  font-size: 12.5px;
  color: var(--muted);
}

.usd-check > summary {
  display: flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  list-style: none;
  width: max-content;
  min-height: var(--ctl-sm);
  border-radius: 7px;
  padding: 0 7px;
  margin: 0 -7px;
}

.usd-check > summary::-webkit-details-marker {
  display: none;
}

.usd-check > summary:hover {
  background: var(--paper-3);
}

/* the caret is the affordance; it turns when the list is open */
.usd-check > summary::after {
  content: "▾";
  font-size: 10px;
  color: var(--faint);
  transition: transform 0.12s ease;
}

.usd-check[open] > summary::after {
  transform: rotate(180deg);
}

/* how many parts are inside, so the reader knows what the click costs */
.usd-check .usd-check-n {
  font-variant-numeric: tabular-nums;
  color: var(--faint);
  font-weight: 700;
}

.usd-check .usd-parts {
  line-height: 1.55;
  padding-top: 4px;
}

.usd-check.flat {
  display: inline-flex;
  align-items: baseline;
  gap: 7px;
}

/* The check is a footnote, not a heading. In the uppercase 11px/760 the other
   labels wear it read as a THIRD column caption in the row of column captions
   directly below it, and on Clients the tracking pushed it out of its own
   track. Sentence case, and the cap keeps one loud thing: the figure. */
.usd-check .usd-note-label {
  color: var(--faint);
  font-size: 11.5px;
  font-weight: 700;
}

/* the cap merges into the table below it: one box, one set of edges */
.r13 .listtotal + .table,
.r13 .listtotal + .table-scroll {
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}

.r13 .listtotal + .table .thead {
  border-radius: 0;
}

/* On an `<table>` screen the cap is a SIBLING of the table, not a row in it:
   `app.js` sorts every `<tr>` after the first and would carry a summary row
   down the list with them. So the alignment is made by matching the table's own
   geometry — the cell padding is the `<td>`'s (13px 16px), and the trailing
   track is the actions column, which is why that column now has a stated width
   below. The figure's right edge then lands exactly on the `Outstanding`
   column's, which is the whole point of the cap. */
.entity .listtotal {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto var(--rowacts-w);
  align-items: baseline;
  gap: 0;
  /* no padding on the right: the trailing track IS the actions column, so the
     cap's content has to end where the table's does, not 16px short of it */
  padding: 7px 0 7px 16px;
}

.entity .listtotal .lt-scope {
  grid-column: 1;
}

/* the 16px is the `<td>`'s own padding-right, so the figure's right edge lands
   on the `Outstanding` cells' right edge and not merely near it */
.entity .listtotal .lt-figs {
  grid-column: 2;
  padding-right: 16px;
}

/* same law as the Invoices cap: the open parts are their own row UNDER the
   figure, sharing its right edge (the 16px is `.lt-figs`' own padding) */
.entity .listtotal .lt-check {
  grid-column: 1 / 3;
  grid-row: auto;
  text-align: right;
  padding-right: 16px;
}

/* Between 901 and the fit point the rows are CARDS while the cap still wears
   the table's grid — and that grid's trailing track is the actions column,
   which a card does not have. So the cap's total sat 186px left of the column
   of money it is the total OF (round 4, found by measuring rather than by
   looking). Here the trailing track collapses and the cap pads itself like the
   card under it: 14px, its own 1px border making up the card's 15. */
@media (min-width: 901px) and (max-width: 1046px) {
  .entity .listtotal {
    grid-template-columns: minmax(0, 1fr) auto 0;
    padding-left: 14px;
  }

  .entity .listtotal .lt-figs,
  .entity .listtotal .lt-check {
    padding-right: 14px;
  }
}

/* A stated width for the actions column — the three entity tables carry the
   same two buttons and one kebab, so the column that holds them is the same
   column, and the cap above it can be aligned to something real. */
.entity .entity-table td.row-actions,
.entity .entity-table th:last-child {
  width: var(--rowacts-w);
}

/* --- the cap on a phone ---------------------------------------------------
   No columns to land on: the desktop table is gone and the rows are cards. So
   the cap stops being a grid and becomes what it always was underneath — the
   count, the figure and the check, in reading order, in the table's own box. */
@media (max-width: 900px) {
  /* The phone shell reserves the tab bar's 64px on `.workspace`, so `.main`
     carries only its own symmetric padding — 35px at the bottom against 20 at
     the top was the other half of the band under a 390 list (round 4, №14). */
  .shell .main {
    padding: 20px 16px;
  }

  /* `.listtotal.invoice-grid` and not `.invoices .listtotal`: the Invoices cap
     wears the table's grid class, and that rule is defined 800 lines further
     down at the same specificity — so the compound is what wins here, whatever
     the source order. */
  .invoices .listtotal.invoice-grid,
  .entity .listtotal {
    display: block;
    padding: 9px 13px;
    border-radius: var(--radius);
    border-bottom: 1px solid var(--line);
    margin-bottom: 10px;
  }

  /* the wrapper comes back, so each scope keeps its own figure beside it */
  .r13 .listtotal .lt-line {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
  }

  .invoices .listtotal.invoice-grid .lt-scope,
  .entity .listtotal .lt-scope {
    text-align: left;
    padding-right: 0;
  }

  .invoices .listtotal.invoice-grid .lt-check,
  .entity .listtotal .lt-check {
    padding-top: 2px;
  }

  /* …and the figure ends where every figure it caps ends. The cap kept the
     table's 16px cell padding here while the card under it uses 14 plus a 1px
     border — and the cap has a 1px border of its own — so 13 + 1 + 1 is the
     card's 14 + 1. Its total sat 15px LEFT of the money it is the total of.
     `.entity` only: the Invoices phone card pads itself 15px, not 14. */
  .entity .listtotal .lt-figs,
  .entity .listtotal .lt-check {
    padding-right: 1px;
  }
}

.r13 .chiprow {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin: 0 0 14px;
}

.r13 .chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 5px 10px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--paper-2);
  color: var(--ink);
  font-size: 12.5px;
  font-weight: 640;
}

.r13 .chip a {
  color: var(--muted);
  text-decoration: none;
  font-size: 14px;
  line-height: 1;
}

.r13 .chip a:hover {
  color: var(--ink);
}

.r13 .mlink {
  color: var(--green);
  font-weight: 730;
  text-decoration: none;
}

/* Below the tablet breakpoint the search takes its own line — a 320px field
   beside two selects leaves neither readable — and every selection control
   goes with the selection mechanism, which the phone cards do not have.
   `.bulk-menu` (the verbs, 2026-08-20) belongs to that group: visible-and-
   locked is a law about a capability the reader can reach, and where nothing
   can be ticked the trigger is furniture no press can ever unlock. */
@media (max-width: 900px) {
  .r13 .toolbar .search {
    flex: 1 1 100%;
    /* the 300px cap above is a DESKTOP resting width; without lifting it here
       the phone's full-width field silently became a 300px one */
    max-width: none;
  }

  .r13 .toolbar .listacts {
    margin-left: 0;
  }

  .r13 .bulk-dl,
  .r13 .listacts .bulk-menu,
  .r13 .selcount {
    display: none;
  }
}

/* KPI tiles.

   A tile's value line is ONE token: the figure and its ISO code never split
   across lines (owner 2026-08-04, with a screenshot of `≈ 157,327.73` sitting
   over a lonely `USD`). Three tools, in the order the owner named them —
   the padding came down first (17px → 12px of it was slack), then the R13
   26px face became a fluid step sized so `≈ 999,999.99 USD` still fits the
   narrowest four-column tile, and only a figure past a million steps down
   further (`.long`/`.xlong`, set by `_totals.html`'s `tile_money`). Nothing
   is ever clipped: a truncated digit is a wrong number. */
.home .kpis {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 12px;
  margin: 20px 0;
}

/* Home's saved-selection picker wears `.search-input`, a class the `.invoices`,
   `.reports` and `.entity` scopes each style — and `.home` did not. So it fell
   through to the UA's white `<select>`: a 222x34 white slab on the dark Home,
   the same class of gap as the country panel, the dimension table and the
   template preview dialog (final sweep, 2026-08-05). Its `margin-top` was an
   inline style and comes along. */
.home .search-input {
  width: 100%;
  margin-top: 5px;
  border: 1px solid var(--line);
  background: var(--paper);
  color: var(--ink);
  border-radius: 10px;
  padding: 9px 12px;
  font: inherit;
  font-size: 13.5px;
}

.home .tile {
  padding: 15px 12px;
  transition: 0.15s;
  text-align: center;
  text-decoration: none;
  color: inherit;
  display: block;
}

.home a.tile:hover {
  transform: translateY(-1px);
  border-color: var(--line-hover);
}

.home .tile .label {
  font-size: 12.5px;
  color: var(--muted);
  font-weight: 740;
}

.home .tile .value {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  --tile-fs: 26px;
  font-size: var(--tile-fs);
  white-space: nowrap;
  font-weight: 840;
  letter-spacing: -0.045em;
  margin-top: 7px;
  font-variant-numeric: tabular-nums;
}

/* Past a million the number itself is longer than the tile was designed for,
   so the face steps down rather than the code leaving the line. Shared by
   every `.r13` tile family, keyed off whatever `--tile-fs` that family set. */
.r13 .tile .value.long {
  font-size: calc(var(--tile-fs) * 0.84);
}

.r13 .tile .value.xlong {
  font-size: calc(var(--tile-fs) * 0.72);
}

/* Four columns — the tight band. At 1181px a tile holds 180px of content and
   `≈ 999,999.99 USD` needs 174px of it at this step; 26px returns at ~1576px,
   where the tile is wide enough for it again. */
@media (min-width: 1181px) {
  .home .tile .value {
    --tile-fs: clamp(19px, 1.65vw, 26px);
  }
}

/* Two narrow columns on a phone: the same rule, a steeper slope. */
@media (max-width: 620px) {
  .home .tile .value {
    --tile-fs: clamp(13px, 4.1vw, 26px);
  }
}

.home .tile .meta {
  font-size: 12px;
  color: var(--faint);
  margin-top: 4px;
}

/* Overdue with something in it — colour is never the only carrier, the tile's
   own meta line says how old the oldest one is. */
.home .tile.alert .value {
  color: var(--red);
}

.home .tile.alert .meta {
  color: var(--red);
}

/* Create / chart / journal stack */
.home .home-stack {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 16px;
  margin-top: 18px;
}

.home .create-card {
  border-color: var(--green-rim);
  background: linear-gradient(145deg, var(--paper), var(--green-softer));
}

.home .create-content-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(340px, 0.85fr);
  gap: 20px;
  align-items: stretch;
}

.home .switch-tabs,
.home .activity-tabs {
  display: flex;
  align-items: center;
  gap: 4px;
}

.home .switch-tabs {
  padding: 4px;
  border: 1px solid var(--line);
  background: var(--paper-2);
  border-radius: 11px;
  width: max-content;
}

.home .switch-tabs button,
.home .activity-tabs button {
  border: 0;
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-weight: 730;
  padding: 8px 11px;
  border-radius: 8px;
  cursor: pointer;
}

.home .switch-tabs button.active,
.home .activity-tabs button.active {
  background: var(--paper);
  color: var(--ink);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
}

/* The Create switch is an accented control, not a quiet one.

   Owner, 2026-08-04: "on Home, Multiple invoices must not be drawn so pale,
   it is an active, important button; green, probably". The prototype draws this
   switch exactly like the journal's tabs — muted grey on a near-white track —
   and the owner is overruling the prototype here: this pair is how invoices
   get created, and it was reading as a caption. The ruling reaches only *this*
   switch; the journal tabs below stay quiet on purpose, or the accent would
   mean nothing.

   Both states stay unmistakable: the chosen pane is a solid green pill, the
   other is green *text* on the track — visibly the same control, visibly not
   selected. Nothing depends on colour alone, `aria-selected` carries the state
   for anyone who cannot see it. */
.home .create-card .switch-tabs {
  background: var(--paper);
  border-color: var(--green-rim);
}

.home .create-card .switch-tabs button {
  color: var(--green);
}

.home .create-card .switch-tabs button:hover {
  background: var(--green-soft);
}

.home .create-card .switch-tabs button.active {
  background: var(--green);
  color: var(--on-green);
  box-shadow: 0 1px 4px rgba(8, 120, 90, 0.3);
}

html[data-theme="dark"] .home .create-card .switch-tabs {
  border-color: var(--green);
}

html[data-theme="dark"] .home .create-card .switch-tabs button.active {
  /* dark's green is a light one — ink reads on it, white does not */
  color: #0f1d19;
}

/* Both panes share one grid cell, so the card keeps the taller pane's height
   and switching One/Multiple never shifts the layout below (owner feedback
   2026-08-02). visibility (not display) also drops the inactive pane from
   the tab order and the accessibility tree. */
/* C17a: the left column spreads to the panel's height — text at the top,
   the buttons at the bottom — so the card carries no dead gap under them */
.home .create-content-grid > div:first-child {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}

.home .create-card .card-body {
  display: grid;
}

.home .create-pane {
  grid-area: 1 / 1;
  visibility: hidden;
  padding-top: 17px;
}

.home .create-pane.active {
  visibility: visible;
}

.home .field {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.home .field label {
  font-size: 12px;
  font-weight: 760;
  color: var(--muted);
  display: block;
  margin: 0;
}

.home .field input,
.home .field select {
  width: 100%;
  border: 1px solid var(--line);
  background: var(--paper);
  color: var(--ink);
  border-radius: 10px;
  padding: 10px 12px;
  font: inherit;
  outline: none;
}

/* ONE focus indicator, app-wide. Three lived here before: this green one (on
   Home only), legacy style.css's lavender `--focus: #c9c5ff` (1.63:1 against
   white — not an indicator) on every dialog field, and Chrome's default blue
   `rgb(0,95,204)` on every button, which is the alien rectangle in the corner
   of the very first frame of every modal. The ring is now the product's own
   green at 3:1+, and it reaches buttons too via :focus-visible. */
.home .field input:focus,
.home .field select:focus,
.r13 .field input:focus,
.r13 .field select:focus,
.r13 .field textarea:focus,
.composer-dialog input:focus,
.composer-dialog select:focus,
.composer-dialog textarea:focus,
.composer-dialog .combo-input:focus,
.entity-dialog input:focus,
.entity-dialog select:focus,
.entity-dialog textarea:focus,
.entity-dialog .combo-input:focus,
.review-dialog input:focus,
.review-dialog select:focus,
.review-dialog textarea:focus,
.review-dialog .combo-input:focus,
.entity dialog input:focus,
.entity dialog select:focus,
.entity dialog textarea:focus,
/* …and the filter band's own controls, which had been left out: a focused
   `select.btn` on Invoices and in the Reports panel painted legacy style.css's
   lavender `--focus: #c9c5ff` (1.63:1), and the search box on Invoices and
   Reports painted nothing at all — three answers for one control. */
.r13 .search-input:focus,
.r13 select.btn:focus,
.r13 .fpanel input.btn:focus,
.home .search-input:focus {
  outline: none;
  border-color: var(--green);
  box-shadow: 0 0 0 3px rgba(8, 120, 90, 0.3);
}

/* A native checkbox is not a bordered text field: the rule above turns its
   focus ring OFF and leans on a border-color a checkbox does not paint,
   leaving only the faint halo. Since the matrix and list rows made their
   checkbox the tab stop (the a11y rework), that checkbox carries the app's
   one focus indicator itself (2026-08-21). */
.review-dialog input[type="checkbox"]:focus-visible,
.composer-dialog input[type="checkbox"]:focus-visible,
.entity-dialog input[type="checkbox"]:focus-visible,
.r13 input[name="ids"]:focus-visible {
  outline: 2px solid var(--green);
  outline-offset: 2px;
  box-shadow: none;
}

html[data-theme="dark"] .home .field input:focus,
html[data-theme="dark"] .home .field select:focus,
html[data-theme="dark"] .r13 .field input:focus,
html[data-theme="dark"] .r13 .field select:focus,
html[data-theme="dark"] .composer-dialog input:focus,
html[data-theme="dark"] .composer-dialog select:focus,
html[data-theme="dark"] .composer-dialog .combo-input:focus,
html[data-theme="dark"] .review-dialog input:focus,
html[data-theme="dark"] .review-dialog select:focus,
html[data-theme="dark"] .review-dialog .combo-input:focus,
html[data-theme="dark"] .entity dialog input:focus,
html[data-theme="dark"] .entity dialog select:focus,
html[data-theme="dark"] .entity dialog textarea:focus,
html[data-theme="dark"] .r13 .search-input:focus,
html[data-theme="dark"] .r13 select.btn:focus,
html[data-theme="dark"] .r13 .fpanel input.btn:focus,
html[data-theme="dark"] .home .search-input:focus,
html[data-theme="dark"] .selection-header .sel-title:focus {
  box-shadow: 0 0 0 3px rgba(72, 189, 149, 0.35);
}

/* Buttons and disclosure summaries: no control may fall back to the UA ring. */
.r13 .btn:focus-visible,
.entity .btn:focus-visible,
.entity .btn-primary:focus-visible,
.entity .modal-close:focus-visible,
.r13 summary:focus-visible,
.entity summary:focus-visible,
.r13 .linkbtn:focus-visible,
.entity .linkbtn:focus-visible,
/* …and the two controls this change added to that list (R3-10c). `Update`
   on the rates stamp had only a muted→ink colour shift to mark focus, and
   the `?` mark opens its note on focus while showing nothing about WHERE
   the focus is. Both are reachable by Tab, so both get the same ring as
   every other control: no control may fall back to the UA ring. */
.hintlink:focus-visible,
.lt-hint:focus-visible {
  outline: 2px solid var(--green);
  outline-offset: 2px;
}

.home .product-summary {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 13px;
  align-items: center;
  border: 1px solid var(--line);
  background: var(--paper);
  border-radius: 12px;
  padding: 18px;
  min-height: 128px;
}

.home .product-summary .meta {
  font-size: 12px;
  color: var(--faint);
  margin-top: 4px;
}

/* The legacy dashboard's `.card b { display: block; font-size: var(--fs-xl) }`
   (style.css) must never reach an R13 card: in the prototype a bare <b> is
   body-size and inline — the leak broke "Issued by <company>." mid-sentence
   and oversized the prefill heading. Sits before the mini-metric/cash-summary
   rules so their explicit sizes still win. */
.home .card b {
  display: inline;
  font-size: inherit;
}

.home .saved-summary {
  border: 1px solid var(--line);
  background: var(--paper);
  border-radius: 13px;
  padding: 14px;
}

.home .selection-insight {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 10px;
  align-content: stretch;
}

.home .mini-metric {
  background: var(--paper-2);
  border-radius: 9px;
  padding: 9px;
  min-height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.home a.mini-metric {
  text-decoration: none;
  color: inherit;
  transition: 0.15s;
}

.home a.mini-metric:hover {
  background: var(--paper);
  box-shadow: inset 0 0 0 1px var(--line);
}

.home .mini-metric b {
  display: block;
  font-size: 14px;
}

.home .mini-metric span {
  font-size: 11.5px;
  color: var(--faint);
}

/* chart */
.home .chart-head,
.reports .chart-head {
  display: flex;
  align-items: center;
  gap: 12px;
}

.home .cash-summary {
  display: flex;
  align-items: center;
  gap: 20px;
}

.home .cash-summary > div {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
}

.home .cash-summary span {
  color: var(--faint);
  font-size: 11.5px;
  /* a series label is one token: wrapped, it pushed its own figure out of
     line with `Issued` beside it (owner 2026-08-04) */
  white-space: nowrap;
}

.home .cash-summary b {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 14px;
  font-variant-numeric: tabular-nums;
  /* the figure and its code are one token here too */
  white-space: nowrap;
}

.home .chart,
.reports .chart {
  height: 270px;
  display: flex;
  align-items: flex-end;
  gap: 12px;
  padding: 23px 6px 25px;
  position: relative;
}

.home .chart:before,
.reports .chart:before {
  content: "";
  position: absolute;
  inset: 25px 0 25px;
  background: repeating-linear-gradient(
    to bottom,
    transparent 0,
    transparent 39px,
    var(--line-2) 40px
  );
}

.home .monthcol,
.reports .monthcol {
  flex: 1;
  min-width: 0;
  max-width: 76px;
  height: 100%;
  display: flex;
  border: 0;
  background: transparent;
  padding: 0;
  align-items: flex-end;
  justify-content: center;
  gap: 4px;
  position: relative;
  text-decoration: none;
}

.home .bar,
.reports .bar {
  width: 18px;
  border-radius: 5px 5px 0 0;
  position: relative;
  z-index: 1;
  transition: 0.15s;
}

/* A hovered bar carries its own tooltip, so it has to outrank its neighbours.

   Owner, 2026-08-04, "zorder crooked", with a screenshot of December's tip
   reading `11,161.80 US…` because January's bars painted over its right edge.
   The cause is one line above: `z-index: 1` makes every bar its own stacking
   context, so the tip's `--z-popover` only ever ranked it *inside* that bar,
   and a later sibling won on document order. Raising the hovered bar itself is
   what the tip needs — the whole subtree comes with it. */
.home .bar:hover,
.reports .bar:hover {
  filter: brightness(0.93);
  z-index: var(--z-popover);
}

.home .bar.issued,
.reports .bar.issued {
  background: var(--bar-issued);
}

.home .bar.collected,
.reports .bar.collected {
  background: var(--green);
}

.home .bar .tip,
.reports .bar .tip {
  display: none;
  position: absolute;
  bottom: calc(100% + 8px);
  left: 50%;
  transform: translateX(-50%);
  background: var(--tooltip-bg);
  color: var(--tooltip-ink);
  padding: 7px 9px;
  border-radius: 8px;
  font-size: 11px;
  white-space: nowrap;
  z-index: var(--z-popover);
}

/* At the ends of the year the centred tip would reach past the card, and the
   shell clips what leaves it — so the outermost columns anchor their tips to
   the inside edge instead of the middle. `:nth-child` counts the six months a
   phone hides too, hence the pairs. */
.home .monthcol:first-child .tip,
.reports .monthcol:first-child .tip,
.home .monthcol:nth-child(7) .tip,
.reports .monthcol:nth-child(7) .tip {
  left: 0;
  transform: none;
}

.home .monthcol:last-child .tip,
.reports .monthcol:last-child .tip {
  left: auto;
  right: 0;
  transform: none;
}

.home .bar:hover .tip,
.reports .bar:hover .tip {
  display: block;
}

.home .monthlabel,
.reports .monthlabel {
  position: absolute;
  bottom: -19px;
  font-size: 11px;
  color: var(--faint);
}

/* activity journal */
.home .activity-tabs {
  border-bottom: 1px solid var(--line-2);
  padding: 0 14px;
}

.home .activity-tabs button {
  border-radius: 8px 8px 0 0;
  padding: 11px 10px;
}

.home .activity-feed {
  padding: 0 17px;
}

.home .activity-row {
  display: grid;
  /* no icon track since round 1 C17c — the decorative glyph left the row,
     the text takes the space, the action link right-aligns at every width */
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 10px;
  align-items: center;
  padding: 12px 0;
  border-bottom: 1px solid var(--line-2);
}

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

/* the tab filter hides rows with [hidden]; an explicit rule is needed because
   the display above would otherwise win over the UA default */
.home .activity-row[hidden],
.home .empty[hidden] {
  display: none;
}

.home .activity-icon {
  width: 29px;
  height: 29px;
  border-radius: 9px;
  background: var(--paper-3);
  display: grid;
  place-items: center;
  color: var(--muted);
}

.home .activity-row .title {
  font-size: 13.5px;
  font-weight: 700;
}

.home .activity-row .meta {
  font-size: 11.8px;
  color: var(--faint);
  margin-top: 2px;
}

/* empty + error states (a section never renders blank) */
.r13 .empty {
  color: var(--muted);
  font-size: 13px;
  padding: 18px 0;
  text-align: center;
}

.reports .error-card,
.home .error-card {
  border: 1px solid var(--red);
  background: var(--red-soft);
  color: var(--red);
  border-radius: var(--radius);
  padding: 16px 18px;
  margin: 16px 0;
}

/* banners inside the themed Home surface */
.home .anon-banner,
.home .update-banner,
.home .banner {
  background: var(--green-soft);
  color: var(--green);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  padding: 11px 15px;
  margin: 0 0 16px;
  font-size: 13px;
}

.home .anon-banner a,
.home .update-banner a,
.home .banner a {
  color: var(--green);
  font-weight: 700;
}

.home .ok,
.home .error {
  border-radius: var(--radius-sm);
  padding: 11px 15px;
  margin: 0 0 16px;
  font-size: 13px;
}

.home .error {
  background: var(--red-soft);
  color: var(--red);
}

/* --- Invoices (All / Drafts / Runs) --------------------------------------
   Ported from the R13 prototype: goldens 1440-invoices-r13.png and
   1440-runs-after-issue-r13.png. Numbers are right-aligned in header cells as
   well as body cells (behavior standard §0); below 900 px the table becomes
   cards (§5). */

/* Every R13 page head owns the same gap, whatever follows it. `.invoices` was
   the only scope that had one, which is why the entity screens' explanatory
   `.sub` ran straight into the search box (owner 2026-08-06: "the explainer
   stuck to the field") — the band's own 18px collapses with this, so the gap is
   18px either way, and stays 18px on a screen whose band is hidden. */
.r13 .pagehead {
  margin-bottom: 18px;
}

/* The rule under the strip and the space below it belong to `.tabrow` now, so
   they run the full width of the page rather than stopping where the tabs do —
   the export group sits on the same line and would otherwise hang off the end
   of the line it is aligned to. `margin: 0` is not tidying: legacy `style.css`
   sets `.tabs { margin: 0 0 var(--sp-3) }`, and inside a flex row that margin
   no longer collapses with anything — it simply made the strip 12px taller
   than its own controls and pushed the first invoice row down with it. */
.invoices .tabs {
  display: flex;
  gap: 4px;
  margin: 0;
}

/* the legacy `.tab` pill from style.css is fully overridden here. A tab is an
   interactive control and takes a control's height: it was 45px, the sum of its
   own padding, and so a seventh value in a scale of two. */
.invoices .tab {
  min-height: var(--ctl);
  padding: 0 14px;
  background: none;
  border: 0;
  border-bottom: 2px solid transparent;
  border-radius: 0;
  margin-bottom: -1px;
  color: var(--muted);
  font-weight: 760;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: 7px;
}

.invoices .tab:hover {
  background: none;
  color: var(--ink);
}

.invoices .tab.on {
  background: none;
  color: var(--green);
  border-color: transparent;
  border-bottom-color: var(--green);
}

.invoices .badge.count {
  padding: 2px 7px;
  font-size: 11px;
}

/* The toolbar, the chips, the selection count and the list actions are the
   shared `.r13` filter band now — one definition, five screens. */

.invoices .table {
  /* No overflow:hidden here: the row menu (kebab) is an absolutely positioned
     popover and the last rows' menus were cut at the table's bottom edge.
     The rounded corners are painted by the thead / last row instead. */
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--paper);
}

.invoices .invoice-grid {
  display: grid;
  grid-template-columns: 34px 96px minmax(140px, 1.3fr) minmax(150px, 1.5fr)
    150px 96px 170px;
  align-items: center;
  gap: 12px;
  padding: 11px 15px;
}

/* The cap wears `invoice-grid` (`_invoices_all.html`), so it inherits the
   table's tracks from the rule above and the two can never drift apart. Only
   the placement is stated here: the scope spans the three identifying columns
   and right-aligns against the figure, the figure lands IN the `Total` column,
   and the check takes `Status` + `Actions` — the two columns that sum to
   nothing, which is exactly why the check can live there. */
/* the cap is a summary, not a row: it takes less vertical padding than the
   63px data rows below it, because nothing in it has to be clickable */
.invoices .listtotal {
  padding: 7px 15px;
  /* the table's 12px gap is a COLUMN gap here; two lines of a summary are one
     block, not two rows */
  row-gap: 1px;
}

.invoices .listtotal .lt-scope {
  grid-column: 1 / 5;
}

.invoices .listtotal .lt-figs {
  grid-column: 5;
  text-align: right;
}

/* The OPEN breakdown is a block, not a neighbour (owner round 2, defect 3):
   parked in the trailing columns it collided with the headline at ~1300 and
   landed wherever auto-placement dropped it below 1280 — and as an extra grid
   item it shoved the page line off the money column (his 26px). Its own full
   row under the money lines, ending on the Total column's right line, in
   flow: content is pushed down, never overlaid, at every width. */
.invoices .listtotal .lt-check {
  grid-column: 1 / 6;
  grid-row: auto;
  text-align: right;
}

.invoices .thead {
  background: var(--paper-2);
  border-bottom: 1px solid var(--line);
  border-radius: calc(var(--radius) - 1px) calc(var(--radius) - 1px) 0 0;
  font-size: 11.5px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--faint);
  font-weight: 800;
}

.invoices .thead .num,
.invoices .trow .num {
  text-align: right;
}

/* Money never wraps (owner round 2, defect 2: 50 of 50 figures wrapped at
   901–1023). nowrap turns a too-narrow column into a LOUD overflow the sweep
   sees, instead of a silent second line; the width itself comes from the
   track floors below. */
.invoices .trow .num,
.invoices .listtotal .lt-figs {
  white-space: nowrap;
}

.invoices .trow {
  border-bottom: 1px solid var(--line-2);
  cursor: pointer;
}

.invoices .trow:last-child {
  border-bottom: 0;
  border-radius: 0 0 calc(var(--radius) - 1px) calc(var(--radius) - 1px);
}

.invoices .trow:hover {
  background: var(--paper-2);
}

.invoices .trow.selected {
  background: var(--green-softer);
}

.invoices .trow.voided {
  opacity: 0.62;
}

.invoices .trow.voided .mono {
  text-decoration: line-through;
}

.invoices .trow.flash,
.invoices .mobile-card.flash,
.invoices .run-block.flash {
  /* The mark FADES on its own (owner review round 1, C13 — the
     Clear-highlight control was the wrong call): a slow decay, generous
     enough to be found after the landing scroll settles. What survives from
     the 2026-08-06 pass is the other half — the server opens the page the
     row is on and the row is scrolled into view, so the fade happens in
     front of the reader, not off-screen.

     Row-specific rather than `.invoices .flash`, so the mark outranks
     `.trow:hover` while it lasts. */
  animation: new-mark-fade 6.5s ease-out forwards;
}

@keyframes new-mark-fade {
  0%, 55% { background: var(--green-soft); }
  100% { background: transparent; }
}

.invoices .mono {
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}

.invoices .invoice-grid .mono {
  /* a fixed track: an unusually wide number ellipsizes instead of colliding */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.invoices .cell-stack {
  min-width: 0;
}

.invoices .cell-stack b,
.invoices .cell-stack span {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.invoices .meta {
  font-size: 11.5px;
  color: var(--faint);
}

/* Every row cell that stacks reserves its second line, even when it has
   nothing to put there (owner round 2b, item 2). The date moved under the
   number, so the number cell is now always two lines while the client's payer
   line is optional — and with `align-items: center` that made the row's
   rhythm depend on whether a document HAS a payer: the client name sat 9px
   below the number on every row without one, and level with it on the rest,
   in the same table. */
.invoices .invoice-grid .cell-stack .meta {
  min-height: 1.35em;
}

/* Only a *cell's* meta is a one-line token that ellipsises to keep the grid's
   columns. A helper sentence in a dialog is prose and wraps — the blanket rule
   used to cut `Numbering and closed periods follow the issuer.` mid-word with
   no way to read the rest (`scripts/layout_check.py`, `truncated`). */
.invoices .cell-stack .meta,
.invoices .mobile-card .top .meta,
.invoices .trow .meta {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.invoices .rowacts {
  display: flex;
  align-items: center;
  gap: 6px;
  justify-content: flex-end;
}

.invoices .badge.ok {
  background: var(--green-soft);
  color: var(--green-dark);
}

.invoices .badge.blue {
  background: var(--blue-soft);
  color: var(--blue);
}

.invoices .badge.bad {
  background: var(--red-soft);
  color: var(--red);
}

/* Row menu (kebab).

   Was scoped to `.invoices`, so the same component rendered UNSTYLED the moment
   the entity lists adopted it (design critique P1's own warning). It is the
   app's one row-overflow control now — `.r13` is the scope every authenticated
   screen carries. */

.r13 .rowmenu {
  position: relative;
}

/* The kebab is a row action, so it is a row action's box: the compact step,
   like the `View` / `PDF` / `Edit` buttons it sits beside. It was 29px — the
   height its padding and its glyph happened to add up to — which put a third
   size in every row of the three biggest tables. */
.r13 .rowmenu > summary {
  list-style: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--ctl-sm);
  /* …and the WIDTH too, because the mark is a glyph: this sheet's own icon
     button is square with the compact step for its side. Stating only the
     height let the width fall out of a 3px glyph + 7px padding + 1px border =
     19px, which made the kebab the one row control off the scale the
     web-shell spec puts it on (owner round 4: "standard kebab width"). The
     touch block at the end of this file has always stated BOTH dimensions for
     this same trigger; this is the mouse half of that sentence. */
  min-width: var(--ctl-sm);
  padding: 0 7px;
  border-radius: 8px;
  color: var(--muted);
  /* legacy style.css gives this summary `1px solid var(--border)`, and that
     token is a fixed light grey in BOTH palettes — so the kebab wore a pale
     rim in dark while the `View`/`Edit` buttons beside it tracked the theme
     (owner round 2b, found beside item 8) */
  border: 1px solid var(--line);
}

.r13 .rowmenu > summary::-webkit-details-marker {
  display: none;
}

/* The dots are DRAWN, not typed (owner round 2b, item 8, reopened: "the text
   still sits low"). The button's box centres its mark to 0.000px — which is
   why an acceptance argued from box geometry passed while the owner kept
   seeing it — but `⋮` carries the offset inside its own font metrics: the
   glyph's ink sits 1.26px below the middle of its em box, and no rule on the
   box can move ink. Three drawn dots have no metrics to inherit, and they
   look the same in every face the user's system may resolve. */
.r13 .rowmenu > summary .kebab {
  width: 3px;
  height: 15px;
  background:
    radial-gradient(circle 1.5px at 50% 1.5px, currentColor 100%, transparent 0),
    radial-gradient(circle 1.5px at 50% 7.5px, currentColor 100%, transparent 0),
    radial-gradient(circle 1.5px at 50% 13.5px, currentColor 100%, transparent 0);
}

/* Hover and open are one state pair and both belong here. The legacy sheet
   still owns this component's two states (`style.css` `details.rowmenu >
   summary:hover` and `[open]`) and paints them from `--fg`/`--panel` — fixed
   light-theme values in BOTH palettes, the same leak the pale rim above
   records, one state over. Measured in dark: the hovered dots were 1.01:1
   against their own background, and an open kebab was a near-white pill
   between two dark buttons (owner round 3, item 3). */
.r13 .rowmenu > summary:hover,
.r13 .rowmenu[open] > summary {
  background: var(--paper-3);
  color: var(--ink);
}

.r13 .rowmenu .menu-list {
  position: absolute;
  right: 0;
  top: 100%;
  z-index: var(--z-popover);
  min-width: 172px;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow);
  padding: 6px;
  display: flex;
  flex-direction: column;
  text-align: left;
  /* the panel states the vocabulary its items state, so `.invoices`' own 14px
     cannot reach anything inside it */
  font-size: 13px;
}

/* one size for a menu item, stated rather than inherited: `font: inherit` gave
   the same item 14px inside `.invoices` and 13px inside `.entity`, so the same
   `Delete…` was 37.7px on Invoices and 36.1px on Clients */
.r13 .rowmenu .menu-list a,
.r13 .rowmenu .menu-list .linkbtn {
  background: none;
  border: 0;
  font: inherit;
  font-size: 13px;
  color: var(--ink);
  text-align: left;
  min-height: var(--ctl-sm);
  padding: 8px 10px;
  border-radius: 8px;
  cursor: pointer;
  text-decoration: none;
}

.r13 .rowmenu .menu-list a:hover,
.r13 .rowmenu .menu-list .linkbtn:hover {
  background: var(--green-soft);
}

.r13 .rowmenu .menu-list .linkbtn.danger {
  color: var(--red);
}

/* The selection's three verbs, behind one trigger (owner ruling 2026-08-20).
   It is the row menu's popover rather than a second vocabulary; only the
   TRIGGER differs, because this one carries a word where a row's carries a
   glyph — so the square icon-button sizing above is released and the label
   gets the same weight the strip's other controls wear.

   Measured, not preferred: the three verbs inline beside the two downloads
   needed 489px of a 427px strip at 901 and wrapped onto three lines. */
.r13 .listacts .bulk-menu > summary {
  min-width: 0;
  gap: 4px;
  padding: 0 10px;
  font-weight: 730;
  color: var(--ink);
}

/* Locked, not hidden — the same state the two downloads wear with nothing
   ticked, and the same look: a control that says what it is and why it is not
   ready yet. `<details>` has no `disabled`, so this pairs with the summary's
   `aria-disabled` and `shell.js`'s refusal to open it. */
.r13 .listacts .bulk-menu > summary[aria-disabled="true"] {
  opacity: 0.45;
  cursor: default;
}

.r13 .listacts .bulk-menu > summary[aria-disabled="true"]:hover {
  background: none;
  color: var(--ink);
}

/* drafts + runs */

.invoices .draft-list,
.invoices .run-block {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--paper);
  overflow: hidden;
  margin-bottom: 12px;
}

.invoices .run-header {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 13px 15px;
}

/* Money never wraps — the law the invoice rows got in round 2 (defect 2),
   applied where the same class was hiding: `.num` is right-aligned only
   inside `.thead`/`.trow`, so the run total sat on a button whose UA default
   is CENTRED, and at 960 it broke into "420,000.00" over "USD", each line
   centred on its own. Found by the breakdown probe: the panel's right edge
   missed the total's by 3px, and the 3px was this wrap. */
.invoices .run-header .num {
  white-space: nowrap;
  text-align: right;
}

.invoices .draft-list .run-header + .run-header {
  border-top: 1px solid var(--line-2);
}

.invoices .run-name {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.invoices .rename-btn {
  border: 0;
  background: none;
  color: var(--faint);
  cursor: pointer;
  font: inherit;
  padding: 0 2px;
}

.invoices .rename-btn:hover {
  color: var(--green);
}

.invoices .rename-input {
  border: 1px solid var(--line);
  background: var(--paper);
  color: var(--ink);
  border-radius: 8px;
  padding: 5px 8px;
  font: inherit;
  width: 240px;
  max-width: 100%;
}

.invoices .run-note {
  padding: 0 15px 6px;
  font-size: 12px;
  color: var(--muted);
}

/* The same law the list cap got (owner round 2, defect 6; round 2b re-raised
   it for this family): the open parts are a compact block right-aligned UNDER
   the total, right edges matching. The block already spans the card, and its
   own 15px padding-right is the 15px `.run-header` pays — so right-aligning
   the TEXT lands it exactly on the toggle's right edge. Keyed on the
   attribute, not the class: the sibling `.run-note` that states voided
   invoices is a sentence and keeps reading from the left. */
.invoices .run-note[data-bd-panel] {
  text-align: right;
}

.invoices .run-actions {
  padding: 11px 15px;
  border-top: 1px solid var(--line-2);
  background: var(--paper-2);
  gap: 8px;
}

.invoices .pagination {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: 16px;
  color: var(--muted);
}

.invoices .mobile-cards {
  display: none;
}

.invoices .mobile-card {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--paper);
  padding: 13px 15px;
  margin-bottom: 10px;
}

.invoices .mobile-card .top {
  display: flex;
  align-items: flex-start;
  gap: 10px;
}

/* The identity block shrinks (ellipsis), the money never does: without
   min-width:0 a long product · run meta pushed the total off the card. */
.invoices .mobile-card .top > div:first-child {
  min-width: 0;
}

.invoices .mobile-card .top > div:first-child > b {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.invoices .mobile-card .top > .num {
  flex: none;
  white-space: nowrap;
}

/* The card already carried `.voided` but nothing painted it: a voided document
   read as a normal one apart from its badge, while the desktop row is dimmed
   with its number struck. Same treatment — applied to the card's *content*,
   not the card, so the action menu it now hosts (Unvoid lives in there) is
   never rendered at 62% through an inherited opacity. */
.invoices .mobile-card.voided .top,
.invoices .mobile-card.voided .cardacts .meta {
  opacity: 0.62;
}

.invoices .mobile-card.voided .top > div:first-child > b {
  text-decoration: line-through;
}

/* F-08 (owner ruling 2026-08-04): a phone card carries the desktop row's whole
   action set — the extras collected behind the same kebab, not a second
   pattern. The action strip wraps so the badge + due meta never squeeze the
   controls off a 390 px card, and the trigger is drawn as a button so it reads
   as the third control beside View / PDF instead of a stray glyph. */
.invoices .mobile-card .cardacts {
  flex-wrap: wrap;
  row-gap: 8px;
}

/* The three controls are one wrapping unit pushed right by an auto margin:
   at 390 px the status badge + due date leave too little room, and without
   the group the kebab alone dropped to a second line, left-aligned. */
.invoices .mobile-card .cardacts .cardacts-btns {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-left: auto;
}

/* ONE kebab, one panel, one item — on every screen and at every width (owner
   round 3, item 9: «the same narrow one on every screen, on the right too…
   the dropdown menu styles must be one as well»). The phone card used to
   patch its own 40×32/radius-10 trigger and a 196px panel with 11px 12px
   items, so the invoice card was a third dialect beside the table and the
   entity card — and the comfortable thumb target it was reaching for now
   comes honestly from the coarse-pointer step below (44px), not from a
   padding that only one screen and only its buttons ever saw. */

/* F-09: the 901–1279px band. The 1440 grid needs ~938px while the content
   region holds ~636–982px here, so the desktop table clipped its right side —
   Actions (the only path to mark paid / void / duplicate) from ~1230px down,
   Total and Status too by 901px. Two tightening tiers keep every column,
   ellipsis absorbing the squeeze in the two name columns. */
@media (max-width: 1279px) {
  .invoices .invoice-grid {
    /* The Total floor is the widest figure the column can print, and that is
       NOT a row figure: the cap's grand total is bold and a step larger, so
       "2,717,674.60 USD" measures 145px against a row's 135px. At the round-2
       floor of 137 it ate the whole 7px gap up to the Status column at
       1000–1024 (found by the breakdown probe once it started measuring real
       text rects). The squeeze is paid by the two NAME columns, whose
       ellipsis absorbs it; money never pays. */
    grid-template-columns: 32px 90px minmax(140px, 1.25fr) minmax(110px, 1fr)
      minmax(146px, 0.55fr) 90px 148px;
    gap: 10px;
    padding: 11px 12px;
  }
}

@media (max-width: 1080px) {
  .invoices .invoice-grid {
    grid-template-columns: 26px 78px minmax(84px, 1.2fr) minmax(64px, 1fr)
      minmax(146px, 0.55fr) 74px 132px;
    gap: 7px;
    padding: 10px;
  }

  .invoices .rowacts {
    gap: 4px;
  }
}

/* phones: matrices become cards (behavior standard §5) */
@media (max-width: 900px) {
  .invoices .table.desktop {
    display: none;
  }

  .invoices .mobile-cards {
    display: block;
  }

  /* (the bulk-download hide and the full-width search moved to the shared
     `.r13` band, where every list screen gets them) */
}

@media (max-width: 620px) {
  .invoices .pagehead {
    flex-wrap: wrap;
  }

  .invoices .pagehead > div:first-child {
    flex: 1 1 100%;
  }

  .invoices .pagehead .spacer {
    display: none;
  }

  .invoices .run-header,
  .invoices .run-actions {
    flex-wrap: wrap;
  }
}


/* --- Single-invoice composer ---------------------------------------------
   Goldens 1440-single-composer-r13.png / 1440-single-preview-r13.png.
   Spacing per behavior standard §1: header/footer 20–24 px, body 24–28 px,
   field gap 16–20 px; only the body scrolls, the footer is sticky. */

dialog.composer-dialog,
dialog.review-dialog,
dialog.preview-dialog,
dialog.entity-dialog {
  border: 0;
  padding: 0;
  background: var(--paper);
  color: var(--ink);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  max-height: min(92dvh, 960px);
  overflow: visible;
}

dialog.composer-dialog {
  width: min(1180px, calc(100vw - 40px));
  z-index: var(--z-modal);
}

dialog.preview-dialog {
  width: min(760px, calc(100vw - 40px));
  z-index: var(--z-modal-high);
}

/* The prototype's `.modal.narrow`, 760 — not the legacy 620 the entity editors
   inherited: the product editor's line table needs the room. */
dialog.entity-dialog {
  width: min(760px, calc(100vw - 40px));
  z-index: var(--z-modal);
}

/* Declared HERE, with its siblings, deliberately: the ≤700 phone band later
   in this sheet turns every dialog into a full-bleed sheet (`width: 100vw;
   margin: 0`), and a width rule that sits AFTER that band re-narrows the
   sheet while the band's `margin: 0` survives — a left-anchored card with a
   dead strip of page on the right, which the owner read as a clipped popup
   (M1 accept round 5). Order is the mechanism; do not move this below the
   phone band. */
dialog.review-dialog {
  width: min(1320px, calc(100vw - 40px));
  z-index: var(--z-modal);
}

/* One backdrop for every dialog in the app, tuned per theme. The legacy
   `rgba(20,19,15,0.35)` left the dark entity dialogs at 1.16:1 against the page
   behind them — a modal with no visible edge, which undermines the one promise
   a modal makes. */
dialog::backdrop {
  background: var(--backdrop);
}

/* Templates' `Preview full size` dialog lives in `base.html`, so it never
   joined the themed set above and fell back to the UA's white `Canvas`: a
   600x780 white slab with black chrome in the middle of a dark app (measured
   2026-08-05, the fourth of this class after the cards, the country panel and
   the dimension table). The DIALOG is host chrome and follows the theme; the
   DOCUMENT inside it stays white, because it is paper — only the desk it sits
   on is themed. */
dialog.tpl-preview-dialog {
  background: var(--paper);
  color: var(--ink);
  border-color: var(--line);
}

dialog.tpl-preview-dialog .modal-head {
  border-bottom-color: var(--line-2);
}

dialog.tpl-preview-dialog .modal-close {
  color: var(--muted);
}

dialog.tpl-preview-dialog .tpl-preview-body {
  background: var(--paper-3);
}

.composer-dialog[open] form,
.review-dialog[open] form,
.entity-dialog[open] form,
.preview-dialog[open] {
  display: flex;
  flex-direction: column;
  max-height: min(92dvh, 960px);
  font-size: 14px;
}

.composer-dialog .modal-head,
.review-dialog .modal-head,
.entity-dialog .modal-head,
.preview-dialog .modal-head {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 22px 24px;
  border-bottom: 1px solid var(--line-2);
}

.composer-dialog .modal-head h2,
.review-dialog .modal-head h2,
.entity-dialog .modal-head h2,
.preview-dialog .modal-head h2 {
  font-size: 21px;
  font-weight: 800;
  margin: 2px 0 0;
}

.composer-dialog .eyebrow,
.review-dialog .eyebrow,
.entity-dialog .eyebrow,
.preview-dialog .eyebrow {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--faint);
  font-weight: 800;
}

.composer-dialog .sub,
.review-dialog .sub,
.entity-dialog .sub,
.preview-dialog .sub {
  color: var(--muted);
  margin-top: 6px;
}

.composer-dialog .spacer,
.review-dialog .spacer,
.entity-dialog .spacer,
.preview-dialog .spacer {
  flex: 1;
}

.composer-dialog .modal-body,
.review-dialog .modal-body,
.entity-dialog .modal-body,
.preview-dialog .modal-body {
  padding: 26px 24px;
  overflow: auto;
  flex: 1;
  /* One wheel gesture over an open dialog scrolled the PAGE behind it from 0 to
     1113px — so closing the dialog returned the user 1113px from the row they
     opened it on, breaking the "a row action comes back to the list it is a row
     of" contract by scroll alone (P10). */
  overscroll-behavior: contain;
}

/* …and the document itself is locked while any dialog is open, which is what
   stops the page moving when the body has nothing left to scroll. */
html.modal-open,
html.modal-open body {
  overflow: hidden;
}

/* An in-flight in-place round trip: edits would be lost mid-swap, so the
   body is inert; the dim is delayed so fast local trips never blink. */
dialog[aria-busy="true"] .modal-body {
  opacity: 0.55;
  pointer-events: none;
  transition: opacity 0.15s ease 0.12s;
}

.composer-dialog .modal-foot,
.review-dialog .modal-foot,
.entity-dialog .modal-foot,
.preview-dialog .modal-foot {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 20px 24px;
  border-top: 1px solid var(--line-2);
  background: var(--paper-2);
  position: sticky;
  bottom: 0;
}

/* The prototype's proportion. Ours handed the preview 48% — it is a
   CONFIRMATION surface, and starving the form column by 93px is the upstream
   cause of the 174px description track and of the dialog scrolling at
   1440x900. The 330px floor keeps the document legible (NC-04). */
.composer-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.2fr) minmax(330px, 0.8fr);
  gap: 18px;
  align-items: start;
}

/* NC-05: the whole justification for a 1180px two-column composer is
   see-as-you-type. With `align-items: start` the pane scrolled away the moment
   the user passed the line items — which is immediately at 1440x900, and always
   with the composer's disclosure open — leaving the right 530px of a modal that is blocking
   the entire app blank for 481.5px of scroll. It stays. */
.composer-grid > .doc-preview {
  position: sticky;
  top: 0;
  height: min(560px, calc(100dvh - 300px));
}

.composer-dialog .form-grid,
.review-dialog .form-grid,
.entity-dialog .form-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 18px;
}

/* `margin-bottom` alone left legacy style.css's `label { margin: 12px 0 4px }`
   top margin standing, so the grid's declared 18px gap rendered as 30px and the
   preview column started 12px above the first label it is supposed to line up
   with. The shorthand is the fix: the declared spacing is now the spacing. */
.composer-dialog .field label,
.review-dialog .field label,
.entity-dialog .field label,
.composer-dialog .style-label,
.review-dialog .style-label {
  display: block;
  font-size: 12px;
  font-weight: 760;
  color: var(--muted);
  margin: 0 0 6px;
}

.composer-dialog .field .opt,
.review-dialog .field .opt,
.entity-dialog .field .opt {
  font-weight: 400;
}

/* THE control rule for both dialogs. It used to gate on `input[type="text"]`
   while the markup writes `<input name="rate-EUR">` with no `type` at all, so
   the rule silently missed eight fields and legacy `style.css` styled them
   instead: 173px-wide boxes in 279px cells, and — because style.css hardcodes
   `background:#fff` — four WHITE boxes inside the dark More-options panel,
   landing on the very inputs that decide every converted total. Matching on
   "not a checkbox/radio/hidden" instead of enumerating types means a field
   added later cannot fall through the same hole. */
.composer-dialog input:not(:where([type="checkbox"], [type="radio"], [type="hidden"])),
.composer-dialog select,
.composer-dialog .combo-input,
.entity-dialog input:not(:where([type="checkbox"], [type="radio"], [type="hidden"])),
.entity-dialog select,
.entity-dialog .combo-input,
.entity-dialog .line-row input,
.review-dialog .more input:not(:where([type="checkbox"], [type="radio"], [type="hidden"])),
.review-dialog .more select,
.review-dialog .line-row input,
.review-dialog .combo-input {
  width: 100%;
  box-sizing: border-box;
  /* one scale: a select computes its line box at `normal` (~18px) while a text
     input inherits 21.7px, so identical padding landed them 3.69px apart. An
     explicit height ends the argument — and `input[type=date]`'s Chromium
     spinner can no longer push its row 5.7px past its neighbour. */
  height: var(--ctl);
  border: 1px solid var(--line-control);
  background: var(--paper);
  color: var(--ink);
  border-radius: 10px;
  padding: 10px 12px;
  font: inherit;
}

/* A textarea is the one control that must grow, and the line-row inputs are
   in-row controls on the `sm` step (see `.line-row input` below). */
.composer-dialog textarea,
.review-dialog textarea,
.entity-dialog textarea {
  height: auto;
}

/* Field width is a promise about content. A date input rendered 627px wide for
   a fixed 10-character value, putting the picker button ~490px from the figure
   it edits; the offset field is three characters. Both are sized to their
   content now, inside a cell that still holds the label's full width. */
.composer-dialog input[type="date"],
.entity-dialog input[type="date"],
.review-dialog .more input[type="date"] {
  width: auto;
  min-width: 170px;
  max-width: 200px;
}

.review-dialog .more input[name="offset"] {
  width: auto;
  max-width: 110px;
}

.composer-dialog input.num,
.review-dialog input.num,
.entity-dialog input.num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.composer-dialog .meta,
.review-dialog .meta,
.entity-dialog .meta {
  font-size: 11.5px;
  color: var(--faint);
  margin-top: 5px;
}

.composer-dialog h3,
.review-dialog h3,
.entity-dialog h3 {
  font-size: 15px;
  margin: 0;
  font-weight: 780;
}

.composer-dialog .row,
.review-dialog .row,
.entity-dialog .row {
  display: flex;
  align-items: center;
  gap: 10px;
}

.composer-dialog .wrap,
.review-dialog .wrap {
  flex-wrap: wrap;
}

.composer-dialog .line-block,
.entity-dialog .line-block,
.composer-dialog .style-block,
.review-dialog .style-block {
  margin-top: 20px;
}

.line-table {
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  margin-top: 10px;
  overflow: hidden;
}

.line-head,
.line-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 150px 40px;
  gap: 10px;
  align-items: center;
  padding: 9px 11px;
}

/* Description · Qty · Rate · Amount — the amount is an output, not a field.
   The numeric tracks give way before the first one does: at 1024 the fixed
   widths squeezed `1fr` to 72px and the `Description` caption painted outside
   its own cell (`scripts/layout_check.py`, `overflow`). */
/* The description is the only free-text field on the line, the longest content
   on it, and the string the CLIENT reads on the printed invoice; the numeric
   columns hold at most seven glyphs. At 1024 the tracks resolved to 92px for
   the description against 78/100/100 for the numbers — the free text was
   NARROWER than each figure, ~8 characters of room, so "Analytics Growth
   subscription" rendered as "Analytics G" (NC-03). The prototype's floor is
   190px; the numeric caps are tightened to survive 1024 with it. */
.line-table.qty .line-head,
.line-table.qty .line-row {
  /* Amount is sized on the widest figure the book prints WITH its ISO code
     ("1,250,000.00 THB" measures 134.9 + the cell's 11px inset), and the room
     comes out of Qty — a quantity is 1–4 characters and had 68 — never out of
     Description, whose floor is the prototype's 190px (owner round 2b, item
     9: at 1024 the amount broke at the space before the code and dropped THB
     onto a second line, growing the row 54 → 61px). */
  grid-template-columns:
    minmax(190px, 1.6fr) minmax(52px, 60px) minmax(76px, 100px)
    minmax(118px, 146px) 40px;
}

/* In-row controls take the `sm` step: a 44px field inside a line would make a
   three-line product taller than the form above it. Radius 8 (not 10) is the
   in-row radius, so a row cannot mix two corner sizes with its own buttons. */
.line-row input,
.composer-dialog .line-row input,
.review-dialog .line-row input,
.entity-dialog .line-row input {
  height: var(--ctl-sm);
  padding: 8px 10px;
  border-radius: 8px;
}

/* Per-field captions for the phone reflow (NC-16); the header names the
   columns at every wider width, so they are absent there. */
.line-row .line-cap {
  display: none;
}

.line-amount {
  text-align: right;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  font-weight: 650;
  /* the same ruling the list rows got in round 2: money never wraps, so a
     track set too narrow becomes an overflow the sweep SEES instead of a
     silent second line. The rule was written for money and applied to only
     one of the two places money is printed. */
  white-space: nowrap;
  /* the head captions sit at the track edge while an input insets its glyphs by
     1px border + 10px padding: three right-aligned columns, two of them 11px
     out. The output pays the same inset so all four edges land on one line. */
  padding-right: 11px;
}

/* Naming a selection lives in the Review footer, next to the button that saves it. */
.review-dialog .sel-name {
  width: 190px;
  padding: 8px 11px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--paper);
  color: var(--ink);
  font: inherit;
  font-size: 13px;
}

.home .manage-selections {
  margin-top: 12px;
}

.home .manage-selections .manage-row {
  gap: 8px;
  margin-top: 8px;
}

.home .manage-selections input {
  padding: 6px 9px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--paper);
  color: var(--ink);
  font: inherit;
  font-size: 13px;
}

.line-head {
  background: var(--paper-2);
  border-bottom: 1px solid var(--line);
  font-size: 11.5px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--faint);
  font-weight: 800;
}

.line-head .num {
  text-align: right;
  padding-right: 11px;
}

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

/* F-10: on phones the 338px of fixed qty/rate/amount tracks left the
   description a 0-px track ("DESCRIPTIQTY"). Each line reflows to two rows —
   description + remove first, the numbers on their own row under the numeric
   header. Composer-scoped; the Review matrix gets its own pass. */
@media (max-width: 700px) {
  /* NC-15: a phone sheet, not a shrunken desktop card. The dialogs kept their
     desktop chrome (390px wide at x=20, 15px radius, 22/26/20px paddings), so
     342px of a 430px screen was usable content — 58px thrown away, which is
     exactly the width the line table and the More-details fields were short of.
     The prototype's own phone rule is a full-bleed sheet with 14/15/12 chrome. */
  dialog.composer-dialog,
  dialog.preview-dialog,
  dialog.review-dialog,
  dialog.entity-dialog {
    width: 100vw;
    max-width: none;
    height: 100dvh;
    max-height: 100dvh;
    margin: 0;
    border: 0;
    border-radius: 0;
  }

  .composer-dialog[open] form,
  .review-dialog[open] form,
  .entity-dialog[open] form,
  .preview-dialog[open] {
    max-height: 100dvh;
    height: 100dvh;
  }

  .composer-dialog .modal-head,
  .preview-dialog .modal-head,
  .review-dialog .modal-head,
  .entity-dialog .modal-head {
    padding: 14px 15px;
  }

  .composer-dialog .modal-body,
  .preview-dialog .modal-body,
  .review-dialog .modal-body,
  .entity-dialog .modal-body {
    padding: 15px;
  }

  .composer-dialog .modal-foot,
  .preview-dialog .modal-foot,
  .review-dialog .modal-foot,
  .entity-dialog .modal-foot {
    padding: 12px 15px;
  }

  .composer-dialog .line-table.qty .line-head,
  .composer-dialog .line-table.qty .line-row,
  .entity-dialog .line-table.qty .line-head,
  .entity-dialog .line-table.qty .line-row,
  .review-dialog .line-table.qty .line-head,
  .review-dialog .line-table.qty .line-row {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1.2fr) 40px;
  }
  /* A table header's contract is "the thing below me is what I name". After the
     row reflows to `desc desc desc remove / qty rate amount amount`, the header
     named the SECOND sub-row while the first sat between them: the QTY caption
     and the first qty input shared an x range 53.7px apart vertically, with the
     description input in between, and it repeated only every other row (NC-16).
     The header goes; each numeric field carries its own caption, in place. */
  .composer-dialog .line-table.qty .line-head,
  .entity-dialog .line-table.qty .line-head,
  .review-dialog .line-table.qty .line-head {
    display: none;
  }

  .line-row .line-cap {
    display: block;
    font-size: 10px;
    font-weight: 800;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--faint);
    align-self: end;
  }

  .composer-dialog .line-table.qty .line-row,
  .entity-dialog .line-table.qty .line-row,
  .review-dialog .line-table.qty .line-row {
    grid-template-areas:
      "desc   desc   desc   remove"
      "capq   capr   capa   capa"
      "qty    rate   amount amount";
    row-gap: 2px;
  }
  .line-row .line-cap.q { grid-area: capq; }
  .line-row .line-cap.r { grid-area: capr; }
  .line-row .line-cap.a { grid-area: capa; text-align: right; }
  .line-row input[name$="-desc"],
  .line-row input[name*="-desc-"] {
    grid-area: desc;
  }
  .line-row input[name$="-qty"],
  .line-row input[name*="-qty-"] {
    grid-area: qty;
  }
  .line-row input[name$="-rate"],
  .line-row input[name*="-rate-"] {
    grid-area: rate;
  }
  .line-row .line-amount {
    grid-area: amount;
  }
  .line-row [data-remove-item],
  .line-row [data-remove-line],
  .line-row [data-review-remove-line] {
    grid-area: remove;
    justify-self: end;
  }
}

/* It was the only interactive element on the screen with no container and no
   hit area — 21.7px of bare green text with the UA's triangle, floating between
   two bands. Styled as the quiet control it is, with the app's own caret. */
.composer-dialog .more > summary,
.review-dialog .more > summary {
  display: flex;
  align-items: center;
  gap: 7px;
  margin-top: 14px;
  min-height: var(--ctl-sm);
  padding: 0 11px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--paper-2);
  cursor: pointer;
  color: var(--green);
  font-weight: 760;
  list-style: none;
}

.composer-dialog .more > summary::-webkit-details-marker,
.review-dialog .more > summary::-webkit-details-marker {
  display: none;
}

.composer-dialog .more > summary::before,
.review-dialog .more > summary::before {
  content: "▾";
  font-size: 11px;
  line-height: 1;
  color: var(--faint);
  transition: transform 0.12s ease;
}

.composer-dialog .more[open] > summary::before,
.review-dialog .more[open] > summary::before {
  transform: rotate(180deg);
}

.composer-dialog .more > summary:hover,
.review-dialog .more > summary:hover {
  border-color: var(--faint);
}

/* --- More options: three labelled bands, not one flat pile (MR-12) -------- */

.review-dialog .more-band {
  margin-top: 16px;
}

.review-dialog .more-band > h4 {
  margin: 0 0 10px;
  font-size: 11px;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--faint);
}

/* The rates band collapses to its own summary line: three of eight field slots
   went to values prefilled from a live quote and almost never touched. It opens
   itself when one is missing, which is the only time it decides anything. */
.review-dialog .more-band.rates > summary {
  display: flex;
  align-items: baseline;
  gap: 10px;
  cursor: pointer;
  list-style: none;
  flex-wrap: wrap;
}

.review-dialog .more-band.rates > summary::-webkit-details-marker {
  display: none;
}

.review-dialog .more-band.rates > summary::after {
  content: "▾";
  font-size: 11px;
  color: var(--faint);
}

.review-dialog .more-band.rates[open] > summary::after {
  content: "▴";
}

.review-dialog .more-band.rates > summary > h4 {
  margin: 0;
  font-size: 11px;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--faint);
}

.review-dialog .more-band.rates > .form-grid {
  margin-top: 10px;
}

/* `Add client` at the end of the list it adds to — the other half of taking it
   out of the filter row. */
.run-list-foot {
  display: flex;
  justify-content: flex-start;
  margin-top: 10px;
}

.composer-dialog .check,
.review-dialog .check {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: 13px;
  color: var(--muted);
  margin: 0;
  /* WCAG 2.2 SC 2.5.8 asks 24x24 of a pointer target; the label was 20.14px */
  min-height: 24px;
  padding: 3px 0;
  cursor: pointer;
}

/* `accent-color: auto` painted these the system blue — the only blue in an
   otherwise single-hue dialog. */
.composer-dialog .check input,
.review-dialog .check input {
  width: 18px;
  height: 18px;
  min-height: 0;
  flex: none;
  accent-color: var(--green);
}

.template-picker {
  display: flex;
  gap: 8px;
  margin-top: 8px;
}

/* Template choice is a once-a-year setting with a company default already
   applied; client, product, currency and the line amounts are what the user
   came for. Three full-width tiles at 184x95.6 made the block BIGGER than the
   four-field identity grid above it and pushed the line items — the actual
   work — further above the fold (NC-14). The prototype's tile is 92px. */
.template-picker .tpl {
  border: 2px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 8px;
  cursor: pointer;
  background: var(--paper);
  text-align: center;
  flex: 0 0 auto;
  width: 92px;
}

.template-picker .tpl.on {
  border-color: var(--green);
}

.template-picker .tpl input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
}

/* A row of tiles is read as one horizontal band of facts; the issuer name
   wrapped to two lines in a 114px box, so its caption sat 22.6px below the
   other four and the band looked misaligned. The declared intent (`overflow`
   + `text-overflow: ellipsis`) never fired without `nowrap` (NC-21). */
.issue-summary .mini-metric b {
  white-space: nowrap;
}

.template-picker .tpl-art {
  display: block;
  height: 46px;
  border-radius: 5px;
  background: linear-gradient(var(--ink) 0 2px, transparent 2px 100%) 0 0 / 100% 9px repeat-y;
  opacity: 0.28;
}

.template-picker .tpl-modern .tpl-art {
  background: linear-gradient(var(--green) 0 5px, transparent 5px 100%) 0 0 / 100% 11px repeat-y;
  opacity: 0.5;
}

.template-picker .tpl-name {
  display: block;
  margin-top: 7px;
  font-size: 12px;
  font-weight: 730;
  color: var(--muted);
}

/* The document keeps its natural A4 width (794 px) and is scaled down to the
   pane, so the preview is the real page geometry, never a reflowed guess. */
.doc-preview {
  position: relative;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: #fff;
  overflow: hidden;
  height: 560px;
  --doc-scale: 1;
}

/* The mark that says this document is not issued yet (owner round 2b, item
   4). It rides the HOST pane, above the iframe — the rendered document's own
   bytes are the invoice goldens and nothing here may reach them (a srcdoc
   iframe is a separate document; host CSS cannot cross into it anyway).

   The size follows `--doc-scale`, the same factor `shell.js` computes to fit
   an A4 page into the pane, so the mark keeps its proportion to the paper at
   every width instead of being one fixed size over three different zooms.

   The colour does NOT flip with the theme, and that is deliberate rather than
   an omission: `.doc-preview` is unconditionally white in both palettes,
   because a preview of paper is paper. A theme-flipping token here would put
   a dark-theme colour on a light surface. */
/* The OTHER mark this pane can carry (failure-telemetry, 2026-08-17): the
   refresh failed, so the document under it is the PREVIOUS one. It sits at the
   top of the pane rather than over the middle — the watermark owns the middle,
   and two marks in one place would fight — and it dims the paper it is about,
   which is what stops the stale document from reading as the current one.
   The red tokens are the ones `.error-card` already uses; no new vocabulary. */
.doc-stale {
  position: absolute;
  left: 8px;
  right: 8px;
  top: 8px;
  z-index: 2;
  border: 1px solid var(--red);
  background: var(--red-soft);
  color: var(--red);
  border-radius: var(--radius-sm);
  padding: 8px 10px;
  font-size: 12px;
  font-weight: 600;
  line-height: 1.35;
}

.doc-preview[data-stale] iframe {
  opacity: 0.45;
}

.doc-watermark {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%) rotate(-24deg);
  font-size: calc(92px * var(--doc-scale));
  font-weight: 800;
  letter-spacing: 0.04em;
  color: var(--doc-watermark);
  white-space: nowrap;
  pointer-events: none;
  z-index: 1;
}

.doc-preview.tall {
  height: 430px;
  margin-top: 16px;
}

.doc-preview iframe {
  width: 794px;
  height: calc(100% / var(--doc-scale));
  transform: scale(var(--doc-scale));
  transform-origin: 0 0;
  border: 0;
  background: #fff;
}

/* preview dialog */

.issue-summary {
  display: grid;
  /* auto-fit: the composer preview grew a fifth (due-date) tile — a fixed
     four-column grid wrapped it onto a lonely second row. 118px min is what
     lets all five share one row inside the 760px preview dialog; the golden's
     row-of-tiles look survives the approved extra tile. */
  grid-template-columns: repeat(auto-fit, minmax(118px, 1fr));
  gap: 10px;
  margin-bottom: 20px;
}

.issue-summary .mini-metric {
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--paper-2);
  padding: 11px 9px;
  min-width: 0;
}

.issue-summary .mini-metric b {
  display: block;
  font-size: 15px;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The money tile of the preview obeys the tile law too: `450,000.00 USD` used
   to drop its currency onto a second line here (owner 2026-08-04). It is the
   one `b` in this row that must never be ellipsised either — a name may be
   shortened, a figure may not. */
.issue-summary .mini-metric b.num {
  font-size: 14px;
  white-space: nowrap;
  overflow: visible;
  text-overflow: clip;
}

.issue-summary .mini-metric span {
  font-size: 11.5px;
  color: var(--faint);
}

.preview-dialog h3 {
  font-size: 14px;
  margin: 0 0 9px;
}

.changes {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.change-chip {
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 5px 11px;
  font-size: 12px;
  font-weight: 720;
  color: var(--muted);
  background: var(--paper-2);
}

.blockers {
  border: 1px solid var(--red);
  border-left-width: 3px;
  background: var(--red-soft);
  color: var(--ink);
  border-radius: var(--radius-sm);
  padding: 13px 15px;
}

.blockers ul {
  margin: 7px 0 0;
  padding-left: 18px;
}

.invoices .error-card {
  border: 1px solid var(--red);
  border-left-width: 3px;
  background: var(--red-soft);
  border-radius: var(--radius-sm);
  padding: 13px 15px;
  margin-bottom: 16px;
}

/* combobox (behavior standard §3) */

.combo {
  position: relative;
}

.combo .combo-native {
  display: none;
}

/* §7: every combobox trigger ends in a caret. Without one a bordered box reads
   as a free-text field — and every OTHER picker in the app is a native <select>
   that paints its own chevron, so the same dialog said "this opens a list" two
   different ways, 215px apart in one matrix row. The trigger stays an input
   (search is available without a second field, which is better than the
   prototype's button); only the missing affordance was the defect. */
.combo-caret {
  position: absolute;
  right: 11px;
  top: 50%;
  transform: translateY(-50%);
  font-size: 11px;
  line-height: 1;
  color: var(--faint);
  pointer-events: none;
  transition: transform 0.12s ease;
}

.combo-input[aria-expanded="true"] + .combo-caret {
  transform: translateY(-50%) rotate(180deg);
}

/* the value must never run under the caret; `.combo >` matches the scoped
   control rules' specificity and comes later in the sheet */
.combo > .combo-input {
  padding-right: 28px;
  cursor: pointer;
}

.combo-list {
  position: fixed;
  z-index: var(--z-popover);
  margin: 0;
  padding: 5px 5px 0;
  list-style: none;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow);
  /* the real ceiling is computed from the viewport in shell.js `place()`;
     this is only the floor for a panel measured before it is placed */
  max-height: 340px;
  /* THE WIDTH LAW (owner round 2b, item 10), the same one the row menu has
     always had: at least as wide as the control that opened it, growing
     LEFTWARD from a right edge aligned with the trigger's, never wider than
     the room to its left. `shell.js placePopover` sets the two properties;
     the panel itself is shrink-to-fit, so an option decides the width. */
  width: auto;
  min-width: var(--pop-min-width, 0);
  max-width: var(--pop-max-width, none);
  overflow: auto;
  /* the panel scrolls, so its end must not hand the wheel to the modal body
     underneath — that scroll would move the panel out from under the cursor */
  overscroll-behavior: contain;
}

/* keyboard travel must not park the active row under the pinned tally */
.combo-list.has-more {
  scroll-padding-bottom: 42px;
}

.combo-list li {
  display: flex;
  align-items: baseline;
  padding: 8px 10px;
  /* one popover vocabulary (2026-08-07): a rowmenu item and a combo option
     share size, radius and hover — 13px/8px/--green-soft — so the two floating
     lists read as one product. The sweep in scripts/verify_actions.py pins the
     computed styles of both open states against each other. */
  font-size: 13px;
  border-radius: 8px;
  cursor: pointer;
  /* an option is one line: folding "Analytics Growth / 910.00 EUR" into a
     two-line column is the defect shape the width law exists to prevent */
  white-space: nowrap;
}

.combo-list li.active,
.combo-list li:hover {
  background: var(--green-soft);
}

/* the label yields first — where the panel IS clamped (a trigger near the
   left edge, or a phone), the name ellipsises and keeps its full text in a
   title; the figure beside it never shrinks */
.combo-list li .combo-label {
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}

.combo-list li .combo-aux {
  flex: 0 0 auto;
  margin-left: auto;
  padding-left: 12px;
  color: var(--faint);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}

.combo-list li.combo-none {
  color: var(--faint);
  cursor: default;
}

/* §3's way forward. Deliberately NOT `.combo-none`: that row is a statement and
   is greyed out as one, while this is a control — greying it out would deny
   what it offers. It fills its row so the whole strip is the target. */
.combo-list li.combo-create {
  padding: 0;
}

.combo-list li.combo-create .linkbtn {
  display: block;
  width: 100%;
  padding: 8px 10px;
  text-align: left;
  font-weight: 600;
}

/* The overflow tally is pinned to the panel's bottom edge, not left at the end
   of a scroll (owner 2026-08-05) — full-bleed over the panel's side padding so
   no row slides through a gutter behind it. */
.combo-list li.combo-more {
  position: sticky;
  bottom: 0;
  margin: 5px -5px 0;
  padding: 9px 15px;
  border-top: 1px solid var(--line);
  border-radius: 0;
  background: var(--paper);
  font-size: 12px;
}

.combo-list li.combo-more:hover {
  background: var(--paper);
}

/* the last row keeps the panel's bottom padding; the pinned tally supplies its
   own, so it can sit flush against the edge */
.combo-list li:last-child:not(.combo-more) {
  margin-bottom: 5px;
}

@media (max-width: 900px) {
  .composer-grid {
    grid-template-columns: minmax(0, 1fr);
  }

  .composer-dialog .form-grid,
  .review-dialog .more .form-grid {
    grid-template-columns: minmax(0, 1fr);
  }

  /* The document, not the tiles, is what "Ready to issue" exists to show:
     five stacked one-value tiles pushed it 545px below the first screen.
     Two-up puts it back near the fold. */
  .issue-summary {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .doc-preview {
    height: 380px;
  }
}

/* --- Review matrix (Create multiple) --------------------------------------
   Goldens 1440-multiple-review-*.png / 1440-multiple-preview-change-legend.png
   / 430-multiple-review-cards.png. The grid template is shared by the header
   and every row (behavior standard §5), so an open combobox or an expanded
   line editor never moves a numeric column: the editor is its own full-width
   grid row. */

/* The prototype's own tracks. Ours were 110/210/130 — ragged widths destroy the
   rhythm that makes three stats read as a set, and the 130px cell was too narrow
   for its own copy, so the strip's height DOUBLED (43.4 -> 86.8) between two
   states of the same screen. */
.selection-header {
  display: grid;
  grid-template-columns: minmax(0, 1fr) repeat(3, 140px);
  gap: 10px;
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 13px;
  background: var(--paper-2);
  margin-bottom: 14px;
}

.selection-header b {
  display: block;
  font-weight: 800;
}

/* The run's name, typed where it is shown. It reads as a title until it is
   touched: the box appears on hover and focus, so the strip is not a form. */
/* With no baseline the strip has TWO cells, not four (owner round 2b, item
   3b) — stated as a modifier so the base rule above stays untouched, and
   restated inside the phone block below, where a 2-track rule of its own
   already wins. */
.selection-header.no-baseline {
  grid-template-columns: minmax(0, 1fr) 140px;
}

/* the name says it is editable the way the run cards do: the same ✎, visible
   at rest rather than on hover (owner round 2b, item 3a) */
/* The ✎ rides the right end of the name field the way a search icon rides a
   search box — an adornment positioned against the CELL, never a wrapper
   around the input. A wrapper is what broke it: a form control's intrinsic
   width is the browser's 20-character default, and once any shrink-to-fit
   parent has measured that, neither a CSS width nor `size` nor
   `field-sizing: content` moves the box off it (measured: 161px under a
   402px label, at every viewport). The input keeps the full-width cell it
   always had; only the glyph is new. */
.selection-header > div:first-child {
  position: relative;
}

/* At the START of the field, not its end: the field spans the whole cell, so
   a trailing glyph lands 700px from the name it belongs to and reads as the
   neighbouring stat's. */
.selection-header .sel-pencil {
  position: absolute;
  left: -1px;
  top: 3px;
  color: var(--faint);
  font-size: 14px;
  pointer-events: none;
}

.selection-header > div:first-child:hover .sel-pencil {
  color: var(--green);
}

.selection-header .sel-title {
  display: block;
  width: 100%;
  border: 1px solid transparent;
  background: transparent;
  border-radius: 8px;
  /* the leading 22px is room for the ✎ that marks the field editable */
  padding: 1px 6px 1px 22px;
  margin: -1px -6px 0;
  font: inherit;
  font-weight: 800;
  color: var(--ink);
  min-height: 0;
}

.selection-header .sel-title::placeholder {
  color: var(--ink);
  opacity: 1;
}

.selection-header .sel-title:hover {
  border-color: var(--line);
  background: var(--paper);
}

.selection-header .sel-title:focus {
  border-color: var(--green);
  background: var(--paper);
  outline: none;
  box-shadow: 0 0 0 3px rgba(8, 120, 90, 0.3);
}

.selection-header span {
  font-size: 11.5px;
  color: var(--faint);
}

.run-toolbar {
  display: grid;
  grid-template-columns: minmax(240px, 1fr) auto auto;
  gap: 9px;
  align-items: center;
}

/* The control itself is the shared band's (`.r13 .search-input`) — same
   boundary token, same padding, same focus ring. Only the grid around it is
   this dialog's own: a row filter with no address state has no chips and no
   Reset, so it is not a filter band, it is a search box in a toolbar. */
.run-toolbar .search-input {
  width: 100%;
}

.run-toolbar .btn[aria-pressed="true"] {
  background: var(--green-soft);
  border-color: var(--green);
  color: var(--green);
}

.run-list {
  border: 1px solid var(--line);
  border-radius: 12px;
  overflow: hidden;
  margin-top: 13px;
}

/* The prototype's own template. Three tracks had drifted: the checkbox 30 vs
   36 (which is what stretched the native box to 23x13 at 430), the product
   floor 200 vs 210, and the currency track 84 vs 58 — the 84 existed only to
   host a select that no longer lives here (MR-01/MR-02/MR-20). The row is the
   unit the eye repeats 76 times, so a few px per track is what "ragged" was. */
.run-grid-head,
.run-row {
  display: grid;
  grid-template-columns:
    36px minmax(180px, 1.25fr) minmax(210px, 1.05fr)
    56px 122px 58px 118px;
  gap: 9px;
  align-items: center;
  padding: 10px;
}

.run-grid-head {
  font-size: 11px;
  color: var(--faint);
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  background: var(--paper-2);
}

/* The master's phone label: on a desktop the column position says what the
   checkbox does, so the words stay hidden; the ≤900 block below turns the
   head into a one-control row and lets them speak. */
.run-master-label {
  display: none;
}

/* A verb that rides a hint line — the Rates stamp's `Update` (owner
   2026-08-15: beside the Updated line, dash-underlined, not a button). */
.hintlink {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: var(--muted);
  text-decoration: underline dashed;
  text-underline-offset: 3px;
  cursor: pointer;
}

.hintlink:hover,
.hintlink:focus-visible {
  color: var(--ink);
}

/* The composer's per-currency rate fields: one under another, in the
   form grid's right column (owner 2026-08-15 — the two-column flow had
   scattered two rates into opposite corners). */
.composer-dialog .rate-fields {
  grid-column: 2;
  display: grid;
  gap: 12px;
  align-self: start;
}

@media (max-width: 700px) {
  .composer-dialog .rate-fields {
    grid-column: 1;
  }
}

.run-grid-head .num,
.run-row .num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.run-row-wrap {
  border-top: 1px solid var(--line-2);
}

.run-row-wrap:first-child {
  border-top: 0;
}

.run-row {
  cursor: pointer;
  font-size: 13.5px;
}

.run-row:hover {
  background: var(--paper-2);
}

/* (The row's own `:focus-visible` ring died with `role="checkbox"` — the row
   is a hit area now and the native checkbox is the tab stop; its ring is
   defined with the dialogs' focus rules, 2026-08-21.) */

.run-row-wrap.selected {
  background: var(--green-softer);
}

.run-row-wrap.selected .run-row:hover {
  background: var(--green-soft);
}

.run-row select,
.run-row .combo-input {
  width: 100%;
  height: var(--ctl-sm);
  border: 1px solid var(--line-control);
  background: var(--paper);
  color: var(--ink);
  border-radius: 8px;
  padding: 7px 26px 7px 8px;
  font: inherit;
  font-size: 13px;
}

.run-row .combo-caret {
  right: 8px;
}

.run-cell-client {
  min-width: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 6px;
}

.run-cell-client b {
  font-weight: 760;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.run-cell-client .sub,
.run-cell-client .row-note {
  flex-basis: 100%;
  font-size: 11.5px;
  color: var(--faint);
}

.run-cell-client .row-note::before {
  content: "· ";
}

/* The composer's counterpart to the matrix's `Needs a … rate` row note: an
   empty line table has to say why it is empty. */
.line-table .line-note {
  padding: 10px 4px 2px;
  font-size: 12px;
  color: var(--faint);
}

/* The gap that proves `TOTAL` and `BILLED IN` are two columns. Two captions of
   one size, one weight and one colour, with the first right-aligned hard
   against the second left-aligned, read as one phrase — and the row below said
   `1,293.00 EUR EUR`, because the code was printed in the Total cell as well as
   in its own (design critique 2026-08-06 §3). The Total cell prints the number
   only now; this is the head catching up. */
.run-grid-head .run-head-currency {
  padding-left: 12px;
}

.run-row-change {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 10px 10px 49px;
  font-size: 11.5px;
}

/* A refused currency change (RM-04), on either editor.

   It is the only message on these screens that reports something the app
   REFUSED to do, so it must not read like the `Converted at` note beside it,
   which reports a success — both were the same muted grey, which is a large
   part of why the matrix's missing one went unnoticed for a round.

   Amber is this app's existing "issuing is still available; worth a look
   first" tier (the preview's warnings, `Total changed`), and that is exactly
   what a refusal is: nothing is broken, the rows kept the currency they were
   typed in, and there is one thing to do about it. §9 is satisfied by the
   sentence — the colour carries no meaning the words do not. 5.14:1 on light
   paper, 7.9:1 on dark, both AA for this size.

   One rule for both editors, because it is one event: the composer's
   `.currency-notice` and the matrix row's are the same class fed by the same
   `planstate.rate_missing_notice`. */
.composer-dialog .currency-notice,
.review-dialog .currency-notice {
  color: var(--amber);
}

.run-editor {
  padding: 4px 16px 18px 49px;
  background: var(--paper-2);
  border-top: 1px dashed var(--line);
}

.run-editor[hidden] {
  display: none;
}

.run-editor .line-table {
  background: var(--paper);
  margin-top: 12px;
}

.run-editor-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: 12px;
}

.run-editor-actions .spacer {
  flex: 1;
}

/* Change legend (§9): a dot is never the only carrier — every marker has text. */

.flag-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  display: inline-block;
  flex: none;
  margin-right: 6px;
}

.flag-dot.amber { background: var(--amber); }
.flag-dot.blue { background: var(--blue); }
.flag-dot.green { background: var(--green); }
.flag-dot.red { background: var(--red); }
.flag-dot.muted { background: var(--faint); }
/* `Different product` — the flag kind added 2026-08-04; violet is the one
   accent the change legend had left (the payer flag it was reserved for was
   dropped with payer itself, owner 2026-08-01) */
.flag-dot.violet { background: var(--violet); }

.row-flag {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  border-radius: 999px;
  padding: 2px 8px;
  font-size: 11px;
  font-weight: 750;
}

.row-flag.amber { background: var(--amber-soft); color: var(--amber); }
.row-flag.blue { background: var(--blue-soft); color: var(--blue); }
.row-flag.muted { background: var(--paper-3); color: var(--muted); }
.row-flag.violet { background: var(--violet-soft); color: var(--violet); }

/* `Currency set` is not a member of the change legend — amber/blue/violet all
   mean "different from the last run", and this means "you set it for this run".
   It borrows none of their colours and states its own case in the app's ink. */
.row-flag.set {
  background: var(--paper);
  color: var(--ink);
  border: 1px solid var(--ink);
  padding: 1px 7px;
}

/* --- the currency cell (MR-01/MR-02, owner ruling 2026-08-06) -------------
   The 95% case is a value like any other: grey mono text, no control, no box.
   The 5% case — an explicit override — is a boxed, ink-weight code with a ⇄
   glyph, which is what makes it findable scanning a 76-row column. The words
   are in the row's flag line; this is only the glance (§9). */
.run-cell-currency {
  color: var(--muted);
  font-size: 13px;
}

.run-grid-head .run-head-currency {
  grid-column: 6 / -1;
}

.run-cell-currency .cur-set {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 1px 6px;
  border: 1px solid var(--ink);
  border-radius: 7px;
  color: var(--ink);
  font-weight: 800;
  font-size: 12px;
  line-height: 1.5;
}

.run-cell-currency .cur-set i {
  font-style: normal;
  font-size: 11px;
  opacity: 0.75;
}

/* The row editor opens on the two fields that decide what this one document
   is, above the lines they act on: the currency, which changes the money, and
   the print options, whose default option carries a whole sentence and so
   needs more room than the 220px the currency lives in. */
.run-editor-head {
  display: grid;
  grid-template-columns: 220px minmax(0, 320px);
  gap: 12px;
  margin-bottom: 4px;
}

.change-chip.amber { background: var(--amber-soft); color: var(--amber); }
.change-chip.blue { background: var(--blue-soft); color: var(--blue); }
.change-chip.green { background: var(--green-soft); color: var(--green); }
.change-chip.muted { background: var(--paper-3); color: var(--muted); }
.change-chip.violet { background: var(--violet-soft); color: var(--violet); }

.change-chip {
  display: inline-flex;
  align-items: center;
}

/* Issue preview */

.preview-finance-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
  margin-bottom: 6px;
}

.preview-finance-card {
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 14px;
  background: var(--paper-2);
}

.financial-delta {
  margin-top: 8px;
}

.financial-delta b {
  font-size: 19px;
  font-weight: 820;
  display: block;
}

.financial-delta[data-delta-dir="up"] b { color: var(--green); }
.financial-delta[data-delta-dir="down"] b { color: var(--red); }

.financial-delta .meta {
  display: block;
}

.warnings {
  border: 1px solid var(--line);
  background: var(--paper-2);
  color: var(--muted);
  border-radius: 12px;
  padding: 12px 14px;
  margin-top: 14px;
  font-size: 13px;
}

.warnings ul,
.blockers ul {
  margin: 6px 0 0;
  padding-left: 18px;
}

.pv-list {
  margin-top: 15px;
  border: 1px solid var(--line);
  border-radius: 12px;
  overflow: hidden;
}

.pv-row {
  display: flex;
  align-items: center;
  gap: 10px;
  justify-content: space-between;
  padding: 10px 12px;
  border-top: 1px solid var(--line-2);
  font-size: 13.5px;
}

.pv-row:first-child {
  border-top: 0;
}

.pv-row .sub,
.pv-row .row-note {
  display: block;
  font-size: 11.5px;
  color: var(--faint);
}

.pv-row.amber { box-shadow: inset 3px 0 0 var(--amber); }
.pv-row.blue { box-shadow: inset 3px 0 0 var(--blue); }
.pv-row.violet { box-shadow: inset 3px 0 0 var(--violet); }
.pv-row.excluded { color: var(--muted); }

.excluded-list .eyebrow {
  padding: 10px 12px 0;
}

.blocked-note {
  color: var(--red);
}

@media (max-width: 1180px) {
  .selection-header {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* Phones — and the band above them where the grid does not actually fit: the
   matrix becomes cards (behavior standard §5), the same inputs stacked, with
   the numbers still right-aligned in their own line.

   944, not 900, and the number is measured rather than chosen. The grid's own
   column floors are 856px border-box; the dialog's inner width is
   `100vw − 88px`, so the floors fit only from 945 up. Between 901 and 944 the
   matrix drew a grid it could not show and `.run-list { overflow: hidden }`
   clipped the trailing column — the `Edit invoice` control, the one way into a
   row's line items (13px of it gone at 920, measured). Cards now own the whole
   unfittable band. ONLY the matrix-scoped rules move: the other `900px` blocks
   in this sheet are the shell nav, the invoice cards and the shared dialog
   chrome, and each has its own measured reason to sit where it does. */
@media (max-width: 944px) {
  /* The head's columns go with the grid — but the master checkbox does NOT
     vanish with them (round-4 accept): the deleted `Select filtered` button
     lived in the always-visible toolbar, so the phone had bulk selection
     before the master replaced it and must keep it after. The head becomes a
     one-control row: the master plus its now-visible label. */
  .run-grid-head {
    display: block;
    padding: 8px 12px;
  }

  .run-grid-head > span {
    display: none;
  }

  .run-grid-head > span:first-child {
    display: flex;
    align-items: center;
    gap: 8px;
  }

  .run-master-label {
    display: inline;
    letter-spacing: 0.04em;
  }

  /* one column: two selects side by side would each be too narrow to read
     their own option labels */
  .run-editor-head {
    grid-template-columns: minmax(0, 1fr);
  }

  .run-row {
    grid-template-columns: 30px minmax(0, 1fr);
    row-gap: 8px;
    padding: 12px;
  }

  .run-row > .run-cell-product,
  .run-row > .run-cell-currency,
  .run-row > .btn {
    grid-column: 2;
  }

  .run-row > .run-cell-lines,
  .run-row > .run-cell-total {
    grid-column: 2;
    text-align: left;
  }

  .run-row .run-cell-lines::after {
    content: " lines";
    color: var(--faint);
    font-size: 11.5px;
  }

  /* MR-05: at 430x932 the matrix opened with ZERO client rows visible — the
     first row sat at y=796.8 against a 505px body, 94px BELOW the fold, a
     +200.7px regression on the prototype's one visible card. The screen's whole
     purpose is picking clients, so the first interaction on a phone was always
     a scroll, with no way to confirm the selection was even populated.
     Everything below is the vertical budget, band by band. */

  /* the search takes its own row; the two filters share the next one, instead
     of three full-width buttons stacked */
  .run-toolbar {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .run-toolbar .search {
    grid-column: 1 / -1;
  }

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

  .run-editor,
  .run-row-change {
    padding-left: 12px;
  }

  /* four stats 2-up rather than a 4-high stack */
  .selection-header {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    padding: 11px;
    margin-bottom: 11px;
  }

  /* …and two stats stay 2-up: the modifier's desktop track would otherwise
     win here on specificity and pin a 140px column on a phone */
  .selection-header.no-baseline {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  /* the head's own subtitle wrapped to THREE lines (160.3px vs the prototype's
     114.8): the dialog names itself in its h2, and the sentence under it is
     read once, not on every phone visit */
  .review-dialog .modal-head .sub {
    display: none;
  }

  /* the footer was 192.1px — six children over three rows, 20.6% of the screen.
     Two rows now: the two saves, then the total and the primary. */
  .review-dialog .modal-foot {
    gap: 8px;
    /* the totals pill must never push Save selection off-screen — this pair
       moved here WITH the block (2026-08-21): it had stayed at 900 when the
       matrix band moved to 944, leaving 901-944 the compact footer with the
       nowrap pill — half of each recipe */
    flex-wrap: wrap;
  }

  .review-dialog .modal-foot .spacer {
    display: none;
  }

  .review-dialog .modal-foot [data-review-summary] {
    flex: 1;
    white-space: normal;
    text-align: right;
  }
}


/* --- burger sheet (all sections, every width) ----------------------------- */

.shell .topbar .nav-burger {
  /* mobile-only: must outweigh `.shell .topbar .btn { display:inline-flex }`
     or the burger leaks into the desktop topbar */
  display: none;
}

/* The sheet opens on the side the burger is on.

   Owner, 2026-08-04: "don't get why the sandwich menu is on the left while the menu opens on the right
   opens — usually it opens where you tapped". The approved prototype
   does put the ☰ at the topbar's left and slide the sheet in from the right
   (`final-candidate-r13.html`), so this is a deliberate, owner-ruled deviation
   from it — recorded in `devstatus.DEVIATIONS` so nobody "restores" it.

   Left, not "burger moved right": this drawer *is* the sidebar, and above
   900px the sidebar lives on the left. One mental model at every width. The
   whole geometry is mirrored, not just the anchor — the row packs to the
   start, the panel's edge is its right one, the shadow falls to the right,
   and the entrance travels from the left. */
.shell-menu {
  position: fixed;
  inset: 0;
  background: rgba(15, 25, 22, 0.58);
  display: flex;
  justify-content: flex-start;
  align-items: stretch;
  z-index: var(--z-modal);
}

.shell-menu[hidden] {
  display: none;
}

.shell-menu-panel {
  width: min(320px, 84%);
  height: 100%;
  background: var(--paper);
  border-right: 1px solid var(--line);
  box-shadow: 14px 0 44px rgba(15, 25, 22, 0.18);
  display: flex;
  flex-direction: column;
  overflow: auto;
  animation: menu-in 0.16s ease-out;
}

@keyframes menu-in {
  from {
    transform: translateX(-100%);
  }
}

@media (prefers-reduced-motion: reduce) {
  .shell-menu-panel {
    animation: none;
  }
}

.shell-menu-head {
  padding: 18px;
  border-bottom: 1px solid var(--line);
  display: flex;
  align-items: flex-start;
  gap: 12px;
}

.shell-menu-head .spacer {
  flex: 1;
}

.shell-menu-head h2 {
  font-size: 21px;
  margin: 2px 0 0;
  color: var(--ink);
}

.shell-menu-head .eyebrow {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--faint);
  font-weight: 800;
}

.shell-menu-head .btn {
  border: 1px solid var(--line);
  background: transparent;
  color: var(--ink);
  width: 38px;
  height: 38px;
  border-radius: 10px;
  font: inherit;
  font-size: 17px;
  cursor: pointer;
}

.menu-nav {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 14px;
}

.menu-nav a {
  border: 0;
  background: transparent;
  color: var(--muted);
  padding: 13px;
  border-radius: 10px;
  text-align: left;
  font-weight: 700;
  font-size: 15px;
  text-decoration: none;
  display: flex;
  align-items: center;
  gap: 12px;
}

.menu-nav a.active,
.menu-nav a:hover {
  background: var(--green-soft);
  color: var(--green);
}

.menu-nav .icon {
  width: 20px;
  text-align: center;
  color: var(--faint);
  flex: none;
}

.menu-nav a.active .icon {
  color: var(--green);
}

html.menu-open,
html.menu-open body {
  overflow: hidden;
}

@media (max-width: 1180px) {
  :root {
    --sidebar: 196px;
  }
  .shell .main {
    padding-left: 24px;
    padding-right: 24px;
  }
  .home .chart {
    gap: 5px;
  }
  .home .bar {
    width: 12px;
  }
  .home .create-content-grid {
    grid-template-columns: minmax(0, 1fr) minmax(300px, 0.8fr);
  }
  .home .kpis {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* --- ≤900px: the sidebar becomes a bottom tab bar ------------------------- */

@media (max-width: 900px) {
  .shell {
    width: 100%;
    margin: 0;
    min-height: 100dvh;
    border: 0;
    border-radius: 0;
    grid-template-columns: 1fr;
    box-shadow: none;
  }
  .shell .sidebar {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: var(--z-nav);
    height: 64px;
    border-right: 0;
    border-top: 1px solid var(--line);
    padding: 6px 8px;
    background: var(--paper);
  }
  .shell .brand,
  .shell .sidebar-footer {
    display: none;
  }
  .shell .nav {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    gap: 2px;
  }
  .shell .nav a {
    padding: 7px 2px;
    justify-content: center;
    flex-direction: column;
    font-size: 10px;
    gap: 2px;
  }
  .shell .nav .icon {
    font-size: 15px;
    width: auto;
  }
  .shell .workspace {
    padding-bottom: 64px;
  }
  .shell .topbar {
    height: 54px;
    padding: 0 15px;
  }
  .shell .main {
    padding: 20px 16px 35px;
  }
  .shell .main.legacy {
    padding: 16px 14px 32px;
  }
  .shell .topbar .nav-burger {
    display: inline-flex;
  }
  .home .pagehead {
    align-items: center;
  }
  .home h1 {
    font-size: 25px;
  }
  .home .kpis {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .home .create-content-grid {
    grid-template-columns: 1fr;
  }
  .home .create-card .card-head {
    flex-wrap: wrap;
  }
  .home .create-card .switch-tabs {
    order: 3;
    width: 100%;
  }
  .home .create-card .switch-tabs button {
    flex: 1;
  }
  .home .cash-summary {
    width: 100%;
    justify-content: flex-start;
  }
  .home .cash-summary > div {
    align-items: flex-start;
  }
  .home .activity-row {
    grid-template-columns: minmax(0, 1fr);
  }
  .home .activity-row .btn {
    grid-column: 2;
    justify-self: start;
  }
  .home .chart {
    gap: 7px;
  }
  .home .bar {
    width: 13px;
  }
}

/* --- ≤520px: the bar trims to five, everything else stacks ---------------- */

@media (max-width: 520px) {
  /* Companies and Reports leave the bar; both stay in the burger sheet */
  .shell .nav a:nth-child(5),
  .shell .nav a:nth-child(7) {
    display: none;
  }
  .shell .nav {
    grid-template-columns: repeat(5, 1fr);
  }
  .home .kpis {
    grid-template-columns: 1fr 1fr;
  }
  .home .tile {
    padding: 13px 11px;
  }
  .home .switch-tabs {
    width: 100%;
  }
  .home .switch-tabs button {
    flex: 1;
  }
  .home .selection-insight {
    grid-template-columns: 1fr;
  }
  .home .cash-summary {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 8px;
  }
  .home .chart-head {
    flex-wrap: wrap;
  }
  /* the six oldest months drop out so the bars stay readable on a phone */
  .home .chart .monthcol:nth-child(-n + 6) {
    display: none;
  }
  .home .chart {
    gap: 9px;
  }
  .home .bar {
    width: 16px;
  }
}

/* Touch devices: ≥44px targets (the shell's own controls) */
@media (pointer: coarse) {
  .shell .topbar .btn,
  .menu-nav a {
    min-height: 44px;
  }
}

/* ===========================================================================
   Reports — the analysis surface (golden 1440-reports-r13.png)

   The chart primitives are shared with `.home` above; everything below is the
   metric/dimension vocabulary this screen adds.
   =========================================================================== */

.reports .titlerow {
  display: flex;
  align-items: baseline;
  gap: 11px;
  flex-wrap: wrap;
}

.reports .titlerow .period-name {
  color: var(--muted);
  font-size: 15px;
  font-weight: 760;
}

.reports .titlerow .arr {
  color: var(--muted);
  text-decoration: none;
  font-size: 18px;
  line-height: 1;
  padding: 2px 6px;
  border-radius: 8px;
}

.reports .titlerow .arr:hover {
  background: var(--paper-2);
  color: var(--ink);
}

.reports .period-form {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
}

.reports .period-form select {
  appearance: auto;
  font-weight: 730;
}

.reports .metric-grid,
.reports .dimension-grid {
  display: grid;
  gap: 14px;
  margin: 0 0 18px;
}

.reports .metric-grid {
  grid-template-columns: repeat(4, minmax(0, 1fr));
}

.reports .dimension-grid {
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

.reports .tile {
  padding: 15px 13px 17px;
  display: block;
  text-decoration: none;
  color: inherit;
  min-width: 0;
}

.reports a.tile:hover {
  border-color: var(--faint);
  background: var(--paper-2);
}

.reports .tile.selected {
  border-color: var(--green);
  background: var(--green-softer);
  box-shadow: inset 0 0 0 1px var(--green);
}

.reports .tile .tile-head {
  display: flex;
  align-items: baseline;
  gap: 8px;
}

.reports .tile .tile-head .spacer {
  flex: 1;
}

.reports .tile .label {
  color: var(--muted);
  font-size: 12.5px;
  font-weight: 760;
  letter-spacing: 0.02em;
}

.reports .tile .groups {
  color: var(--faint);
  font-size: 12px;
  font-weight: 700;
  white-space: nowrap;
}

/* Same law as Home's tiles: the figure and `USD` are one token. Four columns
   survive down to 1101px here, where a tile holds 164px of content — the step
   is sized for `999,999.99 USD` there and reaches the R13 25px at ~1351px. */
.reports .tile .value {
  --tile-fs: clamp(17px, 1.85vw, 25px);
  font-size: var(--tile-fs);
  white-space: nowrap;
  font-weight: 830;
  margin: 7px 0 5px;
  letter-spacing: -0.01em;
}

/* two columns and then one: the tile is wide again, so is the face */
@media (max-width: 1100px) {
  .reports .tile .value {
    --tile-fs: 25px;
  }
}

.reports .tile .meta {
  color: var(--faint);
  font-size: 12px;
  line-height: 1.5;
  overflow-wrap: anywhere;
}

/* Overdue is the product's ONE bordeaux reading, on every screen that reads it
   (owner rule 2026-08-09: «the general rule — overdue is shown in bordeaux
   color»). Home has said so since m1-shell-home (`.home .tile.alert`) and the
   Invoices badge since the prototype; the same 142,799.98 printed `--ink` here
   and `--red` there. Scoped to `.metric-grid` so it cannot reach the six
   dimension tiles, which report a leader rather than a warning. */
.reports .metric-grid .tile.alert .value,
.reports .metric-grid .tile.alert .meta {
  color: var(--red);
}

/* …and the fourth column when it IS the overdue money. The green wash stays the
   selection affordance — it is shared with Outstanding, which is not a warning
   — so only the ink moves, and only while the table says `overdue`. */
.reports .report-table[data-metric="overdue"] th.on,
.reports .report-table[data-metric="overdue"] td.on {
  color: var(--red);
}

/* the quiet line under the dimension table that says what its columns count */
.reports .table-note {
  color: var(--faint);
  font-size: 12px;
  line-height: 1.6;
  margin: 9px 0 18px;
  max-width: 74ch;
}

.reports .usd-note {
  color: var(--muted);
  font-size: 12.5px;
  line-height: 1.65;
  font-variant-numeric: tabular-nums;
  margin: -6px 0 16px;
}

.reports .usd-note-label {
  display: block;
  color: var(--faint);
  font-size: 11px;
  font-weight: 760;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* filters: the toggle, the chips it sits with, and the panel it opens */

/* Reports wears the shared band too: its `.filterbar`/`.rsearch` pair became
   `.toolbar`/`.search`, and its chips and Reset are the same `.r13` rules
   every list screen uses. Only the disclosure panel below is its own. */

.reports .fpanel {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--paper-2);
  padding: 13px 15px;
  margin: 0 0 18px;
}

.reports .fpanel form {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 9px;
  margin: 0;
}

/* A label and the control it names are one thing, and wrap as one. */
.reports .fpanel .fpair {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  min-width: 0;
}

.reports .fpanel label {
  color: var(--muted);
  font-size: 12.5px;
  font-weight: 700;
  margin: 0;
}

/* The panel's controls are the band's controls: it used to size them two
   steps down (7px/13px against the band's 10px/14px), which made the one
   surface that is literally called "Filters" the one place a filter did not
   look like a filter. */
.reports .fpanel input.btn {
  border: 1px solid var(--line-control);
  border-radius: 10px;
  background: var(--paper);
  color: var(--ink);
  padding: 10px 13px;
  font: inherit;
}

/* the chart card + its legend */

/* Reports' chart is COMPACT — 180px (owner's pick from the rendered pair,
   2026-08-20; the alternative was half). Written HERE, on the Reports-only
   rule that already governed this chart, rather than as a new `.reports
   .chart` override: the shared `.home .chart, .reports .chart` rule above is
   Home's and stays untouched, and a second same-specificity selector would
   only have been decided by source order. Its 270px never reached this chart
   anyway — 260 did. Measured on both screens by
   `tests/e2e/test_chart_heights.py`, because Home losing its own height is
   exactly the accident one shared rule invites. */
.reports .chart-large {
  height: 180px;
}

/* The legend block dissolved into the card title (owner ruling 2026-08-20).
   Its swatches survive as chips inline in the `<h2>`, which is what stops the
   two from drifting: a separate legend once diverged into `--green-soft` and
   showed a near-black square beside mint bars in dark (2026-08-07 review).
   They read the SAME tokens the bars read, so that cannot happen again. */
.reports .chart-head h2 {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0 6px;
}

/* the chip belongs to the word after it, so the pair never splits across a
   line break — the legend's own 2026-08-04 rule. The MARKUP pairs them
   (`.series-pair`), because under flex-wrap a bare text run is its own item
   and a comment cannot hold two items together (2026-08-21). */
.reports .series-pair {
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  white-space: nowrap;
}

.reports .series-chip {
  width: 10px;
  height: 10px;
  border-radius: 3px;
  display: inline-block;
  /* an inline-block in a baseline row sits ON the baseline; the optical centre
     of a 10px square against 20px type is one pixel above it */
  align-self: center;
}

.reports .series-chip.issued {
  background: var(--bar-issued);
  border: 1px solid var(--green);
}

.reports .series-chip.collected {
  background: var(--green);
}

/* The chart's own explanation, now under the bars rather than in the head. */
.reports .chart-card .card-body > .sub {
  margin-top: 10px;
}

/* the section label above the dimension tiles */

.reports .section-label {
  display: flex;
  align-items: flex-end;
  flex-wrap: wrap;
  gap: 6px 16px;
  margin: 22px 0 13px;
}

.reports .section-label .sub {
  margin: 0 0 3px;
}

/* the dimension table */

.reports .table-scroll {
  overflow-x: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--paper);
}

.reports .report-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 14px;
  /* The card is the `.table-scroll` around it, which is themed. Without this
     the shared `table` primitive's hard #fff (style.css) painted a white slab
     behind transparent cells, so under the dark palette `Invoices`,
     `Collected` and `Outstanding` were near-white ink on white — three of the
     four columns invisible, and only the selected one and the Total legible.
     The entity lists closed this exact class (FORMS-02); this is Reports'. */
  background: transparent;
  box-shadow: none;
  border: 0;
}

.reports .report-table th,
.reports .report-table td {
  padding: 13px 16px;
  text-align: left;
  border-bottom: 1px solid var(--line-2);
  white-space: nowrap;
  background: transparent;
}

/* …except the group-name column: a label is prose, and one long client name
   forced the whole table into contained scroll while every numeric column
   stayed nowrap (2026-08-07 review). Wrapping the label costs one row its
   height; scrolling costs every column its visibility. */
.reports .report-table th:first-child,
.reports .report-table td:first-child {
  white-space: normal;
  overflow-wrap: anywhere;
}

.reports .report-table th {
  color: var(--muted);
  font-size: 11.5px;
  font-weight: 760;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.reports .report-table .num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* the selected metric's column, header and body alike */
.reports .report-table th.on,
.reports .report-table td.on {
  background: var(--green-softer);
  color: var(--green);
  font-weight: 780;
}

.reports .report-table tbody tr:hover td {
  background: var(--paper-2);
}

.reports .report-table tbody tr:hover td.on {
  background: var(--green-soft);
}

.reports .report-table td a {
  color: var(--green);
  font-weight: 730;
  text-decoration: none;
}

.reports .report-table td a:hover {
  text-decoration: underline;
}

/* The folded tail of a long dimension: one exact aggregate line, visually
   quieter than the named groups, its link expanding to the full table. */
.reports .report-table .rest-row td {
  color: var(--muted);
}

.reports .report-table .rest-row td a {
  color: var(--muted);
  font-weight: 640;
}

.reports .report-table tfoot td {
  border-bottom: 0;
  border-top: 1px solid var(--line);
  background: var(--paper-2);
  font-weight: 830;
}

.reports .report-table tfoot td.on {
  background: var(--green-soft);
}

@media (max-width: 1100px) {
  .reports .metric-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .reports .dimension-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 620px) {
  .reports .metric-grid,
  .reports .dimension-grid {
    grid-template-columns: 1fr;
  }
  .reports .pagehead {
    flex-wrap: wrap;
  }
  /* 150px, not the 190 it was: the desktop chart came down to 180 on
     2026-08-20, and a phone chart TALLER than the desktop one is not a
     narrow-screen adjustment — it is the adjustment inverted. The ratio to
     the desktop height is what this rule was always about, and it is kept. */
  .reports .chart-large {
    height: 150px;
  }
  /* the six oldest months drop out so the bars stay readable on a phone */
  .reports .chart .monthcol:nth-child(-n + 6) {
    display: none;
  }
  .reports .bar {
    width: 16px;
  }
}

/* ===========================================================================
   Templates — the document-style chooser (golden 1440-templates-r13.png)
   =========================================================================== */

.templates .doc-settings {
  margin: 0 0 18px;
}

.templates .toggle-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 12px;
  margin: 0;
  padding: 13px 0;
}

.templates .toggle-row + .toggle-row {
  border-top: 1px solid var(--line-2);
}

/* Where a setting went, said once, in the card the control used to sit in.
   Deliberately NOT a `.toggle-row`: that row is a flex container, and a
   sentence laid out as flex items gets a 12px gap wherever an inline element
   ends — the link came out standing a space away from its own semicolon. */
.templates .moved-setting {
  color: var(--muted);
  font-size: 13px;
  font-weight: 400;
  line-height: 1.55;
  margin: 0;
  padding: 13px 0;
}

.templates .moved-setting b {
  color: var(--ink);
  font-size: 13px;
  font-weight: 760;
}

/* the company link wears the app's own interactive colour, not UA blue */
.templates .moved-setting a {
  color: var(--green);
  text-decoration: underline;
  text-underline-offset: 2px;
}

.templates .moved-setting a:hover {
  text-decoration: none;
}

.templates .moved-setting + .toggle-row {
  border-top: 1px solid var(--line-2);
}

.templates .toggle-row .spacer {
  flex: 1;
}

/* The footer row's hint sits beside its Save button at full width, as the
   golden draws it. Once the row wraps, the hint and the button share the new
   line and the button ends up LEFT-aligned while the row above it is
   right-aligned — two Save buttons in one card that do not line up (measured
   at 620 and 430 on 2026-08-05: 227→378 vs 52→216). Below the wrap the hint
   takes a line of its own, so both buttons end at the card's right edge. */
@media (max-width: 720px) {
  .templates .toggle-row > .hint {
    flex: 1 1 100%;
    text-align: left;
  }
  /* the spacer that right-aligns the button stayed on the line above, so the
     button needs its own push once it is alone on a wrapped line */
  .templates .toggle-row > button {
    margin-left: auto;
  }
}

.templates .switch {
  display: flex;
  align-items: flex-start;
  gap: 11px;
  margin: 0;
  flex: 1 1 340px;
  cursor: pointer;
  font-weight: 400;
}

.templates .switch input {
  width: 17px;
  height: 17px;
  margin: 2px 0 0;
  accent-color: var(--green);
  flex: none;
}

.templates .switch b {
  display: block;
  color: var(--ink);
  font-weight: 760;
  font-size: 14px;
}

.templates .hint {
  display: block;
  color: var(--faint);
  font-size: 12.5px;
  line-height: 1.5;
}

/* the three style cards */

.templates .style-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 16px;
  align-items: start;
}

.templates .style-card.is-default {
  border-color: var(--green);
  box-shadow: inset 0 0 0 1px var(--green);
}

.templates .mini-preview {
  height: 232px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--paper);
  overflow: hidden;
  position: relative;
}

/* the miniature is a document, not a control: it never takes a click */
.templates .mini-preview iframe {
  width: 250%;
  height: 250%;
  border: 0;
  transform: scale(0.4);
  transform-origin: 0 0;
  pointer-events: none;
}

.templates .style-head {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  margin: 15px 0 4px;
}

.templates .style-head .spacer {
  flex: 1;
}

.templates .style-head h2 {
  margin: 0;
  font-size: 17px;
}

.templates .style-head .sub {
  margin: 2px 0 0;
  font-size: 12.5px;
}

.templates .badge {
  white-space: nowrap;
  flex: none;
  background: var(--paper-3);
  color: var(--muted);
}

.templates .badge.ok {
  background: var(--green-soft);
  color: var(--green);
}

.templates form {
  margin: 12px 0 0;
}

.templates label {
  display: block;
  color: var(--muted);
  font-size: 12.5px;
  font-weight: 720;
  margin: 12px 0 6px;
}

.templates input[type="text"] {
  width: 100%;
  border: 1px solid var(--line);
  background: var(--paper);
  color: var(--ink);
  border-radius: 10px;
  padding: 9px 11px;
  font: inherit;
  font-size: 13.5px;
}

.templates input[type="text"]:focus {
  outline: 0;
  border-color: var(--green);
  box-shadow: 0 0 0 3px var(--green-soft);
}

.templates .accent-palette,
.entity .accent-palette {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  align-items: center;
}

/* The ring is `--line`, not a fixed black alpha: three of sixteen swatches
   (#1F2937 at 1.05:1, #475569 at 2.04:1, #4F46E5 at 2.46:1) were invisible
   against the dark card, so a user could not choose — or tell they had chosen —
   a colour that prints on every invoice (CO-06). SC 1.4.11 asks 3:1 of a
   non-text UI component. 21x21 with a 6px gap also guaranteed mis-taps (CO-14). */
.templates .accent-palette .swatch,
.entity .accent-palette .swatch {
  width: 26px;
  height: 26px;
  border-radius: 6px;
  border: 1px solid var(--line-control);
  padding: 0;
  cursor: pointer;
}

@media (pointer: coarse) {
  .templates .accent-palette,
  .entity .accent-palette {
    gap: 10px;
  }

  .templates .accent-palette .swatch,
  .entity .accent-palette .swatch {
    width: 30px;
    height: 30px;
  }
}

.templates .accent-palette .swatch.sel,
.entity .accent-palette .swatch.sel {
  box-shadow: 0 0 0 2px var(--paper), 0 0 0 4px var(--green);
}

.templates .accent-hex,
.entity .accent-hex {
  flex-basis: 100%;
  color: var(--faint);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}

.templates .style-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: 8px;
  margin: 12px 0 0;
}

.templates .style-actions form,
.templates .style-actions > .btn {
  flex: 1 1 118px;
  margin: 0;
  min-width: 0;
}

/* Two captions share one card row, so the row is narrower than either caption
   at several widths — and `Set company default` painted straight out of its
   own rounded rect (owner 2026-08-04, "the caption does not fit the button"). The
   contract was the defect: a fixed-width button holding `white-space: nowrap`
   text. The caption wraps inside the control now, both buttons stretch to the
   taller of the two, and under ~244px of card the row itself wraps. The
   owner's captions are never shortened to fit a box. */
.templates .style-actions .btn {
  width: 100%;
  padding-inline: 10px;
  white-space: normal;
  line-height: 1.25;
  text-align: center;
}

.templates .btn.block {
  width: 100%;
  margin-top: 12px;
}

@media (max-width: 1100px) {
  .templates .style-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 700px) {
  .templates .style-grid {
    grid-template-columns: 1fr;
  }
}

/* ===========================================================================
   Entity screens — Clients, Products, Companies, Settings, Account
   (goldens 1440-clients-r13.png, 1440-companies-r13.png,
   1440-product-editor-r13.png). One scope, because these five are the same
   shape: a pagehead, an optional toolbar, a table or a form, and a dialog.
   =========================================================================== */

/* The toolbar, the chips and Reset are the shared `.r13` filter band. The
   entity screens are where the legacy `.chip` leaked through: nothing here
   defined one, so the pills fell back to style.css and painted #635bff on
   #f0f2ff — a palette this product has never used — in BOTH themes. */

/* --- tables -------------------------------------------------------------- */

.entity .table-scroll {
  overflow-x: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--paper);
}

.entity .entity-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 14px;
  background: transparent;
  box-shadow: none;
  border: 0;
}

.entity .entity-table th,
.entity .entity-table td {
  padding: 13px 16px;
  text-align: left;
  border-bottom: 1px solid var(--line-2);
  vertical-align: middle;
  background: transparent;
}

.entity .entity-table th {
  color: var(--muted);
  font-size: 11.5px;
  font-weight: 760;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  /* captions may wrap: every th nowrap made ~630px of rigid columns and the
     one flexible column — the record's NAME — paid for all of them (108px at
     780px viewport while "Used by (clients)" held 167px for 2 digits;
     2026-08-07 review). A two-line caption is information kept; a 108px
     identity column is information lost. */
}

/* the identity column is what a row IS — it never shrinks below readable.
   183 since round 4: the 36px kebab took 17px more of the actions column and
   the 1141–1272 band has no slack — left at 200 the Clients table scrolled
   19px at 1141 and 26px at 1181. */
.entity .entity-table td[data-card="title"] {
  min-width: 183px;
}

/* In card mode the CARD is the surface: the scroller's own frame drew a stray
   rule above the first card with no sides or bottom to belong to. The card
   frame must move with the cards themselves — style.css carries the fit
   point's arithmetic (1046 since round 4's 36px kebab, a 17px gutter included
   since round 2b item 7). */
@media (max-width: 1046px) {
  .entity .table-scroll:has(table.list-cards) {
    border: 0;
    border-radius: 0;
    background: transparent;
    overflow: visible;
  }
}

/* A sortable header carries its own affordance and its own state — a pointer
   cursor over a header that does nothing (`Actions`) teaches distrust, and an
   interaction with no glyph is one most users never find (P18/CO-11). */
.entity .entity-table th[data-sort] {
  cursor: pointer;
  user-select: none;
}

.entity .entity-table th[data-sort]::after {
  content: "▲";
  /* 3px/7px, not 5px/8px: the PERMANENT glyph is part of every caption's
     rigid minimum — six columns of it were ~24px of table the 1024 band
     could not afford (2026-08-07 container-scroll invariant) */
  margin-left: 3px;
  font-size: 7px;
  color: var(--line);
  vertical-align: middle;
}

.entity .entity-table th[data-sort]:hover::after {
  color: var(--faint);
}

.entity .entity-table th[data-sort][aria-sort="ascending"]::after {
  content: "▲";
  color: var(--green);
}

.entity .entity-table th[data-sort][aria-sort="descending"]::after {
  content: "▼";
  color: var(--green);
}

.entity .entity-table th[data-sort]:focus-visible {
  outline: 2px solid var(--green);
  outline-offset: -2px;
}

/* CO-13: the `Actions` caption sat 68px to the LEFT of the only thing it
   labels, because the cell is right-aligned and the header was not. */
.entity .entity-table th:last-child {
  text-align: right;
}

.entity .entity-table th.num,
.entity .entity-table td.num {
  text-align: right;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}



.entity .entity-table tr:last-child td {
  border-bottom: 0;
}

/* The live search hides rows with [hidden] (shell.js). Here the UA default is
   enough; on a phone `table.list-cards tr` is `display: block` and would win,
   so the card rule below restates it. Same for the two containers the narrowing
   swaps between — a table with no rows left, and the note that says so. */
.entity .entity-table tr[hidden],
.entity .table-scroll[hidden],
.entity .empty[hidden] {
  display: none;
}

.entity .entity-table tr:hover td {
  background: var(--paper-2);
}

.entity .entity-table td b {
  display: block;
  font-weight: 760;
  color: var(--ink);
}

.entity .entity-table td .meta {
  display: block;
  color: var(--faint);
  font-size: 12px;
  margin-top: 2px;
  /* The clamp the server's truncate(64) used to fake: one line, ellipsised,
     with the full text in the cell's title (2026-08-07 review). 240px, not
     64ch: a nowrap line's max-width IS its intrinsic contribution to the
     auto-layout table, and 64ch (≈460px) made the table's minimum wider
     than the content column at 1024 — the container-scroll invariant caught
     the sweep's own first draft (and the pre-change truncate(64) rows had
     been overflowing there unchecked all along). */
  max-width: 240px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The 901–1140 band: the sidebar still stands (it leaves at 900) and the
   shell cap does not, so the content column is at its narrowest — ~748px at
   a 1024 viewport. The columns' honest minimums only fit it with tighter
   cell padding and a shorter meta clamp; both relax back above 1140. */
/* The meta clamp releases where the RELAXED table actually fits, not where
   the padding does. Both used to end at 1140, and the table's rigid minimum
   grew 62px in the same pixel the viewport grew by one: from 1141 the clamp
   went 150 → 240px, the table needed 927px of content, and it did not get it
   until ~1254 — so /clients scrolled sideways across a 113px band of ordinary
   laptop widths that the sweep had never sampled (it jumped 1024 → 1280).
   Measured on the checker book: 62px over at 1141, 43 at 1160, 13 at 1240,
   clean from 1254; a classic scrollbar also puts the shipped demo book 5px
   over at 1141. 1272 = the overlay edge plus a 17px bar. */
@media (max-width: 1272px) {
  .entity .entity-table td .meta {
    max-width: 150px;
  }
}

/* The 36px kebab took 17px more of the actions column (round 4), and at 1280
   with a real 17px scrollbar that was 5px more than the band had: measured
   holder 934, table 939. The tightening the ≤1140 band already applies to the
   ACTIONS cell alone is enough here — 12px back, no other column touched, and
   the identity column keeps every pixel it had. */
@media (min-width: 1141px) {
  .entity .entity-table td.row-actions,
  .entity .entity-table th:last-child {
    padding-left: 10px;
    padding-right: 10px;
  }
}

@media (max-width: 1140px) {
  .entity .entity-table th,
  .entity .entity-table td {
    padding-left: 10px;
    padding-right: 10px;
  }
  /* a numeric CELL never wraps; in this band its HEADER may — `Outstanding
     (USD)` and `Used by (clients)` held 175/167px of rigid column for
     two-digit values, which is what pushed the table into container scroll
     at 1024. Above the band there is room and the headers stay on one line
     (a two-line header everywhere blew the vertical chrome budget). */
  .entity .entity-table th.num {
    white-space: normal;
    overflow-wrap: normal;
  }
  /* the floor gives back 8px in this band (the "~" in ~200px): the columns'
     honest minimums land at 747 of the 748 the band affords */
  .entity .entity-table td[data-card="title"] {
    min-width: 175px;      /* the same 17px, the same reason as above */
  }
  /* the matrix cell's own answer for a name the band cannot hold on three
     lines (a 67-char demo product needs ~230px for that; the band affords
     172): one line, ellipsised, the whole name in the title */
  .entity .entity-table td[data-card="title"] .entity-name {
    display: inline-block;
    max-width: 168px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    vertical-align: bottom;
  }
}

/* R6 — one row, one baseline (the owner's own finding Р6, 2026-08-09; decided
   from a rendered pair on 2026-08-20).

   An entity row carries two weights side by side: the client's name at 760 and
   the button captions at 730, against the ordinary cells — Country, Terms,
   Open, Outstanding — at 400. Same 13px, same 20.15px line height, and the
   line boxes measure identical: the heavy faces of the system font simply put
   their INK a pixel lower. No rule on a box can move ink, which is why every
   padding reading of this was wrong.

   So the ordinary cells come DOWN to meet the heavy text, and that is the
   owner's choice among three: nudging the BUTTON up would have aligned it with
   the figures and parted it from the name — trading one misalignment for
   another — while this aligns the whole row at once. The cost is that the rule
   is wider than one control, which is the honest price of the row being right.

   Table band only. Below 1047 the row is a card: the cells stack, and there is
   no shared baseline left to hold. */
@media (min-width: 1047px) {
  .entity .entity-table td:not([data-card="title"]):not(.row-actions) {
    position: relative;
    top: 1px;
  }
}

.entity .entity-table td a:not(.btn) {
  color: var(--green);
  font-weight: 730;
  text-decoration: none;
}

/* The row's actions are a fixed set of controls, so their column takes the
   width it needs and the identity column takes the rest — `width: 1%` on an
   unwrappable cell is auto table layout's way of saying "as narrow as your
   content". Without it a long client name (the demo's 68-character
   `Northgate … — Accounts Payable, EMEA`) grew the identity column until the
   actions cell was squeezed to 172px, the kebab wrapped under the two buttons,
   and EVERY row on Clients and Products stood 92px tall instead of 68px — one
   long name costing 24px on every row of the list. */
.entity .entity-table td.row-actions {
  width: 1%;
  white-space: nowrap;
}

/* Terms is a closed vocabulary of three short labels, and the longest of them
   is two words: with the actions column now taking its own width, `Due on
   receipt` was the first cell to lose the room to stay on one line. A label
   from a fixed set is not prose — it gets the width it needs. */
.entity .entity-table td[data-th="Terms"] {
  white-space: nowrap;
}

.entity .row-actions {
  display: flex;
  justify-content: flex-end;
  flex-wrap: wrap;
  gap: 6px;
}

.entity .row-actions form {
  margin: 0;
}

/* A `td` that becomes a flex container stops being a table cell: the table
   wraps it in an anonymous cell, so the row's height belongs to that wrapper
   while the border-bottom is still painted on this shorter box. Every row on
   Clients and Products was drawn with two separators a few pixels apart — the
   prototype has one line across the row. The cell stays a cell; its contents do
   the aligning. Below the card fit point the same `td` is a card field
   (`table.list-cards`), where the table is `display: block` and flex is
   exactly right. It moves with the other two thresholds: left behind, this
   rule's `text-align: right` leaks into card mode and pushes the card's
   action row to the right edge, where today's cards align left. */
@media (min-width: 1047px) {
  .entity .entity-table td.row-actions {
    display: table-cell;
    text-align: right;
  }

  .entity .entity-table td.row-actions > *,
  .entity .entity-table td.row-actions form {
    display: inline-block;
    vertical-align: middle;
  }
}

/* --- empty states -------------------------------------------------------- */

.entity .empty {
  border: 1px dashed var(--line);
  border-radius: var(--radius);
  background: var(--paper-2);
  color: var(--muted);
  padding: 34px 22px;
  text-align: center;
}

.entity .empty p {
  margin: 0 0 14px;
}

/* --- forms and cards ----------------------------------------------------- */

.entity .card {
  padding: 0;
  margin: 0 0 18px;
}

.entity .card > h2:first-child {
  margin: 0;
  padding: 15px 18px 0;
}

.entity .form-card {
  padding: 18px;
}

.entity label {
  display: block;
  color: var(--muted);
  font-size: 12.5px;
  font-weight: 720;
  margin: 14px 0 6px;
}

/* Every text-ish control the app renders. `month` was missing, so the closed-
   period field on the company form kept the UA's white box under the dark
   palette (measured 2026-08-05) — the same class of gap as the country panel
   and the dimension table, one input at a time. The list is the inventory of
   `type="…"` actually used in `templates/`, so adding a control means adding
   it here too; `checkbox`/`radio`/`file`/`submit`/`button`/`hidden` are
   deliberately absent — they are not text boxes. */
.entity input[type="text"],
.entity input[type="search"],
.entity input[type="email"],
.entity input[type="password"],
.entity input[type="date"],
.entity input[type="month"],
.entity input[type="number"],
.entity input:not([type]),
.entity textarea,
.entity select {
  border: 1px solid var(--line-control);
  background: var(--paper);
  color: var(--ink);
  border-radius: 10px;
  padding: 10px 12px;
  font: inherit;
  /* one editable-text size for the whole app: the dialogs inherit 14px and the
     entity forms were 13.5px, the smallest type in the product sitting in the
     longest form in it */
  font-size: 14px;
  /* the `md` step — the same box the buttons beside these fields already are */
  min-height: var(--ctl);
  max-width: 100%;
}

.entity textarea {
  width: 100%;
  min-height: 0;
}

/* The country picker is a shell.js combobox now (the only one on this scope):
   the wrapper replaces the <select> in the flow, so the trigger takes the width
   the select had. */
.entity .combo > .combo-input {
  width: 100%;
}

/* …and the same lesson on the other half of the form vocabulary. A disabled
   INPUT rendered pixel-identical to a live one, so the only sign that typing
   would do nothing was that typing did nothing. Settings' Rates card made that
   visible twice over (currency-change-pass): the pair fields are disabled
   while rates are online — a disabled input is never posted, which is what
   stops a save writing live quotes over the owner's own book — and `Rate
   markup` is disabled while they are manual, because a markup applies to
   fetched quotes and there is no quote (owner 2026-08-14: disabled in place,
   visibly, beside the rates it no longer applies to). */
.entity input:disabled,
.entity textarea:disabled,
.entity select:disabled {
  background: var(--paper-2);
  color: var(--faint);
  border-color: var(--line);
  cursor: not-allowed;
}

.entity input:focus,
.entity textarea:focus,
.entity select:focus {
  outline: 0;
  border-color: var(--green);
  box-shadow: 0 0 0 3px var(--green-soft);
}

.entity input[type="checkbox"] {
  width: 17px;
  height: 17px;
  accent-color: var(--green);
  box-shadow: none;
}

.entity .muted,
.entity .hint {
  color: var(--muted);
}

.entity code {
  background: var(--paper-3);
  border-radius: 5px;
  padding: 1px 5px;
}

/* a tapped month shows its values (round 1, C17d): same tip hover shows */
.home .monthcol.tip-on .tip,
.reports .monthcol.tip-on .tip {
  opacity: 1;
  visibility: visible;
}

/* the plaque's receipt line (round 1 item 23). The chip is one unbreakable
   unit: at 390 it wrapped mid-hash and stretched the badge over two lines
   (verifier nit at 219ca04) — as an inline whole it drops to its own line
   instead. Pinned by layout_check's chip-wrap probe. */
.dev-review .dev-receipt.ok { color: var(--green); font-weight: 730; }
.dev-review .dev-receipt.stale { color: var(--red); font-weight: 760; }
.dev-review .dev-receipt { white-space: nowrap; display: inline-block; }

/* --- the breakdown toggle (owner review round 1, C11) --------------------- */

.bd-toggle {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: inherit;
  cursor: pointer;
  text-decoration: underline dotted var(--faint);
  text-underline-offset: 3px;
}

/* The toggle is text, not a button pill: legacy style.css's bare
   `button:hover` (0-1-1) outguns the base reset above (0-1-0) and painted
   the pointer state `--panel` white — unreadable in dark, where that legacy
   token never themes. The hover affordance is the underline alone. */
.bd-toggle:hover {
  background: none;
  text-decoration-color: var(--green);
}

/* --- the "?" hint (owner review round 1, B9) ------------------------------ */

.lt-hint {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  margin-left: 6px;
  border: 1px solid var(--line);
  border-radius: 50%;
  font-size: 11px;
  font-weight: 760;
  color: var(--faint);
  cursor: help;
  position: relative;
}

.lt-hint:hover::after,
.lt-hint:focus::after {
  content: attr(data-hint);
  position: absolute;
  left: 50%;
  bottom: calc(100% + 8px);
  transform: translateX(-50%);
  z-index: var(--z-popover);
  width: max-content;
  max-width: 320px;
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--paper);
  color: var(--ink);
  font-size: 12.5px;
  font-weight: 450;
  box-shadow: var(--shadow);
  white-space: normal;
}

/* --- row buttons and band heights (owner review round 1, C14/C16/C18) ----- */

/* C18: the label is CENTERED in the box — inline-block + line-height sat the
   text low in Invoices/Edit on every list */
.entity .row-actions .btn,
.invoices .rowacts .btn,
.invoices .cardacts-btns .btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/* C14: View/PDF lose horizontal fat… */
.invoices .rowacts .btn.sm {
  padding-left: 9px;
  padding-right: 9px;
}

/* …and the per-row PDF slides into the kebab as the list tightens: exactly
   one of the two entries shows at any width */
.invoices .menu-list .menupdf {
  display: none;
}

@media (max-width: 1240px) {
  .invoices .rowacts .rowpdf {
    display: none;
  }
  .invoices .menu-list .menupdf {
    display: block;
  }
}

/* C16: one control height per band row — the export group's buttons matched
   the 36 scale while every select beside them stood 44 */
.r13 .listacts .btn,
.r13 .listacts .bulk-dl {
  min-height: var(--ctl);
}

/* C12 (round 1): at the 901-1140 band the composer's line table is cramped —
   Qty yields, Description keeps the space, and the live preview pane narrows
   ~20% so the form column breathes */
/* bounded below at 821: under that the composer reflows to stacked rows with
   per-field captions, and this five-track template must not override it */
@media (min-width: 821px) and (max-width: 1140px) {
  .composer-dialog .line-table.qty .line-head,
  .composer-dialog .line-table.qty .line-row {
    /* the band that was meant to RELIEVE the cramping tightened Amount
       104 → 96 and made the wrap arrive a step earlier; Qty pays instead.
       Rate stays at 92: at 84 an eight-figure rate clipped inside its own
       input, which is the same defect one column left. */
    grid-template-columns: minmax(0, 1fr) 48px 92px 130px 34px;
  }
  .composer-grid {
    grid-template-columns: minmax(0, 1fr) minmax(300px, 26%);
  }
}

/* --- the discard confirm panel (owner review round 1, B2) ----------------- */

.confirm-dialog {
  width: min(92vw, 420px);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--paper);
  color: var(--ink);
  padding: 22px;
  box-shadow: var(--shadow);
}

.confirm-dialog::backdrop {
  background: rgba(10, 16, 14, 0.45);
}

.confirm-dialog h2 {
  margin: 0 0 8px;
  font-size: 17px;
  font-weight: 780;
}

.confirm-dialog p {
  margin: 0 0 18px;
  color: var(--muted);
  font-size: 13.5px;
}

.confirm-actions {
  display: flex;
  gap: 10px;
  align-items: center;
}

.confirm-dialog .btn-danger {
  border: 1px solid var(--red);
  background: var(--red-soft);
  color: var(--red);
  border-radius: 10px;
  padding: 8px 13px;
  font: inherit;
  font-weight: 760;
  cursor: pointer;
  min-height: var(--ctl-sm);
}

/* --- dialogs (the client and product editors) ---------------------------- */

/* The R13 dialog rule bodies above already reach `.entity-dialog`; this is what
   is specific to it. (Its WIDTH is declared beside the other dialogs' — it has
   to precede the phone media block, or the sheet rule loses on source order.) */

/* A field that carries a sentence, not a value, takes the whole row. */
.entity-dialog .form-grid .field.wide {
  grid-column: 1 / -1;
}

/* the entity scope's own label rule (14px top margin, for a stacked page form)
   must not reach a dialog laid out on `.field` boxes */
.entity-dialog label {
  margin: 0 0 6px;
}

.entity-dialog .modal-foot .btn-primary {
  min-height: var(--ctl);
}


.entity dialog:not(.entity-dialog) {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--paper);
  color: var(--ink);
  box-shadow: var(--shadow);
  width: min(620px, 94vw);
}

.entity dialog .modal-head,
.entity dialog .modal-foot {
  border-color: var(--line-2);
}

.entity dialog .modal-head h2 {
  font-size: 19px;
  font-weight: 830;
}

.entity dialog .modal-close {
  color: var(--muted);
}

.entity dialog .modal-close:hover {
  color: var(--ink);
}

.entity dialog .btn-primary,
.entity .btn-primary {
  background: var(--green);
  border-color: var(--green);
  color: var(--on-green);
  border-radius: 10px;
  font-weight: 760;
}

.entity dialog .btn-primary:hover,
.entity .btn-primary:hover {
  background: var(--green-dark);
  border-color: var(--green-dark);
}

.entity .error {
  border: 1px solid var(--red);
  background: var(--red-soft);
  color: var(--red);
  border-radius: var(--radius-sm);
  padding: 10px 13px;
}

/* `.entity .ok` and `.home .ok` styled the old top-of-content acknowledgement
   bar and went with it (R4-1). `.ok` itself stays in `style.css`: the register
   screen still uses it for a paragraph, which is a different element with a
   different job. */

/* The flash toast — the acknowledgement, out of flow (owner 2026-08-15).
   `position: fixed` is the whole fix: out of flow, the page cannot be pushed
   down when the message appears or springs back when it leaves, and the
   message is on the reader's screen wherever they have scrolled to. */
.flash-toast {
  position: fixed;
  right: 20px;
  /* The UPPER part of the screen (owner 2026-08-15). Clear of the 62px
     topbar rather than over it: a message that covers the appearance
     control has taken a control away for half a minute. */
  top: 74px;
  z-index: var(--z-toast);
  display: flex;
  align-items: flex-start;
  gap: 10px;
  max-width: 380px;
  padding: 11px 12px 11px 14px;
  border: 1px solid var(--green);
  background: var(--green-soft);
  color: var(--green);
  border-radius: var(--radius-sm);
  box-shadow: var(--card-shadow);
  /* Two animations on purpose. The first carries the ARRIVAL —
     opacity and the slide, over .22s. The second carries only
     COLOUR, over a full second, so the toast keeps settling long
     after it has finished moving: it lands saturated enough to
     catch the eye and cools to the resting tint by itself. They
     touch no shared property, so neither fights the other. */
  animation: flash-in .22s ease-out both,
             flash-settle 1s ease-out both;
}

.flash-toast .flash-text {
  /* a message can name a client or an invoice mask, so it wraps rather than
     stretching the toast across the viewport */
  overflow-wrap: break-word;
  min-width: 0;
}

.flash-toast .flash-x {
  flex: none;
  padding: 0 2px;
  border: 0;
  background: none;
  color: inherit;
  font-size: 17px;
  line-height: 1.05;
  cursor: pointer;
  opacity: .75;
}

.flash-toast .flash-x:hover {
  opacity: 1;
}

.flash-toast .flash-x:focus-visible {
  outline: 2px solid var(--green);
  outline-offset: 2px;
  opacity: 1;
}

.flash-toast.leaving {
  animation: flash-out .35s ease-in both;
}

/* The same toast, saying the opposite thing (failure-telemetry, 2026-08-17):
   a background fetch failed. Same geometry, same two ways out, same timer —
   only the colour differs, and it differs with the tokens `.error-card` and
   `.home .error` already use, so the app has one red rather than a second one.
   The settle animation is dropped: it exists to cool an acknowledgement to its
   resting tint, and a failure has nothing to cool to. */
.flash-toast.failed {
  border-color: var(--red);
  background: var(--red-soft);
  color: var(--red);
  animation: flash-in .22s ease-out both;
}

.flash-toast.failed .flash-x:focus-visible {
  outline-color: var(--red);
}

@keyframes flash-in {
  from { opacity: 0; transform: translateY(-10px); }
  to   { opacity: 1; transform: none; }
}

@keyframes flash-out {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: translateY(-10px); }
}

/* The flash itself: saturated on arrival, cooling to the resting palette.
   Ends on exactly the values the rule above declares, so the toast is in its
   normal state for the other 29 seconds and the animation can be removed
   without changing what the reader ends up looking at. */
@keyframes flash-settle {
  from {
    background: var(--flash-peak-bg);
    color: var(--green-dark);
    box-shadow: 0 6px 20px rgb(0 0 0 / 16%);
  }
  to {
    background: var(--green-soft);
    color: var(--green);
    box-shadow: var(--card-shadow);
  }
}

/* Narrow screens: the toast spans the width rather than hanging off one
   corner at 380px on a 360px viewport. */
@media (max-width: 480px) {
  .flash-toast {
    left: 12px;
    right: 12px;
    top: 66px;                    /* the topbar is 54px on this band */
    max-width: none;
  }

  /* Five tabs do not fit 390px: the last one painted 22px outside the card
     (found 2026-08-16, by teaching the layout sweep to measure an element
     against the box that holds it). The strip scrolls instead — the same
     answer the tables give, and the one that keeps a tab strip one line. */
  .home .activity-tabs {
    overflow-x: auto;
    scrollbar-width: none;
  }

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

  .home .activity-tabs button { flex: 0 0 auto; }
}

/* A reader who asked for less motion still gets the message and the fade —
   what goes is the travel. */
@media (prefers-reduced-motion: reduce) {
  /* The travel goes; the colour settle stays. A reader who asked for less
     MOTION did not ask for less message, and a cross-fade is not motion. */
  .flash-toast {
    animation: flash-settle 1s ease-out both;
  }

  .flash-toast.leaving {
    animation-duration: .01ms;
  }

  @keyframes flash-in {
    from { opacity: 0; }
    to   { opacity: 1; }
  }

  @keyframes flash-out {
    from { opacity: 1; }
    to   { opacity: 0; }
  }
}

@media (max-width: 900px) {
  .entity .table-scroll {
    border: 0;
    background: transparent;
  }
}

/* the product editor's line grid — Description · Qty · Rate · Amount, the same
   four columns the composer and the Review row editor use (m1-domain) */

.entity .item-head,
.entity .item-row {
  display: grid;
  grid-template-columns: 20px minmax(0, 1fr) 78px 104px 104px;
  gap: 8px;
  align-items: center;
}

.entity .item-head {
  margin: 8px 0 6px;
  padding: 8px 10px;
  border-radius: var(--radius-sm);
  background: var(--paper-3);
  color: var(--muted);
  font-size: 11px;
  font-weight: 760;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.entity .item-head span:nth-child(n + 3) {
  text-align: right;
}

.entity .item-row {
  margin: 6px 0;
}

.entity .item-row input {
  width: 100%;
  min-width: 0;
}

.entity .item-row input.qty,
.entity .item-row input.amt {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.entity .item-row .idx {
  color: var(--faint);
  font-size: 12px;
  text-align: right;
}

/* Amount is the server's figure, never a field */
.entity .item-row output.line-amount {
  text-align: right;
  font-variant-numeric: tabular-nums;
  font-weight: 760;
  color: var(--ink);
  padding-right: 4px;
}

/* FORMS-04, the product editor's turn: 286px of fixed qty/rate/amount tracks
   plus the index left the description a 0-px track on a phone, so the header
   read "DESCRIPTIQTY" and the description input vanished under Qty. Same
   breakpoint and same reflow as the composer's F-10 rule above — description
   first, the numbers on their own line under the numeric header — but it
   cannot be the *same* rule: the two editors are different markup (this one
   has an index cell and an `output` amount, the composer has a remove button
   and `name$=` hooks) in different scopes. Same shape, not a second idea. */
@media (max-width: 700px) {
  .entity .item-head,
  .entity .item-row {
    grid-template-columns: 20px minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1.2fr);
  }

  /* the head labels the numeric line; the description needs no label above an
     input that carries its own placeholder */
  .entity .item-head span:nth-child(2) {
    display: none;
  }

  .entity .item-row {
    grid-template-areas:
      "idx desc desc desc"
      "idx qty  rate amount";
    row-gap: 6px;
  }

  .entity .item-row .idx { grid-area: idx; align-self: center; }
  .entity .item-row input.desc { grid-area: desc; }
  .entity .item-row input.qty { grid-area: qty; }
  .entity .item-row input.amt { grid-area: rate; }
  .entity .item-row output.line-amount { grid-area: amount; }
}

/* --- entity forms: Companies, Settings, Account --------------------------- */

.entity .form-column {
  max-width: 720px;
}

.entity .card.form-card {
  padding: 18px 20px 20px;
  margin: 0 0 16px;
}

.entity .form-card > h2:first-child {
  margin: 0 0 4px;
  padding: 0;
  font-size: 17px;
}

.entity .form-card h2 {
  margin: 22px 0 2px;
  font-size: 17px;
}

/* A second group inside one form-card: Settings' single Save covers both the
   rates and the two install-only options, and `Rates` over a port number is a
   heading that does not describe what is under it (screen pass 2026-08-05).
   Quieter than the card's own h2, and it earns a rule above it. */
.entity .form-card .group-label {
  margin: 26px 0 2px;
  padding-top: 18px;
  border-top: 1px solid var(--line-2);
  font-size: 13px;
  font-weight: 800;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.07em;
}

/* A checkbox with its caption on one line — was three inline styles. */
.entity .form-card .checkline {
  display: flex;
  align-items: center;
  gap: 8px;
  font-weight: 400;
  margin-top: 12px;
}

.entity .form-card .checkline input {
  width: auto;
}

/* A default's own sentence, under the box it explains and aligned with the
   caption rather than with the checkbox. */
.entity .form-card .opt-hint {
  margin: 4px 0 0 26px;
  font-size: 12px;
}

/* Appearance — the theme control, moved off the topbar and onto this screen
   (owner 2026-08-04: "the theme switch moves into Settings (let it sit at the top
   sits nicely highlighted)"). A real segmented control: three cards, each
   painting the palette it selects, so the choice is visible before it is
   made. Both palettes reach it because the swatches are literal colours, not
   tokens. */
.entity .appearance-card {
  border-color: var(--green-rim);
  background: linear-gradient(145deg, var(--paper), var(--green-softer));
}

.entity .appearance-head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 6px 14px;
}

.entity .appearance-head .spacer {
  flex: 1;
}

.entity .appearance-head .eyebrow {
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--faint);
}

.entity .appearance-head .sub {
  color: var(--muted);
  font-size: 12.5px;
}

.entity .theme-choice {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 10px;
  margin: 14px 0 0;
}

.entity .theme-option {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 3px;
  text-align: left;
  padding: 12px 13px 13px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--paper);
  color: var(--ink);
  font: inherit;
  cursor: pointer;
  min-width: 0;
}

.entity .theme-option:hover {
  border-color: var(--green);
}

.entity .theme-option.active {
  border-color: var(--green);
  background: var(--green-soft);
  box-shadow: inset 0 0 0 1px var(--green);
}

.entity .theme-option b {
  font-size: 14px;
  font-weight: 780;
}

.entity .theme-option .hint {
  font-size: 11.5px;
  color: var(--faint);
  line-height: 1.35;
}

.entity .theme-swatch {
  width: 100%;
  height: 34px;
  border-radius: 8px;
  border: 1px solid var(--line);
  margin-bottom: 6px;
}

/* literal colours: a swatch has to show the palette it picks, not the one in
   use — `light` must stay light while the app is dark, and the other way round */
.entity .theme-swatch.light {
  background: linear-gradient(135deg, #ffffff 0 55%, #ecebe6 55% 100%);
}

.entity .theme-swatch.dark {
  background: linear-gradient(135deg, #1a2723 0 55%, #111b18 55% 100%);
  border-color: #35443f;
}

.entity .theme-swatch.system {
  background: linear-gradient(135deg, #ffffff 0 50%, #1a2723 50% 100%);
}

@media (max-width: 560px) {
  .entity .theme-choice {
    grid-template-columns: 1fr;
  }
  .entity .theme-swatch {
    height: 26px;
  }
}

.entity .form-card.danger {
  border-color: var(--red);
}

.entity .form-card.danger h2 {
  color: var(--red);
}

/* CO-01, the owner's "just try to find the Save button". Save sat at document
   y=2230 on a 2732px page — 1,374px of scrolling and 46 Tab presses from the
   first field — with 391px of content BELOW it, ending on `Delete company`.
   The commit action is on screen at every scroll position now, and nothing
   renders below it (§1: the footer always carries the primary actions). The
   same class serves Settings, so both are fixed once. */
/* A DIRECT child of the page: the company editor's one commit bar, which owns
   the whole form. Settings keeps its in-card action rows untouched — that page
   has several cards each with its own Save, and a single page-level commit bar
   would claim to save all of them. (Scoping matters structurally too: the
   negative margins below are sized to `.shell .main`'s padding and would push a
   bar nested in a card outside it — measured as a horizontal overflow on
   Settings before this selector was narrowed.) */
.entity > .form-actions {
  position: sticky;
  bottom: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  gap: 10px;
  /* full-bleed across the main column (the negative inline margins undo
     `.shell .main`'s padding), on the same `--paper-2` surface every dialog
     footer uses — a commit bar reads as chrome, not as a card */
  margin: 18px clamp(-42px, -3vw, -22px) -50px;
  padding: 14px clamp(22px, 3vw, 42px);
  background: var(--paper-2);
  border-top: 1px solid var(--line);
}

.entity > .form-actions .spacer {
  flex: 1;
}

/* the in-card rows Settings uses keep what they had */
.entity .form-card .form-actions {
  margin: 0 0 4px;
}

/* A form's default submit, clipped rather than removed (R3-6).
   Implicit submission fires a form's FIRST submit button, so a card whose
   only submit is a secondary verb hands the Enter key to that verb — which
   is how Enter in the Rates markup field refreshed the book instead of
   saving it. Clipped, not `display:none` or `hidden`: a hidden control is
   not a submitter at all, and this one has to stay one. It also stays
   focusable and announced, so the keyboard has a Save on a card whose mouse
   affordance is autosave. */
.entity .form-card .default-submit {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* A section that writes the moment its own button is pressed says so — five
   buttons on this page commit immediately while one Save owns the fields, and
   nothing told the reader which was which (CO-12). */
.entity .form-card h2 .section-note,
.entity .form-card > h2 .section-note {
  margin-left: 8px;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--faint);
}

.entity .inline-form {
  display: flex;
  align-items: flex-end;
  gap: 10px;
  flex-wrap: wrap;
}

/* CO-07: the wrapper was a bordered 678px box holding a 360px table — 47% of a
   card-in-card frame empty, with the header rule stopping in mid-air. The table
   fills its own card; there is no scroller and no second frame. */
.entity .locked-months {
  margin-top: 12px;
  width: 100%;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  overflow: hidden;
}

.entity .btn-danger {
  border: 1px solid var(--red);
  background: var(--red-soft);
  color: var(--red);
  border-radius: 10px;
  padding: 8px 13px;
  font: inherit;
  font-weight: 760;
  cursor: pointer;
}

.entity .btn-danger:hover {
  background: var(--red);
  color: #fff;
}

.entity .auth-form label {
  margin-top: 14px;
}

/* The auth form wraps each input INSIDE its label. `.entity label` is a block,
   but a bare `<input>` inside it is inline, so `Current password` and `Confirm
   new password` sat on the same line as their boxes while `New password` —
   whose input is wrapped in the flex `.pw-field` — sat above its own. Three
   fields, three different label placements and three different widths, on one
   card (Account screen pass 2026-08-05). The input takes its own line like the
   flex one, so all three read alike. */
.entity .auth-form label > input {
  display: block;
  margin-top: 6px;
}

.entity .auth-form input {
  width: 100%;
  max-width: 380px;
}

/* `style.css` gives an auth submit `width: 100%`, which is the idiom of the
   narrow centred login card. Inside the Account screen the same class sits in
   a wide form-card, where it stretched `Change password` to 678px while `Save`
   on Settings and `Log out everywhere else` on this very screen are their own
   width. The public pages keep the full-width rule; this scope does not. */
.entity .auth-form button[type="submit"] {
  /* the form is a flex column, so `width: auto` alone is still stretched */
  width: auto;
  align-self: flex-start;
}

.entity .pw-field {
  display: flex;
  gap: 8px;
  align-items: center;
  max-width: 380px;
}

/* On the public auth card `Show` is a bare text button positioned INSIDE the
   input (`style.css`: `.pw-field { position: relative }` + an absolute
   toggle). This scope restyled it as a bordered pill and made the field a flex
   row — but never took the absolute positioning off it, so the pill painted on
   top of the input's right end, overlapping it by 61px at every width
   (measured on Account, 2026-08-05). The row is a row: the button sits beside
   the field it reveals, and the field gives way to it. */
.entity .pw-toggle {
  position: static;
  transform: none;
  flex: none;
  border: 1px solid var(--line);
  background: var(--paper);
  color: var(--muted);
  border-radius: 9px;
  padding: 7px 10px;
  font: inherit;
  font-size: 12.5px;
  cursor: pointer;
}

.entity .pw-field > input {
  flex: 1 1 auto;
  min-width: 0;
  padding-right: 11px;
}

/* Admin's reset-link cell: a long URL that must stay copyable without pushing
   the table wide. Was two inline styles. */
.entity .reset-cell {
  max-width: 220px;
}

.entity .reset-cell input {
  width: 100%;
  font-size: 11px;
}

/* …but a card row is a flex line, not a column, so the cap would strand the
   value short of the card's right edge while every figure above it is flush */
@media (max-width: 560px) {
  .entity .reset-cell {
    max-width: none;
  }
}

/* plain tables inside a form card (closed periods) */
.entity .form-card table {
  width: 100%;
  border-collapse: collapse;
  background: transparent;
  box-shadow: none;
  border: 0;
  margin-top: 10px;
}

.entity .form-card table th,
.entity .form-card table td {
  padding: 9px 10px;
  text-align: left;
  border-bottom: 1px solid var(--line-2);
  background: transparent;
}

.entity .form-card table th {
  color: var(--muted);
  font-size: 11.5px;
  font-weight: 760;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* the company form's own compound controls */

.entity .crypto-row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
  margin: 8px 0;
  padding: 10px;
  border: 1px solid var(--line-2);
  border-radius: var(--radius-sm);
  background: var(--paper-2);
}

.entity .crypto-row .crypto-primary {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0;
  text-transform: none;
  letter-spacing: 0;
}

.entity .crypto-row .crypto-address {
  flex: 1 1 260px;
}

.entity .template-picker {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}

.entity .template-radio {
  margin: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 10px;
  background: var(--paper);
  cursor: pointer;
}

.entity .template-radio:has(input:checked) {
  border-color: var(--green);
  box-shadow: inset 0 0 0 1px var(--green);
}

.entity .template-radio-preview {
  display: block;
  width: 150px;
  height: 106px;
  overflow: hidden;
  border: 1px solid var(--line-2);
  border-radius: 8px;
  background: var(--paper);
  position: relative;
}

.entity .template-radio-preview iframe {
  width: 400%;
  height: 400%;
  border: 0;
  transform: scale(0.25);
  transform-origin: 0 0;
  pointer-events: none;
}

.entity .template-radio-label {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 8px;
  color: var(--ink);
  font-size: 13px;
  font-weight: 730;
  text-transform: capitalize;
}

.entity .tpl-enlarge {
  border: 0;
  background: none;
  color: var(--muted);
  cursor: pointer;
  font-size: 13px;
  padding: 0;
}

.entity .logo-block {
  display: flex;
  gap: 14px;
  align-items: center;
  margin: 8px 0;
}

.entity .logo-preview {
  max-width: 160px;
  max-height: 80px;
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 6px;
  background: #fff;
  object-fit: contain;
}

.entity .badge {
  display: inline-block;
  border-radius: 999px;
  padding: 3px 9px;
  background: var(--paper-3);
  color: var(--muted);
  font-size: 11.5px;
  font-weight: 730;
}

.entity .badge.bad {
  background: var(--red-soft);
  color: var(--red);
}

/* Legacy markup leaves some submit buttons unclassed (Load demo data, Set,
   Close period, Import backup, Add row); they get the shell's own button. */
.entity button:not([class]),
.entity input[type="submit"]:not([class]) {
  border: 1px solid var(--line);
  background: var(--paper);
  color: var(--ink);
  padding: 8px 12px;
  border-radius: 10px;
  font: inherit;
  font-weight: 730;
  cursor: pointer;
}

.entity button:not([class]):hover,
.entity input[type="submit"]:not([class]):hover {
  border-color: var(--faint);
  background: var(--paper-2);
}

/* A file input is not a text box, but it is still a control on a themed card:
   left bare it painted the UA's white box in dark mode (the logo picker on the
   company form and the restore-from-zip picker on Settings, measured
   2026-08-05). Only the surface is themed — the browser's own "Choose file"
   button inside it is left alone, because replacing it costs the native
   keyboard and accessibility behavior for nothing. */
.entity input[type="file"] {
  color: var(--muted);
  font-size: 13px;
  background: var(--paper-2);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 8px 10px;
  max-width: 100%;
}

/* the muted note that follows a data action reads as a caption, not a label */
.entity .form-card form + .muted,
.entity .form-card button + .muted,
.entity .form-card .btn + .muted {
  margin-left: 8px;
  font-size: 12.5px;
}

/* guided setup — the same three steps, in the shell's own palette */

.entity .setup-steps {
  list-style: none;
  padding: 0;
  margin: 0 0 22px;
  max-width: 640px;
}

.entity .setup-step {
  display: flex;
  gap: 14px;
  align-items: flex-start;
  padding: 16px 18px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--paper);
  margin: 0 0 10px;
}

.entity .setup-step.done {
  background: var(--paper-2);
}

.entity .setup-step-mark {
  flex: none;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: var(--paper-3);
  color: var(--muted);
  font-weight: 800;
  font-size: 13px;
}

.entity .setup-step.done .setup-step-mark {
  background: var(--green);
  color: var(--on-green);
}

.entity .setup-step-body {
  flex: 1;
  min-width: 0;
}

.entity .setup-step-body > div {
  margin-top: 6px;
}

.entity .setup-cta {
  margin-top: 22px;
}

/* was an inline `style="margin-top:8px"` on the note under the CTA */
.entity .setup-cta p {
  margin-top: 8px;
  max-width: 46ch;
}

/* Dev-review plaque (web/devstatus.py) — the markup exists only while
   BATCHER_REVIEW=1; without the flag nothing matches these rules. */
.dev-review {
  margin: 0 0 14px;
  padding: 8px 12px;
  border: 1px dashed var(--line);
  border-radius: var(--radius-sm);
  background: var(--paper-2);
  color: var(--muted);
  font-size: 12.5px;
}
.dev-review-status {
  font-weight: 600;
}
.dev-review-list {
  margin-top: 2px;
}

/* The sink's absolute path is one long unbreakable token, and a `code` element
   does not wrap on its own: at 430 it ran straight out of the card (visual
   pass, 2026-08-17). It breaks anywhere now — a path is read by its parts, not
   by its line. */
.telemetry-sink {
  display: inline-block;
  max-width: 100%;
  overflow-wrap: anywhere;
  word-break: break-all;
}

/* The demo panel that LOST CONTACT must stop looking like progress (owner
   decision, the third silent surface): the green banner and a bar frozen at
   its last position are exactly the "slow, but working" reading the change
   exists to end. The bar goes — a bar that cannot move is a promise nothing is
   keeping — and the panel takes the red the rest of the app uses. */
#demo-progress[data-stalled] {
  background: var(--red-soft);
  color: var(--red);
  border-color: var(--red);
}

#demo-progress[data-stalled] b {
  color: var(--red);
}

#demo-progress[data-stalled] progress {
  display: none;
}

/* The telemetry line (failure-telemetry, 2026-08-17). Same dev-plaque idiom —
   dashed, quiet, out of the product's own vocabulary — and it goes LOUD the
   moment something was recorded, because a manual pass should not have to look
   for it. Review-flag only, like everything else in this family. */
.telemetry-plaque {
  margin: 0 0 10px;
  padding: 6px 12px;
  border: 1px dashed var(--line);
  border-radius: var(--radius-sm);
  background: var(--paper-2);
  color: var(--muted);
  font-size: 12.5px;
  font-weight: 600;
}
.telemetry-plaque.ok {
  color: var(--green);
}
.telemetry-plaque.loud {
  border-color: var(--red);
  background: var(--red-soft);
  color: var(--red);
  font-weight: 760;
}

/* ===========================================================================
   Touch.

   style.css already carries `@media (pointer: coarse) { input, select,
   textarea { font-size: 16px } }` — the rule that stops iOS Safari zooming the
   page on focus and leaving the user zoomed and horizontally scrolled inside a
   modal. Every scope in THIS sheet outranks it on specificity, so the rule was
   live in the DOM and dead on the screen (measured 13.5px at 430 with
   `has_touch`). Restated here at a specificity that wins, together with the
   44px floor for the in-row `sm` controls, which is what a thumb needs.
   =========================================================================== */
@media (pointer: coarse) {
  .entity input,
  .entity select,
  .entity textarea,
  .composer-dialog input,
  .composer-dialog select,
  .composer-dialog textarea,
  .composer-dialog .combo-input,
  .review-dialog input,
  .review-dialog select,
  .review-dialog textarea,
  .review-dialog .combo-input,
  .r13 .field input,
  .r13 .field select {
    font-size: 16px;
  }

  .line-row input,
  .composer-dialog .line-row input,
  .review-dialog .line-row input,
  .run-row select,
  .run-row .combo-input,
  .run-row .btn,
  .r13 .btn.sm {
    min-height: var(--ctl);
    height: auto;
  }

  /* the compact step is 36px by design; a thumb still gets 44 — the icon
     buttons and the row kebab join the `sm` buttons above */
  .r13 .btn.iconbtn,
  .entity .btn.iconbtn,
  .shell .topbar .iconbtn {
    width: var(--ctl);
    height: var(--ctl);
  }

  .r13 .rowmenu > summary {
    min-height: var(--ctl);
    min-width: var(--ctl);
  }

  /* …and the ITEMS the spec names in the same sentence: they measured 36.1px
     under a thumb while the trigger had its 44 (web-shell §control scale).
     This is what makes deleting the phone card's local 11px 12px padding an
     honest simplification rather than a downgrade. */
  .r13 .rowmenu .menu-list a,
  .r13 .rowmenu .menu-list .linkbtn {
    min-height: var(--ctl);
  }

  /* the 30px grid track was stretching the native box to 23x13 — a control
     that looks like a rendering fault is not a control */
  .run-row input[type="checkbox"] {
    width: 20px;
    height: 20px;
    flex: none;
    justify-self: start;
  }

  .composer-dialog .check,
  .review-dialog .check {
    min-height: var(--ctl);
  }
}
