/* Amos, Clean-Sheet 2026: the app shell (templates/cs_base.html).
   The chrome, and every overlay and state class app.js drives by name.
   Class names here are app.js's contract, not a style choice: the JS
   adds .is-open, .is-visible, .nav-pending and the rest, and this file
   says what each looks like in the new system. */

/* ---------------------------------------------------------------- frame */
/* The sticky topbar's height: sticky day headers sit under it, and
   Schedule's reorder mode scrolls by it (jobs.html reads this token). */
:root { --topbar-height: calc(68px + env(safe-area-inset-top)); }
/* Anything scrolled into view - keyboard focus, an anchor, a sheet's
   field - clears the fixed bars instead of landing underneath them. */
html:has(body.cs) { scroll-padding-top: calc(var(--topbar-height) + var(--s-2)); scroll-padding-bottom: calc(88px + env(safe-area-inset-bottom)); }
@media (min-width: 1024px) { html:has(body.cs) { scroll-padding-bottom: var(--s-4); } }
body.cs { min-height: 100dvh; }
.app-shell { min-height: 100dvh; }

/* THE BAR (round 2, item 2). Inside the swapped region, so it arrives
   with its page: the sheep (a tap goes to Today) or the back control,
   the page's title at title weight, at most two actions. It pulls out
   to the edges of the gutter it sits in. */
.cs-main > .cs-topbar {
  margin: 0 calc(-1 * var(--gutter)) var(--s-3);
  border-bottom: 1px solid transparent;
  background: var(--canvas);
}
.cs-topbar .topbar__bar { justify-content: flex-start; gap: var(--s-1); }
.cs-identity {
  display: inline-grid; place-items: center; flex: none;
  width: 44px; height: 44px; margin-left: calc(-1 * var(--s-2));
  border-radius: var(--r-pill);
}
.cs-identity .mark { width: 30px; height: 30px; color: var(--live-mark); }
/* "‹ Schedule": the way back, named (round 3, item 2). The page names
   itself below the bar, so the bar never says a record's name twice. */
.topbar-back {
  display: inline-flex; align-items: center; gap: 2px; flex: 0 1 auto; min-width: 0;
  min-height: 44px; margin-left: calc(-1 * var(--s-2)); padding: 0 var(--s-2) 0 0;
  color: var(--live-text); text-decoration: none; font: var(--t-body); font-weight: 600;
}
.topbar-back .icon { flex: none; width: 26px; height: 26px; }
#topbar-back-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.topbar-back[hidden], .cs-identity[hidden] { display: none; }
.topbar__title { flex: 1 1 auto; min-width: 0; display: flex; align-items: baseline; gap: var(--s-2); }
.topbar__title h1 {
  margin: 0; min-width: 0;
  font: var(--t-title); letter-spacing: var(--track-title);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;   /* the full name is in the identity block below */
}
.topbar__count { flex: none; font: var(--t-meta); color: var(--ink-2); white-space: nowrap; }
.topbar__count::before { content: "\00b7"; margin-right: var(--s-2); }
.cs-topbar .topbar__actions { flex: none; margin-left: auto; align-items: center; }
.cs-topbar .icon-btn { color: var(--ink-2); }
/* A tab's one primary: the accent, round, the plus. */
.topbar-primary {
  appearance: none; border: 0; cursor: pointer; flex: none;
  width: 44px; height: 44px; padding: 0; border-radius: var(--r-pill);
  display: inline-grid; place-items: center;
  background: var(--accent); color: var(--on-accent);
}
.topbar-primary .icon { width: 22px; height: 22px; stroke-width: 2.4; }
/* Field fixes Sept 23, item 4. Every page's "+" also carries
   .header-action-btn, whose "display: inline-flex; align-items: center"
   (further down this file, same specificity) replaced the grid above - a
   flex row with no horizontal centring, so the 22px plus sat at the
   button's left edge: measured 11px left of centre on Schedule, Money,
   Clients, Quotes and Leads. Scoped to the bar so it wins on purpose.
   No optical nudge: this set's plus is drawn dead centre (strokes 5 to 19
   on both axes of a 24 box) and measures 0,0 once centred. */
.cs-topbar .topbar-primary { display: inline-grid; place-items: center; gap: 0; }
.topbar-primary:active { background: var(--accent-pressed); }
/* Polish pass, 3: ONE primary per screen. When the content is showing a
   primary of its own - an open row's Send, "Set up schedule" - the bar's
   "+" steps down to the quiet style, so the accent marks one thing. A
   primary out of sight does not count: inside a closed row, a sheet's or
   modal's source, or anything hidden. :has() re-reads on every toggle, so
   opening a row quiets the "+" and closing it gives the accent back. The
   count is pinned by scripts/polish/walk.py (audit_probe.js, primaries). */
#page-main:has(.cs-content :is(.btn--primary, .btn-primary, .action-row__btn.is-primary, .job-finish-door.is-start):not(
    details:not([open]) > :not(summary) *, details:not([open]) > :not(summary),
    .action-sheet-source *, .form-modal-source *, [hidden], [hidden] *,
    [style*="display:none"] *, [style*="display: none"] *)) .topbar-primary {
  background: var(--panel); color: var(--ink-1); box-shadow: inset 0 0 0 1px var(--line-strong);
}
.page-context { margin: calc(-1 * var(--s-2)) 0 var(--s-3); font: var(--t-meta); color: var(--ink-2); }
/* A page reached by going in names itself here, once. */
.page-title { margin: 0 0 var(--s-3); font: var(--t-display); font-size: 1.875rem; letter-spacing: var(--track-display); overflow-wrap: anywhere; }

/* The record's whole name, under a bar whose title may truncate. */
.identity { display: grid; gap: 2px; }
.identity__name { margin: 0; font: var(--t-display); font-size: 1.875rem; letter-spacing: var(--track-display); overflow-wrap: anywhere; }
.identity__meta { margin: 0; font: var(--t-body); color: var(--ink-2); }

.cs-main {
  padding: 0 var(--gutter) calc(96px + env(safe-area-inset-bottom));
  max-width: 1200px; margin: 0 auto;
}
.cs-content > * + * { margin-top: var(--s-4); }
.cs-backlink { padding-top: var(--s-2); }

/* ------------------------------------------------------------------ nav */
/* A phone: a tab bar under the thumb. */
.cs-nav {
  position: fixed; z-index: 30; left: 0; right: 0; bottom: 0;
  display: flex; justify-content: space-around;
  background: var(--panel);
  border-top: 1px solid var(--hairline);
  padding: var(--s-1) var(--s-1) max(var(--s-2), env(safe-area-inset-bottom));
}
.cs-nav a {
  flex: 1; max-width: 96px;
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  min-height: 52px; padding-top: 6px;
  color: var(--ink-3); text-decoration: none;
  font: var(--t-meta);
  border-radius: var(--r-control);
  -webkit-tap-highlight-color: transparent;
}
.cs-nav a .icon { width: 24px; height: 24px; }
.cs-nav a.active { color: var(--live-text); font-weight: 700; }
.cs-nav__brand, .cs-nav .nav-desktop-only { display: none; }
.nav-icon-wrap { position: relative; display: inline-flex; }
.nav-badge-dot {
  position: absolute; top: -1px; right: -3px;
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--attention-mark); box-shadow: 0 0 0 2px var(--panel);
}
body.keyboard-open .cs-nav { display: none; }
/* Each tab's tap area reaches through the bar's own padding, down into
   the home-indicator strip (the Sept 18 field bug: taps landing there
   hit nothing). Phone only - the desk rail stacks its links. */
@media (max-width: 1023px) {
  /* Five labels on one line at any text size (round 4, A): each grows
     with the phone's text until a fifth of the bar can hold no more. */
  .cs-nav a { font-size: min(0.875rem, 4.1vw); }
  .cs-nav a { position: relative; }
  .cs-nav a::after {
    content: ""; position: absolute; left: 0; right: 0;
    top: calc(-1 * var(--s-1));
    bottom: calc(-1 * max(var(--s-2), env(safe-area-inset-bottom)));
  }
}

/* A desk: the same nav as a left rail. The topbar keeps its actions. */
@media (min-width: 1024px) {
  .app-shell { padding-left: 232px; }
  .cs-nav {
    top: 0; right: auto; width: 232px;
    flex-direction: column; justify-content: flex-start; gap: 2px;
    border-top: 0; border-right: 1px solid var(--hairline);
    background: var(--canvas);
    padding: var(--s-5) var(--s-3);
  }
  .cs-nav__brand {
    display: flex; align-items: center; gap: var(--s-2);
    font: var(--t-title); font-weight: 700; letter-spacing: var(--track-title);
    padding: 0 var(--s-3) var(--s-6);
  }
  .cs-nav__brand .mark { width: 32px; height: 32px; color: var(--live-mark); }
  .cs-nav .nav-desktop-only { display: flex; }
  .cs-nav a {
    flex: none; max-width: none;
    flex-direction: row; gap: var(--s-3);
    min-height: 44px; padding: 0 var(--s-3);
    font: var(--t-body); font-weight: 550;
    color: var(--ink-2);
  }
  .cs-nav a:hover { background: var(--hover); color: var(--ink-1); }
  .cs-nav a.active { background: var(--live-fill); color: var(--live-ink); }
  .cs-identity { display: none; }            /* the rail carries the mark on a desk */
  .cs-main { padding-bottom: var(--s-16); }
}

/* Boosted navigation waiting: nothing for the first beat (announcing
   short taps is what made the app feel slow), then a thin accent bar. */
:root { --nav-pending-delay: 1s; }   /* how long before a wait is worth mentioning */
body.nav-pending::before {
  content: ""; position: fixed; z-index: 60; top: 0; left: 0; right: 0; height: 3px;
  background: var(--live-mark); pointer-events: none;   /* never eats the next tap */
  transform-origin: left; animation: nav-bar 1.6s var(--ease) var(--nav-pending-delay) both;
}
@keyframes nav-bar { from { transform: scaleX(0); } to { transform: scaleX(0.85); } }
@media (prefers-reduced-motion: reduce) {
  body.nav-pending::before { animation: none; opacity: 0; transition: opacity 0s 1s; }
}

/* ------------------------------------------------------------- banners */
.cs-banner, .cs-flash, .billing-banner {
  display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap;
  padding: var(--s-3) var(--s-4);
  border-radius: var(--r-control);
  background: var(--owed-fill); color: var(--owed-ink);
  font: var(--t-meta); font-weight: 600;
}
.cs-banner[data-level="error"], .cs-banner[data-level="danger"], .billing-banner-danger { background: var(--attention-fill); color: var(--attention-ink); }
:is(.cs-banner, .billing-banner) > span { flex: 1; min-width: 12ch; }
:is(.cs-banner, .billing-banner) a, .cs-banner button {
  color: inherit; font: inherit; font-weight: 700; background: none; border: 0; padding: 0;
  text-decoration: underline; text-underline-offset: 3px; cursor: pointer;
}
.cs-banner form { margin: 0; }
.cs-flash { background: var(--money-in-fill); color: var(--money-in-ink); }

/* ------------------------------------------------------ inline controls */
.text-btn {
  appearance: none; background: none; border: 0; padding: var(--s-2) 0; margin: 0;
  font: var(--t-meta); font-weight: 700; color: var(--live-text); cursor: pointer;
  display: inline-flex; align-items: center; gap: 6px; min-height: 44px;
  text-decoration: none;
}
.text-btn--quiet { color: var(--ink-2); }
form { margin: 0; }
.pro-badge { display: inline-flex; align-items: center; gap: 3px; margin-left: 6px; color: var(--ink-3); font: var(--t-meta); font-weight: 700; }
.pro-badge svg { width: 14px; height: 14px; }
.btn.btn-submitting, .btn.is-loading { pointer-events: none; }
.btn.btn-submitting::before, .btn.is-loading::before {
  content: ""; width: 18px; height: 18px; border-radius: 50%;
  border: 2px solid currentColor; border-right-color: transparent;
  animation: spin 0.8s linear infinite;
}

/* ----------------------------------------------------- swipe-to-complete */
.swipe-row { position: relative; overflow: hidden; list-style: none; }
.swipe-row-content { position: relative; z-index: 1; background: var(--panel); display: flow-root; }
.swipe-row-action {
  position: absolute; inset: 0 0 0 auto; width: 100%;
  display: flex; align-items: center; justify-content: flex-end; gap: var(--s-2);
  padding-right: var(--s-5);
  /* A solved pair (label 7:1 on its tint), never white on a mark. */
  background: var(--money-in-fill); color: var(--money-in-ink);
  font: var(--t-body); font-weight: 700; cursor: pointer;
}
/* Armed: past the commit point, the action goes solid - the canvas
   colour on the money-in text grade clears AA in both themes. */
.swipe-row.swipe-armed .swipe-row-action { background: var(--money-in-text); color: var(--canvas); }
.swipe-row.row-completing .swipe-row-content { background: var(--money-in-fill); transition: background-color var(--m-base) var(--ease); }

/* ----------------------------------------------------------- toasts etc */
.undo-toast {
  position: fixed; z-index: 70; left: 50%; bottom: calc(84px + env(safe-area-inset-bottom));
  transform: translate(-50%, 12px); opacity: 0; pointer-events: none;
  display: flex; align-items: center; gap: var(--s-4);
  min-height: 48px; padding: 0 var(--s-2) 0 var(--s-5);
  /* As wide as its words, up to the screen less its gutters. Fixed at
     left: 50% with no width, it had only the right half of the screen to
     lay out in, and "Sending invoice #1" broke over three lines. */
  width: max-content; max-width: calc(100vw - 2 * var(--gutter));
  background: var(--ink-1); color: var(--canvas);
  border-radius: var(--r-pill);
  font: var(--t-body); font-weight: 600;
  transition: opacity var(--m-base) var(--ease), transform var(--m-base) var(--ease);
}
#saved-toast { padding-right: var(--s-5); }
.undo-toast.is-visible { opacity: 1; transform: translate(-50%, 0); pointer-events: auto; }
.undo-toast button {
  appearance: none; border: 0; background: transparent; color: inherit;
  font: inherit; font-weight: 700; min-height: 44px; min-width: 44px; padding: 0 var(--s-4);
  border-radius: var(--r-pill); cursor: pointer; text-decoration: underline; text-underline-offset: 3px;
}
@media (min-width: 1024px) { .undo-toast { bottom: var(--s-8); margin-left: 116px; } }

.pull-refresh-indicator {
  position: fixed; z-index: 50; top: calc(env(safe-area-inset-top) + 8px); left: 50%;
  width: 36px; height: 36px; margin-left: -18px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--overlay); color: var(--live-text); border: 1px solid var(--hairline);
  opacity: 0; transform: translateY(-48px);
  transition: opacity var(--m-fast) var(--ease), transform var(--m-fast) var(--ease);
  pointer-events: none;
}
.pull-refresh-indicator .icon { width: 20px; height: 20px; }
.pull-refresh-indicator.is-visible { opacity: 1; transform: none; }
.pull-refresh-indicator.is-refreshing .icon { animation: spin 0.8s linear infinite; }

/* The waiting moment: submits only, and only after the same beat. */
.personality-wait {
  position: fixed; z-index: 80; inset: 0;
  display: grid; place-items: center;
  background: var(--scrim);
  opacity: 0; visibility: hidden; pointer-events: none;
}
body.submit-pending .personality-wait { opacity: 1; visibility: visible; transition: opacity var(--m-base) var(--ease) 1s, visibility 0s 1s; }
.personality-wait-card {
  display: grid; justify-items: center; gap: var(--s-2);
  padding: var(--s-6) var(--s-8);
  background: var(--overlay); border-radius: var(--r-sheet);
  font: var(--t-body); font-weight: 650;
}
.personality-wait-card i { display: none; }
.mascot-pic { display: contents; }

/* ------------------------------------------------------------ overlays */
/* One pattern for every sheet app.js opens: a scrim, and a surface that
   rises from the bottom on a phone and centres on a desk. */
.action-sheet-backdrop, .form-modal-backdrop, .compose-modal-backdrop,
.confirm-modal-backdrop, .doc-modal-backdrop, .search-overlay {
  position: fixed; inset: 0; z-index: 90;
  display: none;
  background: var(--scrim);
}
.action-sheet-backdrop.is-open, .form-modal-backdrop.is-open, .compose-modal-backdrop.is-open,
.confirm-modal-backdrop.is-open {
  display: flex; align-items: flex-end; justify-content: center;
}
.confirm-modal-backdrop.is-open, .compose-modal-backdrop.is-open { align-items: center; padding: var(--gutter); }

.action-sheet {
  width: 100%; max-width: 560px; max-height: 88dvh; overflow: auto;
  background: var(--overlay); color: var(--ink-1);
  border-radius: var(--r-sheet) var(--r-sheet) 0 0;
  padding: var(--s-2) var(--gutter) max(var(--s-5), env(safe-area-inset-bottom));
  animation: sheet-in var(--m-sheet) var(--ease);
}
.action-sheet-grabber {
  width: 40px; height: 5px; border-radius: var(--r-pill);
  background: var(--line-strong); margin: 0 auto var(--s-4);
}
.action-sheet-title { font: var(--t-title); letter-spacing: var(--track-title); margin: 0 0 var(--s-3); }
.action-sheet-item {
  display: flex; align-items: center; gap: var(--s-3);
  width: 100%; min-height: 52px; padding: 0 var(--s-2);
  background: none; border: 0; border-top: 1px solid var(--hairline);
  color: var(--ink-1); font: var(--t-body); font-weight: 600; text-align: left; text-decoration: none;
  cursor: pointer;
}
.action-sheet-item svg { width: 22px; height: 22px; color: var(--ink-2); }
.action-sheet-item.is-selected { color: var(--live-text); }
.action-sheet-close {
  display: flex; width: 100%; align-items: center; justify-content: center;
  min-height: var(--hit); margin-top: var(--s-4);
  background: var(--sunken); color: var(--ink-1); border: 0; border-radius: var(--r-control);
  font: var(--t-body); font-weight: 650; cursor: pointer;
}
@media (min-width: 720px) {
  .action-sheet-backdrop.is-open, .form-modal-backdrop.is-open { align-items: center; }
  .action-sheet { border-radius: var(--r-sheet); padding-bottom: var(--s-5); }
}

.form-modal-header { display: flex; align-items: center; justify-content: space-between; gap: var(--s-3); margin-bottom: var(--s-4); }
.form-modal-header h3 { font: var(--t-title); margin: 0; }
.form-modal-close, .doc-modal-close { appearance: none; border: 0; background: none; color: var(--ink-2); width: 44px; height: 44px; display: grid; place-items: center; border-radius: var(--r-pill); cursor: pointer; }
.form-modal-footer { position: sticky; bottom: 0; padding-top: var(--s-3); background: var(--overlay); }

.confirm-modal-card, .compose-modal {
  width: 100%; max-width: 420px;
  background: var(--overlay); color: var(--ink-1);
  border-radius: var(--r-sheet); padding: var(--s-6);
  animation: modal-in var(--m-base) var(--ease);
}
.confirm-modal-message { color: var(--ink-2); margin: var(--s-2) 0 var(--s-6); }
.compose-modal h3 { font: var(--t-title); margin: 0 0 var(--s-4); }
/* Field fixes Sept 23, item 1: the message IS the sheet. This rule used to
   read ".compose-modal textarea { min-height: 160px }" (0,1,1) - and lost,
   every time, to the shell's form normaliser below,
   "body.cs :is(input:not(...)x5, select, textarea) { min-height: 52px }".
   :is() takes its MOST specific argument, so that rule scores (0,6,2) even
   for a textarea. The composer drew two lines at 390.
   The composer's own id (1,0,0) outranks any class-level normaliser, on
   purpose; tests/test_field_fixes_0923.py computes both and compares.
   At least six lines, in the body face (17px - at or over 16px, so iOS
   does not zoom on focus); grows with what is typed (app.js
   fitComposer) until the sheet is as tall as the screen, then scrolls.
   em-based, so it follows the phone's text size (Round 4, Part A). */
body.cs .compose-modal {
  display: flex; flex-direction: column;
  max-height: calc(100dvh - 2 * var(--gutter));
}
#compose-modal-textarea {
  font: var(--t-body); line-height: 1.4;
  padding: var(--s-3) var(--s-4);
  min-height: calc(6 * 1.4em + 2 * var(--s-3) + 2px);   /* six lines + padding + the border (border-box) */
  flex: 0 1 auto; resize: none; overflow-y: auto;
  margin-bottom: var(--s-3);
}
/* One row at any width: Cancel and Send keep their size, the count takes
   what is left and wraps inside itself ("133 characters ·" / "1 text"). */
.compose-modal-actions { align-items: center; flex: none; flex-wrap: nowrap; }
.compose-count {
  flex: 1 1 auto; min-width: 0;
  font: var(--t-meta); color: var(--ink-2); font-variant-numeric: tabular-nums;
}
.compose-modal-actions > .btn { flex: 0 0 auto; }
.compose-context { margin: 0 0 var(--s-4); display: grid; gap: var(--s-1); }
/* display:grid above outranks the [hidden] attribute, so a draft with no
   recipient still drew an empty "To" row. */
.compose-context[hidden] { display: none; }
.compose-context-row { display: flex; gap: var(--s-2); font: var(--t-meta); }
.compose-context dt { color: var(--ink-3); font-weight: 600; }
.compose-context dd { margin: 0; font-weight: 650; }
/* Field fixes Sept 24, item 2: a reminder's Text / Email / Both - Money's
   segmented control in miniature: three equal 44px segments, the chosen
   one raised. One that cannot go is dimmed, and says why underneath.
   Anchored on the fieldset's id: the shell's label normaliser below,
   "body.cs label:not(.settings-row):not(.field)" (0,3,2), made each
   segment a block with a 16px top margin (measured). */
.compose-channels { flex: none; border: 0; margin: 0 0 var(--s-3); padding: 0; min-width: 0; }
.compose-channels[hidden], .compose-channels__why[hidden], .compose-hint[hidden] { display: none; }
#compose-modal-channels .compose-channels__options {
  display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); gap: 2px;
  padding: 3px; border-radius: var(--r-control); background: var(--sunken);
}
#compose-modal-channels .compose-channel {
  position: relative; display: grid; place-items: center; min-height: 44px; margin: 0; border-radius: 9px;
  color: var(--ink-2); font: var(--t-meta); font-weight: 700; cursor: pointer;
}
#compose-modal-channels .compose-channel input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: inherit; }
#compose-modal-channels .compose-channel:has(input:checked) { background: var(--panel); color: var(--ink-1); box-shadow: 0 0 0 1px var(--hairline); }
#compose-modal-channels .compose-channel:has(input:disabled) { color: var(--ink-3); cursor: not-allowed; }
#compose-modal-channels .compose-channel:has(input:focus-visible) { outline: 2px solid var(--focus); outline-offset: 2px; }
.compose-channels__why { list-style: none; margin: var(--s-2) 0 0; padding: 0; font: var(--t-meta); color: var(--ink-2); }
.compose-hint { flex: none; margin: 0 0 var(--s-3); font: var(--t-meta); color: var(--ink-2); }

.doc-modal-backdrop.is-open { display: flex; flex-direction: column; background: var(--canvas); }
.doc-modal-header { display: flex; justify-content: flex-end; padding: var(--s-2); padding-top: max(var(--s-2), env(safe-area-inset-top)); }
.doc-modal-body { flex: 1; }
.doc-modal-body iframe { width: 100%; height: 100%; border: 0; background: #fff; }

/* Search: a full sheet, field at the top. */
/* THE SEARCH SHEET (round 4, B): full height on a phone, the field in a
   sticky bar at the top, results in the list idiom - the name, its chip,
   the figure at the right edge, one meta line - grouped by kind. */
.search-overlay.is-open { display: block; background: var(--canvas); overflow: auto; overscroll-behavior: contain; }
.search-overlay-bar {
  position: sticky; top: 0; z-index: 1; display: flex; align-items: center; gap: var(--s-2);
  padding: var(--s-3) var(--gutter); padding-top: max(var(--s-3), env(safe-area-inset-top));
  background: var(--canvas); border-bottom: 1px solid var(--hairline);
}
.search-overlay-bar > svg { width: 22px; height: 22px; color: var(--ink-3); flex: none; }
.search-overlay-bar input {
  flex: 1; min-width: 0; min-height: 48px; border: 0; background: transparent;
  color: var(--ink-1); font: var(--t-body); font-size: 1.1875rem; outline: none;
}
.search-overlay-bar input::-webkit-search-cancel-button { display: none; }
.search-overlay-bar button { appearance: none; border: 0; background: none; color: var(--ink-2); width: 44px; height: 44px; display: grid; place-items: center; cursor: pointer; border-radius: var(--r-control); }
.search-overlay-bar button:hover { background: var(--hover); color: var(--ink-1); }
/* flow-root: the first heading's margin stays inside the panel on a desk. */
.search-results { display: flow-root; padding: 0 var(--gutter) var(--s-12); max-width: 720px; margin: 0 auto; }
.search-results-group-title { font: var(--t-meta); font-weight: 700; color: var(--ink-2); margin: var(--s-5) 0 var(--s-1); }
.search-result-item {
  display: grid; gap: 2px; align-content: center; min-height: 56px;
  padding: var(--s-3) var(--s-3); margin: 0 calc(-1 * var(--s-3));
  border-radius: var(--r-control);
  color: var(--ink-1); text-decoration: none;
  position: relative;
}
.search-result-item + .search-result-item::before {
  content: ""; position: absolute; top: 0; left: var(--s-3); right: var(--s-3);
  border-top: 1px solid var(--hairline);
}
a.search-result-item:hover, .search-result-item[aria-selected="true"] { background: var(--hover); }
.search-result-item[aria-selected="true"] { box-shadow: inset 0 0 0 2px var(--live-mark); }
.search-result-item .row3__name { overflow-wrap: anywhere; }
.search-result-item.is-static .row3__name { color: var(--ink-2); font-weight: 600; }
.search-result-meta { display: block; font: var(--t-meta); color: var(--ink-2); overflow-wrap: anywhere; }
.search-empty-state { padding: var(--s-12) 0; margin: 0; text-align: center; color: var(--ink-2); font: var(--t-body); }
@media (min-width: 1024px) {
  /* A desk: the sheet is a panel over a scrim, not the whole screen. */
  .search-overlay.is-open { background: var(--scrim); padding: var(--s-12) var(--s-8); }
  .search-overlay-bar, .search-results { max-width: 720px; margin: 0 auto; background: var(--panel); }
  .search-overlay-bar { border-radius: var(--r-sheet) var(--r-sheet) 0 0; padding-top: var(--s-3); }
  .search-results { border-radius: 0 0 var(--r-sheet) var(--r-sheet); padding-bottom: var(--s-5); min-height: 12rem; }
}

/* A row search opened is marked for a moment so the eye finds it. */
.row-item.is-found { outline: 2px solid var(--live-mark); outline-offset: 2px; border-radius: var(--r-control); }

/* BULK SEND IN THE COMPOSER (docs/archive/bulk-send-composer-spec.md): the
   recipients - each selected invoice, the excluded ones greyed with the
   reason - the placeholders the draft can use, the first message's
   preview; and, on each Money row after, what the send did. */
.compose-recipients {
  list-style: none; margin: 0 0 var(--s-3); padding: 0; max-height: 11rem; overflow: auto;
  border: 1px solid var(--hairline); border-radius: var(--r-control);
}
.compose-recipient {
  display: flex; flex-wrap: wrap; justify-content: space-between; column-gap: var(--s-3);
  padding: var(--s-2) var(--s-3); font: var(--t-meta);
}
.compose-recipient + .compose-recipient { border-top: 1px solid var(--hairline); }
.compose-recipient__who { color: var(--ink-1); font-weight: 650; }
.compose-recipient__doc { color: var(--ink-2); font-variant-numeric: tabular-nums; }
.compose-recipient__why { flex-basis: 100%; color: var(--ink-2); }
.compose-recipient.is-excluded .compose-recipient__who,
.compose-recipient.is-excluded .compose-recipient__doc { color: var(--ink-3); text-decoration: line-through; }
.compose-recipient.is-note { color: var(--ink-2); }
.compose-placeholders { margin: var(--s-2) 0 0; font: var(--t-meta); color: var(--ink-2); overflow-wrap: anywhere; }
.compose-preview { margin-top: var(--s-2); }
.compose-preview__text {
  margin: var(--s-1) 0 0; padding: var(--s-3); border-radius: var(--r-control);
  background: var(--sunken); color: var(--ink-1); font: var(--t-body); white-space: pre-wrap; overflow-wrap: anywhere;
}
/* anywhere: a provider's message can carry one long unbroken run
   ("TWILIO_ACCOUNT_SID/TWILIO_AUTH_TOKEN") that would widen the page. */
.bulk-outcome { margin: 2px 0 0; font: var(--t-meta); color: var(--ink-2); overflow-wrap: anywhere; }
.bulk-outcome.is-error { color: var(--attention-text); }

/* The upgrade sheet (partial _upgrade_sheet.html). */
.upgrade-sheet-body { display: grid; gap: var(--s-2); padding-bottom: var(--s-2); }
.upgrade-eyebrow { display: inline-flex; align-items: center; gap: 6px; margin: 0; font: var(--t-meta); font-weight: 700; color: var(--live-text); }
.upgrade-eyebrow svg { width: 16px; height: 16px; }
.upgrade-title { font: var(--t-title); margin: 0; }
.upgrade-sentence { margin: 0 0 var(--s-3); color: var(--ink-2); }
.upgrade-cta .btn-primary {
  display: flex; width: 100%; align-items: center; justify-content: center;
  min-height: var(--hit-lg); border: 0; border-radius: var(--r-control);
  background: var(--accent); color: var(--on-accent);
  font: var(--t-body); font-size: 1.125rem; font-weight: 650; cursor: pointer;
}
.upgrade-cta-note { font: var(--t-meta); font-weight: 450; color: var(--ink-3); margin: 0; }
.upgrade-more { font: var(--t-meta); font-weight: 700; color: var(--live-text); text-decoration: none; padding: var(--s-2) 0; }

/* Full page loads (crossing between shells, form posts) cross-fade like
   the original shell does, briefly; both documents must opt in or the
   browser aborts the transition. Off under reduced motion. */
@media (prefers-reduced-motion: no-preference) {
  @view-transition { navigation: auto; }
  ::view-transition-old(root), ::view-transition-new(root) { animation-duration: 160ms; }
}

/* ------------------------------------------------------------- forms */
/* Every form field in the new shell looks like .input, whatever markup
   a partial brings - so forms moved across from the original templates
   need no per-field classes. */
body.cs label:not(.settings-row):not(.field) {
  display: block; margin: var(--s-4) 0 6px;
  font: var(--t-meta); font-weight: 700; color: var(--ink-1);
}
body.cs :is(input:not([type=checkbox]):not([type=radio]):not([type=hidden]):not([type=range]):not([type=file]), select, textarea) {
  appearance: none; width: 100%;
  min-height: 52px; padding: 0 var(--s-4);
  background: var(--panel); color: var(--ink-1);
  border: 1px solid var(--line-strong); border-radius: var(--r-control);
  font: var(--t-body);
}
body.cs textarea { padding: var(--s-3) var(--s-4); min-height: 104px; }
body.cs select {
  padding-right: var(--s-10);
  background-image: linear-gradient(45deg, transparent 50%, var(--ink-2) 50%),
                    linear-gradient(135deg, var(--ink-2) 50%, transparent 50%);
  background-position: right 22px center, right 16px center;
  background-size: 6px 6px; background-repeat: no-repeat;
}
body.cs :is(input, select, textarea):focus { outline: none; border-color: var(--focus); box-shadow: 0 0 0 1px var(--focus); }
body.cs input[type=checkbox] { width: 22px; height: 22px; accent-color: var(--accent); }
body.cs input::placeholder, body.cs textarea::placeholder { color: var(--ink-3); }
/* Field fixes Sept 23, item 2. The normaliser above gives date and time
   fields appearance:none like every other input. WebKit draws those two
   as an inline-flex box sized off its own formatted value, and with the
   native look removed that inner box can collapse or centre out of view
   on iOS - the reschedule sheet "opened blank" on Yasuke's phone while
   the values were there in the page (and draw fine in Chrome). Block
   display and a value box that keeps a line of height and sits left. */
body.cs input:is([type=date], [type=time], [type=datetime-local]) { display: block; line-height: 1.4; }
body.cs input:is([type=date], [type=time], [type=datetime-local])::-webkit-date-and-time-value {
  text-align: left; min-height: 1.4em; margin: 0;
}
/* Sept 24 (Yasuke: "the filled in data isn't centered"). The block display
   above let iOS draw the value again, but a block date field in WebKit puts
   its one line at the TOP of the 52px box - the normaliser gives fields no
   vertical padding, because a text input centres itself and a block date
   field does not. Chrome centres its own inner box, which is why every
   screenshot looked right. Padding sized to the line centres it in any
   engine: equal space above and below a 1.4em line in a 52px box, and none
   once the text is so large the line fills it. The :is(#…) arm only lifts
   this over the normaliser's (0,6,2); no element has that id. */
body.cs :is(input:is([type=date], [type=time], [type=datetime-local]), #date-field-centring) {
  padding-top: max(0px, calc((52px - 2px - 1.4em) / 2));
  padding-bottom: max(0px, calc((52px - 2px - 1.4em) / 2));
  height: max(52px, calc(1.4em + 2px));   /* the other fields' 52px: Chrome's value box runs a little taller than the line */
}
.search-overlay-bar input { border: 0 !important; box-shadow: none !important; background: transparent !important; }

/* The original templates' shared vocabulary, drawn in the new system,
   for markup that partials still share with the original shell. */
body.cs .btn-primary, body.cs .btn-secondary {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s-2);
  min-height: var(--hit); padding: 0 var(--s-5);
  border: 1px solid transparent; border-radius: var(--r-control);
  font: var(--t-body); font-weight: 650; text-decoration: none; cursor: pointer; white-space: nowrap;
}
body.cs .btn-danger {
  display: inline-flex; align-items: center; justify-content: center; min-height: var(--hit); padding: 0 var(--s-5);
  border: 0; border-radius: var(--r-control); background: var(--danger); color: var(--on-danger);
  font: var(--t-body); font-weight: 650; cursor: pointer;
}
body.cs .btn-primary { background: var(--accent); color: var(--on-accent); }
body.cs .btn-secondary { background: var(--panel); color: var(--ink-1); border-color: var(--line-strong); }
/* At a large text size a label may take two lines rather than run off the
   button (round 4, A; the em query is explained in components.css). */
@media (max-width: 22em) {
  body.cs .btn-primary, body.cs .btn-secondary {
    white-space: normal; text-align: center; padding-top: var(--s-2); padding-bottom: var(--s-2);
  }
}
body.cs .btn-primary:disabled, body.cs .btn-secondary:disabled { background: var(--sunken); color: var(--ink-3); border-color: var(--hairline); cursor: not-allowed; }
/* A stacked form's submit spans it; an inline one-line form
   (.settings-row) keeps its button beside the field. */
body.cs form:not(.settings-row) > .btn-primary[type=submit],
body.cs .form-modal-footer .btn-primary { width: 100%; margin-top: var(--s-5); min-height: var(--hit-lg); font-size: 1.125rem; }
body.cs .form-modal-footer .btn-primary { margin-top: 0; }
.meta { font: var(--t-meta); font-weight: 450; color: var(--ink-2); }
.fine-print { font: var(--t-meta); font-weight: 450; color: var(--ink-3); margin: var(--s-2) 0 0; }
.form-subheading { font: var(--t-meta); font-weight: 700; color: var(--ink-2); margin: var(--s-5) 0 var(--s-2); }
.mt-16 { margin-top: var(--s-4); } .mt-6 { margin-top: 6px; } .m-0 { margin: 0; }
.no-underline { text-decoration: none; }
.settings-row { display: flex; align-items: center; justify-content: space-between; gap: var(--s-3); min-height: 52px; }
.settings-row-label { font: var(--t-body); font-weight: 600; }
.action-sheet-count { margin-left: auto; font: var(--t-meta); color: var(--ink-3); font-variant-numeric: tabular-nums; }

/* The footer bar every bulk mode uses (rain delay, reorder, batches). */
.batch-action-bar {
  position: fixed; z-index: 35; left: 0; right: 0; bottom: 0;
  display: flex; align-items: center; justify-content: space-between; gap: var(--s-3);
  padding: var(--s-3) var(--gutter) max(var(--s-3), env(safe-area-inset-bottom));
  background: var(--overlay); border-top: 1px solid var(--hairline);
  font: var(--t-body); font-weight: 650;
  transform: translateY(110%); transition: transform var(--m-base) var(--ease);
}
.batch-action-bar.is-visible { transform: none; }
.batch-action-bar button {
  min-height: var(--hit); padding: 0 var(--s-4); border-radius: var(--r-control);
  font: var(--t-body); font-weight: 650; cursor: pointer;
  background: var(--panel); color: var(--ink-1); border: 1px solid var(--line-strong);
}
.batch-action-bar button:last-child { background: var(--accent); color: var(--on-accent); border-color: transparent; }
.batch-action-bar button:disabled { background: var(--sunken); color: var(--ink-3); border-color: var(--hairline); }
.batch-action-bar .is-refused { color: var(--attention-text); }
@media (min-width: 1024px) { .batch-action-bar { left: 232px; } }

/* ============================================ THE SHARED VOCABULARY */
/* The original templates speak one small vocabulary of class names -
   card, row-item, info-row, badge, stat-row, quick-actions, the
   show-all lists. Pages moving across keep those names where their
   partials and tests rely on them; this is what each one means in the
   new system. A page's own composition lives in screens.css. */

/* THE COMPATIBILITY LAYER. Some markup carried across still names the
   original tokens in inline styles (and in HTML a few scripts build).
   Each old name is mapped onto the new system here, on body, so it
   resolves in whichever theme <html> carries. Never used by new work;
   it shrinks as markup is recomposed. `--danger` is deliberately NOT
   mapped: it is a new token (the destructive fill), so markup naming the
   old --danger as a text colour is converted to .is-attention instead. */
body.cs {
  --warning: var(--owed-text);
  --warning-soft: var(--owed-fill);
  --success: var(--money-in-text);
  --border: var(--hairline);
  --bg: var(--canvas);
  --surface: var(--panel);
  --text: var(--ink-1);
  --text-secondary: var(--ink-2);
  --text-tertiary: var(--ink-3);
  --brand-text: var(--live-text);
  --radius-sm: 8px;
  --radius-md: var(--r-control);
  --control-h: var(--hit);
  --icon-md: 22px;
  --text-meta: 0.875rem;
  --text-body: 1.0625rem;
  --text-input: 1.0625rem;
  --text-title: 1.375rem;
  --text-heading: 1.375rem;
  --text-display: 2.5rem;
}

/* Panels. */
.card, .card-plain {
  background: var(--panel); border: 1px solid var(--hairline); border-radius: var(--r-panel);
  padding: var(--s-4);
}
.card > h2, .card-plain > h2, .card-header h2 {
  font: var(--t-title); letter-spacing: var(--track-title); margin: 0 0 var(--s-2);
}
.card-header { display: flex; align-items: baseline; justify-content: space-between; gap: var(--s-3); margin-bottom: var(--s-1); }
.card-header h2 { margin: 0; }
.card-header-action {
  appearance: none; background: none; border: 0; padding: 0; cursor: pointer;
  font: var(--t-meta); font-weight: 700; color: var(--live-text); text-decoration: none;
  min-height: 44px; display: inline-flex; align-items: center;
  /* polish pass, 6: a short label ("CSV") still takes a 44px-wide tap,
     its text kept on the right edge where it sat */
  min-width: 44px; justify-content: flex-end;
}
.card-description { font: var(--t-body); color: var(--ink-2); margin: 0 0 var(--s-4); }

/* Rows: hairline-separated, the name reads, one fact trails. */
.row-item {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s-3);
  min-height: 56px; padding: var(--s-3) 0;
  border-top: 1px solid var(--hairline); color: var(--ink-1);
}
.card > h2 + .row-item, .card-plain > h2 + .row-item,
.card > h2 + a > .row-item, .card-plain > h2 + a > .row-item, .card-plain > h2 + .plain-link-block > .row-item,
.card-header + .row-item, .card-header + a > .row-item { border-top: 0; }
.row-item > div { min-width: 0; }
.row-stack { flex-direction: column; align-items: stretch; gap: 4px; }
.row-item .name { font: var(--t-body); font-weight: 650; display: block; }
.row-item .meta { display: block; }
.amount { font: var(--t-body); font-weight: 650; font-variant-numeric: tabular-nums; white-space: nowrap; }
.plain-link, .plain-link-block { display: block; color: inherit; text-decoration: none; }
a.row-item.plain-link { display: flex; }
/* A bare link inside running text reads as a link, in the accent text. */
body.cs .cs-content a:not([class]) { color: var(--live-text); text-underline-offset: 3px; }
.plain-link:hover .row-item, .plain-link-block:hover .row-item { background: var(--hover); margin: 0 calc(-1 * var(--s-4)); padding-left: var(--s-4); padding-right: var(--s-4); }
.row-item-leading { display: flex; gap: var(--s-3); align-items: flex-start; min-width: 0; }
.row-item-icon { width: 20px; height: 20px; flex: none; color: var(--ink-3); margin-top: 2px; }
.row-item-trailing { display: flex; align-items: center; gap: var(--s-2); flex: none; }
.row-accent-muted .row-item-icon { color: var(--live-text); }
.timeline-detail { font: var(--t-meta); font-weight: 450; color: var(--ink-2); margin-top: 2px; }

/* "Show all" lists: the first few, then the rest on request. */
.list-overflow-item { display: none; }
.list-open .list-overflow-item { display: block; }
.show-all-link, .show-less-link {
  display: flex; align-items: center; min-height: 44px; margin-top: var(--s-1);
  font: var(--t-meta); font-weight: 700; color: var(--live-text); text-decoration: none;
}
.show-less-link, .list-open .show-all-link { display: none; }
.list-open .show-less-link { display: flex; }

/* Status badges retired in round 2, item 1: every state is the chip
   (components.css, .chip / .status). */

/* A fact with its icon and its label. */
.info-row { display: grid; grid-template-columns: 22px minmax(0, 1fr); gap: var(--s-3); padding: var(--s-3) 0; border-top: 1px solid var(--hairline); }
.card-header + .info-row, .card-header + .form-modal-source + .info-row { border-top: 0; }
.info-row > svg { width: 20px; height: 20px; color: var(--ink-3); margin-top: 2px; }
.info-row-text { font: var(--t-body); min-width: 0; overflow-wrap: anywhere; }
.info-row-text a { color: var(--live-text); text-decoration: none; font-weight: 600; }
.field-k { display: block; font: var(--t-meta); font-weight: 650; color: var(--ink-3); margin-bottom: 2px; }
/* The caution on a client or property page: amber tint, as on the job
   page's instructions panel (round 3, item 5). */
.info-row.is-caution {
  margin: 0 calc(-1 * var(--s-2)); padding-left: var(--s-2); padding-right: var(--s-2);
  background: var(--owed-fill); border-radius: var(--r-control); border-top-color: transparent;
}
.info-row.is-caution > svg, .info-row.is-caution .field-k { color: var(--owed-ink); }
.info-row.is-caution + .info-row { border-top-color: transparent; }
.info-row-group-label { font: var(--t-meta); font-weight: 800; color: var(--owed-text); margin-top: var(--s-4); }
.prose { margin: 0 0 var(--s-1); }

/* The numbers band at the top of a detail page. */
/* The columns are as wide as the labels' text (5.5rem: 88px at 100%), so
   at a large text size a figure takes the next row rather than its label
   running into its neighbour's (round 4, A). */
.stat-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(5.5rem, 1fr)); gap: var(--s-3); }
.stat-row > .stat { display: grid; grid-template-columns: minmax(0, 1fr); align-content: start; gap: 2px; background: none; border: 0; padding: 0; border-radius: 0; }
.stat-row .num { font: var(--t-title); font-size: min(1.625rem, 8.5vw); font-weight: 700; letter-spacing: var(--track-title); font-variant-numeric: tabular-nums; }
.stat-row .num.is-owed { color: var(--attention-text); }
/* Polish pass, 9: at 150% text "Outstanding" ran into "Overdue". The label
   grows with the phone's text until the column can hold no more - the same
   min(rem, vw) cap the figure above it uses - rather than breaking a word. */
.stat-row .label { font: var(--t-meta); font-size: min(0.875rem, 4vw); font-weight: 650; color: var(--ink-3); }


/* Small things. */
/* Inside a panel "empty" is one quiet line, not the full empty state. */
:is(.card, .card-plain) .empty { color: var(--ink-3); margin: var(--s-1) 0 0; padding: 0; display: grid; gap: var(--s-3); justify-items: start; text-align: left; }
:is(.card, .card-plain) p.empty { display: block; }
.btn-danger-text, .action-sheet-item-danger { color: var(--attention-text) !important; }
.action-sheet-item-danger svg { color: var(--attention-text); }
.flash { padding: var(--s-3) var(--s-4); border-radius: var(--r-control); background: var(--money-in-fill); color: var(--money-in-ink); font: var(--t-meta); font-weight: 600; }
.mt-4 { margin-top: 4px; } .mt-8 { margin-top: 8px; } .mt-10 { margin-top: 10px; }
.form-subheading-note { font: var(--t-meta); font-weight: 450; color: var(--ink-3); }
.header-action-btn { display: inline-flex; align-items: center; gap: 6px; }
.header-action-btn svg { width: 18px; height: 18px; }
.flex-1 { flex: 1; min-width: 0; } .flex-2 { flex: 2; min-width: 0; }

/* The schedule preview (partial _schedule_preview.html). */
.sched-identity { font: var(--t-meta); font-weight: 700; color: var(--ink-2); margin: 0 0 var(--s-2); }
.sched-preview { margin-top: var(--s-4); padding: var(--s-3) var(--s-4); background: var(--planned-fill); color: var(--planned-ink); border-radius: var(--r-control); }
.sched-preview-title { font: var(--t-meta); font-weight: 800; }
.sched-preview-sentence, .sched-preview-scope { font: var(--t-body); margin: var(--s-1) 0 0; }
.sched-preview-dates { font: var(--t-meta); font-variant-numeric: tabular-nums; margin-top: var(--s-1); }
.sched-preview-note { font: var(--t-meta); font-weight: 450; margin: var(--s-1) 0 0; opacity: 0.9; }

/* ============================================================ DENSITY */
/* Settings > Appearance > View Density: compact tightens rows and
   panels. It only ever takes space away - type never grows. */
[data-density="compact"] .row { min-height: 52px; padding-top: var(--s-2); padding-bottom: var(--s-2); }
[data-density="compact"] .job-row { min-height: 56px; padding-top: var(--s-2); padding-bottom: var(--s-2); }
[data-density="compact"] .row-item { min-height: 48px; padding-top: var(--s-2); padding-bottom: var(--s-2); }
[data-density="compact"] .card, [data-density="compact"] .card-plain { padding: var(--s-3); }
[data-density="compact"] .cs-content > * + * { margin-top: var(--s-3); }

/* ======================================================= DETAIL PAGES */
/* Round 2, items 7/8 (docs/archive/cleansheet-2026-round2-spec.md): every
   detail page says its state once, lists its facts once, and offers one
   row of actions.

   The state block: the chip, one sentence, the single next action. */
.state-block {
  display: grid; gap: var(--s-2); justify-items: start;
  padding: var(--s-4); background: var(--panel);
  border: 1px solid var(--hairline); border-radius: var(--r-panel);
}
.state-block.is-live { background: var(--live-fill); border-color: transparent; }
.state-block__sentence { margin: 0; font: var(--t-title); color: var(--ink-1); font-variant-numeric: tabular-nums; }
.state-block > .btn, .state-block > form { justify-self: stretch; }
.state-block > form > .btn, .state-block > .btn { width: 100%; min-height: var(--hit); margin-top: var(--s-2); }
/* In a record opened inside a list row (invoice, quote, lead), the
   chip is already on the row's own line, so the block is the sentence. */
.state-line { margin: 0 0 var(--s-2); font: var(--t-body); font-weight: 600; color: var(--ink-1); font-variant-numeric: tabular-nums; }

/* The facts: one panel of label + value rows. */
.facts .field-list { margin: 0; }
.facts { padding: var(--s-1) var(--s-4); }
.facts .field:first-child { border-top: 0; }
.field--form { align-items: center; }
.field--form select { min-height: 44px; }

/* ONE action row: three or four equal buttons, 44px tall, a short word
   beside its icon. The words are chosen to fit a quarter of a phone;
   the button's aria-label says the whole thing. */
.action-row { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); gap: var(--s-2); }
.action-row__btn {
  appearance: none; display: flex; justify-content: center; align-items: center; gap: 6px;
  width: 100%; min-height: 44px; padding: 0 var(--s-1);
  background: var(--panel); border: 1px solid var(--hairline); border-radius: var(--r-control);
  color: var(--ink-1); font: var(--t-meta); font-weight: 650; text-decoration: none; cursor: pointer;
  white-space: nowrap;
}
.action-row__btn .icon, .action-row__btn svg { flex: none; width: 18px; height: 18px; color: var(--live-text); }
@media (max-width: 420px) {
  /* Four to a phone: the icon sits over its word, still 44px tall. */
  /* The word grows with the phone's text until a quarter of the row can
     hold no more (round 4, A). */
  .action-row__btn { flex-direction: column; gap: 1px; font-size: min(0.75rem, 3.5vw); min-height: 44px; }
}
.action-row__btn:hover { background: var(--hover); }
.action-row__btn.is-primary { background: var(--accent); border-color: transparent; color: var(--on-accent); }
.action-row__btn.is-primary .icon, .action-row__btn.is-primary svg { color: var(--on-accent); }
.action-row form { display: contents; }
/* Field fixes Sept 24, item 3: THE RECORD'S ACTION ROW ON MONEY, QUOTES AND
   LEADS (an invoice, a quote or a lead opened in its list). On a phone
   the rule above stacked each icon over its word and shrank the word to
   12px so four fitted a row - and at 130% text "Charge card" still ran
   out of its 75px box (measured). Buttons v2 instead: the icon inline
   beside its word, the word at the row's own 14px (the desk's size,
   never smaller), 44px tall, equal widths. What gives is how many share
   a row: three when three fit, two by two for four, and fewer as the
   phone's text grows. The column floor is in the text's own units:
   7em holds the longest word a three-button row carries ("Mark paid",
   66px at 14px, + the 18px icon, the 6px gap and 4px each side) - so
   at 130% a three-button row goes two to a line instead of clipping. */
.action-row--words {
  font: var(--t-meta);
  grid-auto-flow: row; grid-auto-columns: auto;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, max(7em, 30%)), 1fr));
}
.action-row--words:has(> :nth-child(4)) {
  grid-template-columns: repeat(auto-fit, minmax(min(100%, max(7em, 40%)), 1fr));
}
.action-row--words .action-row__btn {
  flex-direction: row; gap: 6px; font: var(--t-meta); font-weight: 650;
  min-height: 44px; padding: 0 var(--s-1);
}
.sms-feedback:empty { display: none; }

/* Each panel's one control sits at its bottom right. */
.panel__foot { display: flex; justify-content: flex-end; }
.item-edit-add { margin: var(--s-2) 0; }

