/* ==========================================================================
   AUDUBON OPERATIONS DASHBOARD — style-v2.css
   Round 1 of 8: FOUNDATION.  Stripe-shaped system, Audubon hues.
   Written 08/06/2026 against STRIPE-REDESIGN-SPEC.md §4.

   WHAT THIS FILE IS
   -----------------
   An override layer loaded AFTER style.css.  style.css is 5,625 lines of ~36
   append-only rounds; a 37th round appended to its tail would have to
   out-specify all of it.  This file instead copies incumbent selectors
   VERBATIM and wins on cascade order, so nothing here escalates specificity.

   TO REVERT THE WHOLE REDESIGN
   ----------------------------
   Delete the style-v2.css <link> line in layout() in app.py.  style.css is
   untouched and still renders every page on its own.

   CONTRAST RULE (measured on these ramps, and on Stripe's own gray as a
   control): steps FIVE POSITIONS apart on the nine-step 100-900 scale clear
   WCAG AA for small text (5.27-5.33:1).  e.g. 400 on 900.
   FOUR positions apart is 3.52-3.55 and FAILS.  e.g. 500 on 100 = 4.11.
   "Positions" means places on the scale, NOT the difference in the label.
   When in doubt use a 6+ gap (7.40+, AAA).

   DESIGN-NOTES RULE 14 IS NON-NEGOTIABLE, AND THIS FILE HONOURS IT BY
   NOT TOUCHING THE SYSTEMS IT NAMES.
   No rule in this file selects .kv-tone-*, .side-tag-*, .rp-side-label,
   .waitlist-panel, .maint-panel or .rate-panel.  All seven .kv-tone-* hues
   are literal hexes in style.css (lines 2284-2289, 2802) and stay literal.
   Two mockups were rejected for collapsing these onto one colour.  Nine hue
   ramps ship below for exactly that reason: consistency here means one scale
   of LIGHTNESS, SPACING and ELEVATION — never one hue.

   ON LITERALS: colour, radius, elevation, motion and type are always tokens.
   Component padding and sizing may be literal pixels (DESIGN-NOTES 11).
   ========================================================================== */


/* ==========================================================================
   §4.1  THE RAMPS — perceptually even, CIELAB-derived, Stripe's method.
   Nine hues, because colour is functional here (DESIGN-NOTES 14).
   Each ramp is laid out on the mean L* ladder measured off Stripe's own Sail
   tokens — 98, 94, 87, 80, 70, 60, 48, 38, 29, 21, 10 — with the chroma
   envelope scaled so the ramp passes through the real Audubon brand colour.
   ========================================================================== */
:root {
  /* NEUTRAL — teal-tinted, not pure grey. Carries the brand at low chroma. */
  --slate-50:#f5f8f9;  --slate-100:#e9efef; --slate-150:#d2dcdd; --slate-200:#bbcacc;
  --slate-300:#9cafb1; --slate-400:#7e9396; --slate-500:#607679; --slate-600:#495d5f;
  --slate-700:#3a4849; --slate-800:#2a3435; --slate-900:#171d1d;

  /* TEAL — brand primary. #286e75 sits at L*42.6, between 500 and 600.
     NOTE: --teal-600/700/800/900 are ALREADY declared in style.css at lines
     2049-2052 (#2b7880 / #286e75 / #1c4f54 / #14383c). This file supersedes
     them by cascade order. That is deliberate — see the header of §4.3a in
     the spec, and the round summary, for the 14 references it re-colours. */
  --teal-50:#f0fafb;  --teal-100:#e1f0f2; --teal-150:#c1dfe2; --teal-200:#a3cfd4;
  --teal-300:#7cb5bb; --teal-400:#5a9aa1; --teal-500:#347c84; --teal-600:#1e6269;
  --teal-700:#1d4b50; --teal-800:#16373a; --teal-900:#0c1f21;

  /* MINT — brand accent */
  --mint-50:#eafcf6;  --mint-100:#d6f4ea; --mint-150:#abe5d3; --mint-200:#81d8be;
  --mint-300:#47bea0; --mint-400:#00a384; --mint-500:#008269; --mint-600:#006652;
  --mint-700:#004f3f; --mint-800:#003a2d; --mint-900:#002218;

  /* GOLD — brand accent, used sparingly */
  --gold-50:#fff7ec;  --gold-100:#ffebcf; --gold-150:#fdd392; --gold-200:#f4bf5f;
  --gold-300:#dca11c; --gold-400:#bb8600; --gold-500:#966a00; --gold-600:#775300;
  --gold-700:#5c4000; --gold-800:#442e00; --gold-900:#281a00;

  /* GREEN — success / active */
  --green-50:#edfcf0; --green-100:#ddf3e2; --green-150:#b9e4c3; --green-200:#96d6a7;
  --green-300:#6bbc83; --green-400:#45a165; --green-500:#0e8345; --green-600:#006833;
  --green-700:#075028; --green-800:#093a1d; --green-900:#05210e;

  /* RED — danger / clinical */
  --red-50:#fff6f4;  --red-100:#ffe9e4; --red-150:#ffcec4; --red-200:#ffb5a6;
  --red-300:#ff8a75; --red-400:#eb6651; --red-500:#cd4031; --red-600:#a82a20;
  --red-700:#7f251b; --red-800:#5d1c14; --red-900:#370f04;

  /* COPPER — Phosphor house */
  --copper-50:#fff6f1;  --copper-100:#ffeadb; --copper-150:#fdd1b0; --copper-200:#f7bb8c;
  --copper-300:#e39c62; --copper-400:#c87f40; --copper-500:#a9601b; --copper-600:#884909;
  --copper-700:#68390d; --copper-800:#4c2a0a; --copper-900:#2d1700;

  /* BLUE — Seminole house / info / care tone */
  --blue-50:#f5f8ff;  --blue-100:#e7edff; --blue-150:#cad9ff; --blue-200:#aec8ff;
  --blue-300:#86acf0; --blue-400:#6290da; --blue-500:#3573c0; --blue-600:#195b9e;
  --blue-700:#1e4677; --blue-800:#183356; --blue-900:#0d1c32;

  /* VIOLET — waitlist / residency tone */
  --violet-50:#faf6ff;  --violet-100:#f3eaff; --violet-150:#e5d1ff; --violet-200:#d7bbfe;
  --violet-300:#bd9cef; --violet-400:#a17fd8; --violet-500:#8260be; --violet-600:#67499c;
  --violet-700:#4f3975; --violet-800:#3a2a55; --violet-900:#211732;

  /* The literal brand colours stay addressable and unchanged. */
  --brand-teal:#286e75; --brand-teal-dark:#1c4f54;
  --brand-mint:#58c5a8; --brand-gold:#d99e14;
}


/* ==========================================================================
   §4.2  SEMANTIC LAYER — the only names components should use.
   Never a raw ramp step in a component rule; never a literal hex.
   ========================================================================== */
:root {
  /* surfaces */
  --surface:       #ffffff;
  --surface-hover: var(--slate-50);

  /* text — measured on white */
  --text-muted:    var(--slate-600);   /*  6.97:1  AA  */
  --text-subtle:   var(--slate-500);   /*  4.81:1  AA  — small caps and labels only */
  --text-onbrand:  #ffffff;

  /* lines */
  --line:          var(--slate-150);
  --line-strong:   var(--slate-200);
  --line-subtle:   var(--slate-100);

  /* interactive */
  --brand:         var(--teal-600);    /*  6.98:1 on white */
  --brand-hover:   var(--teal-700);
  --brand-sunk:    var(--teal-50);
  --link:          var(--teal-600);

  /* status — bg / text / line triplets, copied structurally from Stripe's
     Connect appearance API, where every badge semantic ships those three. */
  --ok-line:      var(--green-150);
  --warn:         var(--gold-700);    --warn-line:    var(--gold-150);
  --danger-line:  var(--red-150);
  --info:         var(--blue-700);    --info-bg:      var(--blue-50);
  --info-line:    var(--blue-150);
  --neutral:      var(--slate-700);   --neutral-bg:   var(--slate-100);
  --neutral-line: var(--slate-200);
}


/* ==========================================================================
   §4.3  RE-POINTING THE LEGACY VARIABLES — same names, new values.
   DESIGN-NOTES 11: "do not invent a second scale." So nothing is renamed.
   Counted uses in style.css, so the blast radius is on the record:
     --navy 43 · --navy-dark 48 · --border 50 · --text 29 · --accent 26
     --danger 17 · --radius-sm 14 · --shadow 12 · --card 11 · --danger-bg 10
     --warn-text 10 · --shadow-hover 10 · --radius 10 · --ink 9 · --bg 8
     --warn-bg 8 · --gold 7 · --ok 6 · --mint 6 · --ok-bg 2 · --warn-border 2
     --elev-1 2 · --elev-2 2 · --elev-3 1
   ========================================================================== */
:root {
  /* ---- brand aliases ---- */
  --navy:      var(--teal-600);
  --navy-dark: var(--teal-700);
  --accent:    var(--mint-400);
  --mint:      var(--mint-400);
  --bg:        var(--slate-50);
  --card:      #ffffff;
  --border:    var(--slate-150);
  --text:      var(--slate-800);   /* 12.80:1 on white, AAA */
  --muted:     var(--slate-600);
  --ink:       var(--slate-800);

  /* --gold is DELIBERATELY --gold-300, not the spec's --gold-400.
     `.badge.gold` (style.css:249) is `background: var(--gold); color:#4a3300`,
     and the same pairing carries the Forever Pricing / status-pending bubbles
     at lines 282 and 1117 — a DESIGN-NOTES 14 system.
       --gold-400 #bb8600 under #4a3300 = 3.70:1  FAILS AA for small text
       --gold-300 #dca11c under #4a3300 = 5.19:1  passes AA
       (today's #f3b929 = 6.68:1)
     The spec acknowledges this breakage in §6.5 but only repairs it in
     Round 3. Round 1 must not ship a failing contrast for two rounds, so the
     ramp step that keeps the badge legible is the one used here.
     --gold-300 is also within 1 ΔE of --brand-gold #d99e14. */
  --gold:      var(--gold-300);

  /* ---- status families declared in :root #1 (style.css lines 13-19).
          Re-point the WHOLE family or the warning colours end up half-old. -- */
  --ok:           var(--green-700);   --ok-bg:      var(--green-50);
  --danger:       var(--red-700);     --danger-bg:  var(--red-50);
  --warn-text:    var(--gold-700);    --warn-bg:    var(--gold-50);
  --warn-border:  var(--gold-150);

  /* ---- legacy radius + elevation. Without this block the panels and
          shadows keep their old values and most of Round 1 is invisible. ---- */
  --radius:       var(--r-lg);     /* was 14px -> 10px */
  --radius-sm:    var(--r-sm);     /* was 10px ->  6px */
  --shadow:       var(--sh-2);
  --shadow-hover: var(--sh-3);
  --elev-1:       var(--sh-1);
  --elev-2:       var(--sh-2);
  --elev-3:       var(--sh-3);

  /* --ring is NOT re-pointed, on purpose. Globally it is a box-shadow value
     (style.css:2058); inside .viz-root it is a COLOUR (style.css:3477).
     Touching it produces garbage on the Financials page. */

  /* ---- house palette — SAME NAMES, new values (DESIGN-NOTES 5).
          1 Dreyfous teal · 2 Phosphor COPPER · 3 Seminole blue.
          (DESIGN-NOTES 5 still says Phosphor is gold; round .22 recoloured it
          to copper and tests/palette_test.py's docstring records that. The
          note is stale, the copper is current.)
          The paired inline gradients in view_census_tracking are updated in
          the same round — change one, change the other. ---- */
  --h1-bg:var(--teal-50);     --h1-fg:var(--teal-700);     --h1-dot:var(--teal-500);
  --h1-line:var(--teal-150);  --h1-solid-a:var(--teal-500); --h1-solid-b:var(--teal-700);
  --h1-bar-a:var(--teal-300);
  --h2-bg:var(--copper-50);   --h2-fg:var(--copper-700);   --h2-dot:var(--copper-500);
  --h2-line:var(--copper-150);--h2-solid-a:var(--copper-400);--h2-solid-b:var(--copper-700);
  --h2-bar-a:var(--copper-200);
  --h3-bg:var(--blue-50);     --h3-fg:var(--blue-700);     --h3-dot:var(--blue-500);
  --h3-line:var(--blue-150);  --h3-solid-a:var(--blue-400); --h3-solid-b:var(--blue-700);
  --h3-bar-a:var(--blue-200);
}


/* ==========================================================================
   §4.4  TYPE
   Measured live on docs.stripe.com across 9,561 elements: body 14px / 18.2px
   (ratio 1.30) at weight 400. Stripe's density is a LINE-HEIGHT decision, not
   a font-size decision — 14px at 1.30 reads denser than 13px at 1.55.

   The --lh-* prefix already exists in style.css (--lh-tight/snug/normal,
   unitless ratios). Those three are left alone; the px pairs below extend the
   same prefix 1:1 with --fs-*, rather than inventing a second scale.
   ========================================================================== */
:root {
  --fs-2xs: 11px;  --lh-2xs: 15px;   /* micro labels, th */
  --fs-xs:  12px;  --lh-xs:  16px;   /* badges, captions, eyebrows */
  --fs-sm:  13px;  --lh-sm:  18px;   /* dense cells, help text */
  --fs-md:  14px;  --lh-md:  18px;   /* DEFAULT — body, inputs, buttons, td */
  --fs-base:16px;  --lh-base:22px;   /* DO NOT LOWER — see the warning below */
  --fs-lg:  16px;  --lh-lg:  22px;   /* h3 / panel titles */
  --fs-xl:  20px;  --lh-xl:  26px;   /* h2 */
  --fs-2xl: 24px;  --lh-2xl: 30px;   /* h1 */
  --fs-3xl: 30px;  --lh-3xl: 34px;   /* stat numbers */
  --fs-4xl: 38px;  --lh-4xl: 42px;   /* hero figures */

  --fw-normal:400; --fw-medium:500; --fw-semi:600; --fw-bold:700;
  --tracking-tight:-0.011em;
  --tracking-caps: 0.06em;
}

/* --fs-base MUST STAY 16px. style.css:4897, inside @media (max-width: 760px),
   is `input, select, textarea { font-size: var(--fs-base) }` — the iOS
   no-zoom floor expressed as a token (DESIGN-NOTES 9). --fs-base also has 58
   uses across the sheet; it is a second body size, not a "lead paragraph"
   token. It was 1rem before this file and is 16px now: the same computed
   value, stated in the unit the rest of this block uses. */

/* THE BODY DENSITY RULE IS DESKTOP-ONLY, AND THAT IS A DELIBERATE DEPARTURE
   FROM THE SPEC.
   The spec's §4.4 sets `body { font-size: var(--fs-md) }` globally, which
   would take phones from 16px to 14px. DESIGN-NOTES 9 and PROJECT-INSTRUCTIONS
   are explicit that Round .20 made phones deliberately TALLER than desktop and
   that "desktop-only rules go inside @media (min-width: 761px)". A global body
   rule harmonises the two densities, which is the thing both documents forbid,
   and the spec's own §2 names DESIGN-NOTES as the authority. So: desktop gets
   Stripe's 14px/18px; the phone keeps 16px.
   style.css:5033 is `body { font-size: var(--fs-base); line-height:
   var(--lh-normal) }` at 0-0-1 — matched here, not escalated. */
@media (min-width: 761px) {
  body { font-size: var(--fs-md); line-height: var(--lh-md); }
}


/* ==========================================================================
   §4.5  SPACING AND RADII
   Spacing: --sp-* is a 4px ramp and is NOT changed. Moving the spacing rhythm
   mid-flight would move every page on every round.
   Radii, measured live off Stripe: small radii on controls, larger on
   surfaces — a 4px button inside a 10px card. Observed usage 4px x165,
   8px x85, 6px x17.

   THIS IS THE MOST VISIBLE SINGLE CHANGE IN ROUND 1. ~107 references shift:
     --r-sm   8px -> 6px   (30 uses, incl. every button at style.css:5143)
     --r-md  10px -> 8px   (18 uses)
     --r-lg  12px -> 10px  (29 uses, incl. .panel)
     --r-xl  16px -> 14px  (6 uses)
     --radius    14px -> --r-lg  10px  (10 uses)
     --radius-sm 10px -> --r-sm   6px  (14 uses)
     --r-xs and --r-pill are unchanged.
   To dial it back, raise the six values here; nothing else has to move.

   --r-pill stays 999px rather than the spec's 999em. Both clamp to a full
   pill, so the two are visually identical, but 999em recomputes to 15984px
   and shows up as a "changed" value on all four rate chips and every status
   bubble — DESIGN-NOTES 14 surfaces that should read as untouched in a
   before/after diff. No benefit, real noise, so it keeps its current value.
   ========================================================================== */
:root {
  --r-xs:4px;  --r-sm:6px;  --r-md:8px;  --r-lg:10px;  --r-xl:14px;  --r-pill:999px;
}


/* ==========================================================================
   §4.6  ELEVATION AND THE KEYLINE
   Sampling 5,000 elements on docs.stripe.com returned 0px border-width on
   ~1,950 of them. Stripe draws every hairline, focus ring and elevation as a
   slot in ONE composite box-shadow — their tokens even name the slots
   (--s--top-shadow, --s--keyline-width: 1px, --s--focus-ring, --s--box-shadow).
   A hairline drawn this way costs zero layout, so hover states stop reflowing
   and focus rings compose instead of collide.

   Opacities here are 5-8%. style.css currently ships several at 28-35%
   (0 2px 8px rgba(28,79,84,.35) on the active nav item; 0 18px 50px
   rgba(10,30,30,.28) on the confirm box). Dropping to single digits is most
   of the "expensive" feel.

   NOTE: the components themselves are NOT restyled in Round 1 — that is
   Rounds 2-7. What ships here is the token set plus the re-pointed --shadow /
   --shadow-hover / --elev-* above, which is what carries the change onto the
   ~27 existing references.
   ========================================================================== */
:root {
  --sh-key:   0 0 0 1px var(--line);
  --sh-key-2: 0 0 0 1px var(--line-strong);
  --sh-0: 0 0 #0000;
  --sh-1: 0 1px 1px rgba(16,17,26,.06);
  --sh-2: 0 1px 2px rgba(16,17,26,.05), 0 3px 6px rgba(42,52,53,.05);
  --sh-3: 0 3px 6px rgba(16,17,26,.06), 0 7px 14px rgba(42,52,53,.06);
  --sh-4: 0 8px 16px rgba(16,17,26,.07), 0 18px 36px rgba(42,52,53,.08);
}


/* ==========================================================================
   §4.7  MOTION
   style.css has 43 transition declarations, 37 distinct, no token, with
   durations scattered across .08s .1s .12s .13s .14s .15s .16s .2s .35s.
   Round 1 declares the scale; the existing 43 survive until Round 8.
   Stripe published no UI durations (Villar deliberately withheld them);
   De Cock's public easings are decorative at ~500ms, which is far too slow
   for a state change.
   ========================================================================== */
:root {
  --t-fast:  90ms;   /* colour, background, opacity */
  --t-base: 150ms;   /* shadow, transform */
  --t-slow: 240ms;   /* panels, drawers */
  --ease:     cubic-bezier(.2,.7,.3,1);
  --ease-out: cubic-bezier(.16,1,.3,1);
}
/* The reduced-motion kill switch at style.css:2255 (* { transition: none
   !important }) stays. It is one of the few !important rules worth keeping. */


/* ==========================================================================
   §4.8  FOCUS — this must be BETTER than the reference, not equal to it.
   style.css has eight `outline:` declarations in 5,625 lines: two are
   `outline: none` (481, 2182) and six are real rings (3623, 3913, 4329, 4502,
   5150, 5502).

   Two traps, both real:
   1. `:where(...):focus-visible` is 0-1-0, NOT 0-0-0 — :focus-visible is a
      real pseudo-class. On its own it LOSES to every incumbent below, and the
      result is a DOUBLE ring: the old 2px outline plus the new one. Every
      incumbent is therefore named verbatim.
   2. A 3px ring at 18% alpha is 1.23:1 on white. WCAG 2.2 SC 1.4.11 requires
      3:1 for a focus indicator. The white spacer ring below keeps the
      indicator legible on any background at 6.98:1.

   `.cens-older-check:focus-visible ~ .cens-older-label` (style.css:5502,
   0-3-0) is deliberately NOT overridden: it puts the outline on a SIBLING
   label, and the checkbox itself is 1px/opacity:0 (5487), so the new ring
   lands on an invisible element and the label keeps its working outline.
   That rule was added after the spec was written.
   ========================================================================== */
:root {
  /* white spacer ring + brand ring: 6.98:1, legible on any background */
  --sh-ring: 0 0 0 2px var(--surface), 0 0 0 4px var(--teal-600);
}

:where(a, button, .button, input, select, textarea, summary, [tabindex]):focus-visible,
button:focus-visible, .button:focus-visible, a.button:focus-visible,
.icon-btn:focus-visible,
.confirm-cancel:focus-visible, .confirm-ok:focus-visible,
.fin-tab:focus-visible,
.fin-presets button:focus-visible {
  outline: none;
  box-shadow: var(--sh-ring);
}

:where(a, button, .button, input, select, textarea, summary, [tabindex]):focus:not(:focus-visible) {
  outline: none;
}

/* Typed text inputs keep their existing focus treatment for now: style.css:
   2180 is `input[type="text"], ... select, textarea:focus` at 0-2-1 and wins
   over the 0-1-0 :where() above. Unifying it means restating that whole typed
   selector list, which is Round 2's job (spec §6.6). Pulling it forward here
   would put input geometry into a round that is supposed to be tokens only. */

/* ==========================================================================
   END ROUND 1. Rounds 2-8 append below this line, one round per deploy.
   ========================================================================== */


/* ==========================================================================
   ROUND 2 — BUTTONS + INPUTS  (spec §6.4, §6.6)         08/06/2026
   Deploys as 2026.07.22.20. Round 1 (.19) is live and unchanged above.

   Every selector below is copied VERBATIM from its incumbent in style.css so
   v2 wins on cascade order, never on specificity (DESIGN-NOTES 13c). The
   incumbent's current line number and computed specificity are named on each
   block, because the spec's §3.3 table was written against a 5,404-line file
   and this one is 5,626.

   WHAT THIS ROUND DELIBERATELY DOES NOT DO
   ----------------------------------------
   - No option-grid geometry, and no checkbox or radio is selected anywhere
     below (DESIGN-NOTES 1 / 1b). Those controls are pinned to 15px with nine
     !important declarations at 1547-1549, 1614-1616 and 3303-3305; giving
     them a keyline, a shadow and a 6px radius would visibly change .cp-opt,
     .cp-opts, .checkbox-grid and .yn-row.
   - No .field-label / .field-help / .field-error markup. The app already has
     an informal field anatomy -- a bare <label> plus `.muted.small` (145
     uses) -- so this is a consolidation, not a gap, and it belongs with the
     error microcopy and empty states in Round 7 rather than in the same
     deploy as a control restyle. Shipping the four rules now with nothing
     using them would just be dead CSS for five rounds.
   ========================================================================== */


/* --------------------------------------------------------------------------
   §6.4  BUTTONS

   Stripe's rules: primary is ONE per context; secondary is the default;
   destructive is only for actions that destroy data. No lift on hover -- the
   elevation change is the feedback, not the movement.
   -------------------------------------------------------------------------- */

/* Base. Incumbents: style.css:5142 (0-0-1 / 0-1-0 / 0-1-1 / 0-1-1) and
   style.css:2147, which gives `button[type="submit"]` (0-1-1) --r-md while a
   plain `.button` gets --r-sm from 5142. Today that is 8px vs 6px on two
   buttons that sit side by side. Both are named here so they agree. */
button, .button, a.button, input[type="submit"] {
  font-family: inherit;
  font-size: var(--fs-md); line-height: 18px;
  font-weight: var(--fw-semi);
  border: 0;
  border-radius: var(--r-sm);
  cursor: pointer;
  /* inline-flex, not inline. `min-height` does not apply to a non-replaced
     INLINE box, and `a.button` was inline -- so style.css:5366's 44px phone
     floor silently missed every link-shaped button. Measured before this
     round: a phone Cancel was 38px beside a 44px primary, against
     DESIGN-NOTES 9. This also centres the label without relying on padding
     symmetry. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  vertical-align: middle;
  transition: background var(--t-fast) var(--ease),
              box-shadow var(--t-base) var(--ease),
              color var(--t-fast) var(--ease);
}
.button, button[type="submit"], .cp-toolbar button { border-radius: var(--r-sm); }

/* PRIMARY — flat teal, no gradient, no lift. Incumbent style.css:2150
   (0-2-0 / 0-2-2). The gradient there carries a 28% shadow; §4.6 puts the
   whole sheet at 5-8%. */
.button:not(.ghost), form button[type="submit"]:not(.ghost) {
  background: var(--brand);
  background-image: none;
  color: var(--text-onbrand);
  box-shadow: var(--sh-1);
  filter: none;
  transform: none;
  transition: background var(--t-fast) var(--ease), box-shadow var(--t-base) var(--ease);
}
/* Incumbent style.css:2155 (0-3-0 / 0-3-2) */
.button:not(.ghost):hover, form button[type="submit"]:not(.ghost):hover {
  background: var(--brand-hover);
  background-image: none;
  box-shadow: var(--sh-2);
  filter: none;
  transform: none;
}
/* Incumbent style.css:2161 (0-3-0 / 0-3-2) */
.button:not(.ghost):active, form button[type="submit"]:not(.ghost):active {
  background: var(--teal-800);
  box-shadow: var(--sh-0);
  transform: none;
  filter: none;
}

/* Padding must live at 0-2-2: style.css:5340 sets `padding: var(--sp-1)
   var(--sp-3)` on exactly this list and would otherwise win. Copied verbatim. */
.button, a.button, form button[type="submit"]:not(.ghost), button {
  padding: 6px 12px;
}

/* SECONDARY — Stripe's default button. Incumbent style.css:2165 (0-2-0),
   which carries `border: 1px solid rgba(40,110,117,.25)`; without `border: 0`
   that hairline stacks under the new keyline as two lines in two colours. */
.button.ghost {
  border: 0;
  background: var(--surface);
  background-image: none;
  color: var(--text);
  box-shadow: var(--sh-key), var(--sh-1);
  transition: background var(--t-fast) var(--ease), box-shadow var(--t-base) var(--ease);
}
.button.ghost:hover {
  background: var(--surface-hover);
  box-shadow: var(--sh-key-2), var(--sh-1);
  border-color: transparent;
}

/* DESTRUCTIVE, text form. Incumbent style.css:874 (0-3-0) / 875 (0-4-0).
   The table overrides are required: `table.data-table a.button` (223, 0-2-2)
   and `table.data-table a.button.ghost` (225, 0-3-2) set `color: var(--navy)`,
   and nearly every delete affordance in this app lives inside a table. */
.button.ghost.danger, .btn-inline-danger {
  border: 0;
  color: var(--danger);
  background: var(--surface);
  box-shadow: 0 0 0 1px var(--danger-line), var(--sh-1);
}
.button.ghost.danger:hover {
  background: var(--danger-bg);
  color: var(--danger);
  box-shadow: 0 0 0 1px var(--red-200), var(--sh-1);
}
/* `.btn-inline-danger` (0-1-0) is NOT protected by :not(.ghost) -- it carries
   no .ghost class -- so the PRIMARY rule above (0-2-2) wins on any
   <button type="submit"> and paints it teal with white text. That is the exact
   Round .19 bug (style.css:5247) one component over: the two buttons using
   this class were only surviving because they carried an inline style, and
   this round removed those because an inline `border: 1px` draws a second
   line under the keyline. Matched here the same way Round .19 matched
   `.icon-btn-danger` -- by naming the component's own class in a selector that
   reaches 0-3-2. Verified: without this the Projections "Delete" and the Users
   "Remove permanently" buttons render as teal primaries. */
.btn-inline-danger,
form button.btn-inline-danger[type="submit"]:not(.ghost) {
  border: 0;
  color: var(--danger);
  background: var(--surface);
  background-image: none;
  box-shadow: 0 0 0 1px var(--danger-line), var(--sh-1);
}
.btn-inline-danger:hover,
form button.btn-inline-danger[type="submit"]:not(.ghost):hover {
  background: var(--danger-bg);
  color: var(--danger);
  box-shadow: 0 0 0 1px var(--red-200), var(--sh-1);
}
table.data-table a.button.ghost.danger, table.data-table .btn-inline-danger {
  color: var(--danger);
}
table.data-table a.button.ghost.danger:hover { color: var(--danger); }

/* SMALL. Incumbent style.css:5348 (0-2-0 / 0-1-1 / 0-3-2). */
.button.small, button.small, form button.small[type="submit"]:not(.ghost) {
  font-size: var(--fs-xs);
  padding: 4px 8px;
  border-radius: var(--r-xs);
}

/* ...EXCEPT inside a .form-actions row.
   Incumbent style.css:2357 (0-3-0) is Round XX-4: "Cancel buttons match the
   size of their primary button wherever they sit in an action row." Scott
   asked for that specifically.
   The spec's §6.4 would set this to 12px / 4px 8px, which makes Cancel
   SMALLER than the primary beside it -- and then §9's own Round 2 check says
   "confirm .form-actions Cancel and primary are the same size." Those two
   contradict. The existing rule and the existing check agree with each other,
   so the size parity wins: in an action row a small button is primary-sized. */
.form-actions .button.small {
  font-size: var(--fs-md);
  padding: 6px 12px;
  border-radius: var(--r-sm);
}

/* PENDING — the affordance you clicked shows progress, not the page.
   Set by static/js/confirm-dialog.js on submit. */
.button.is-pending, button.is-pending {
  pointer-events: none;
  opacity: .65;
}
.button.is-pending::after, button.is-pending::after {
  content: "";
  width: 12px; height: 12px; margin-left: 6px;
  display: inline-block; vertical-align: -2px;
  border: 2px solid currentColor; border-right-color: transparent;
  border-radius: 50%;
  animation: btnspin .6s linear infinite;
}
@keyframes btnspin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .button.is-pending::after, button.is-pending::after { animation: none; }
}

/* ICON BUTTONS. Incumbents style.css:5256 (0-1-0 / 0-3-2), 5266, 5270, 5274.
   `.icon-btn:hover { transform: translateY(-1px) }` at 4501 (0-2-0) must be
   reset explicitly or "no lift" never reaches icon buttons.
   The bare `button` rule at 486 sets `color: white`; an icon draws in
   currentColor, so every one of these states names its own colour or the
   glyph vanishes on a white chip (DESIGN-NOTES 13). */
.icon-btn,
form button.icon-btn[type="submit"]:not(.ghost) {
  width: 32px; height: 32px; padding: 0;
  border: 0;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--surface);
  color: var(--text-muted);
  box-shadow: var(--sh-key);
  border-radius: var(--r-sm);
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease),
              box-shadow var(--t-base) var(--ease);
}
.icon-btn:hover {
  background: var(--surface-hover);
  color: var(--text);
  box-shadow: var(--sh-key-2);
  transform: none;
}

/* Delete stays NEUTRAL until you point at it, and that is deliberate.
   style.css:4504 -- "neutral by default so a table isn't a wall of red ... It
   turns red only when you're actually pointing at it" -- and 4748, "Delete
   button, take five. Scott: keep the emoji, lighten the bubble. The earlier
   white-on-red-ring read as an alert."
   The spec's §6.4 would ship exactly that rejected white-chip-with-red-ring
   as the DEFAULT state. It is restyled onto the v2 tokens here, but the
   neutral -> red behaviour Scott asked for is preserved. */
.icon-btn-danger,
form button.icon-btn-danger[type="submit"]:not(.ghost) {
  border: 0;
  background: var(--surface);
  color: var(--text-muted);
  box-shadow: var(--sh-key);
}
.icon-btn-danger:hover,
form button.icon-btn-danger[type="submit"]:not(.ghost):hover {
  background: var(--danger-bg);
  color: var(--danger);
  box-shadow: 0 0 0 1px var(--danger-line);
  transform: none;
}
.icon-btn-edit,
form button.icon-btn-edit[type="submit"]:not(.ghost) {
  border: 0;
  background: var(--brand-sunk);
  color: var(--teal-700);
  box-shadow: 0 0 0 1px var(--teal-150);
}
.icon-btn-edit:hover {
  background: var(--teal-100);
  color: var(--teal-800);
  box-shadow: 0 0 0 1px var(--teal-200);
  transform: none;
}

/* The phone floor stays exactly as Round .20 set it (DESIGN-NOTES 9).
   Incumbents style.css:5366 / 5367, copied verbatim. */
@media (max-width: 760px) {
  .button, a.button, form button[type="submit"]:not(.ghost), button { min-height: 44px; }
  .icon-btn, form button.icon-btn[type="submit"]:not(.ghost) {
    min-height: 0; width: 44px; height: 44px;
  }
  /* style.css:4862 sets `.button.small, button.small { min-height: 38px }` on a
     phone (0-2-0), which beats the 44px floor above (0-1-0 on `.button`). That
     38px is a deliberate Round .20 choice for small buttons generally and is
     left alone -- EXCEPT in an action row, where Round XX-4 (style.css:2357)
     says Cancel matches its primary and DESIGN-NOTES 9 asks for 44px targets.
     Measured before this rule: a phone Cancel was 38px beside a 44px primary.
     0-3-0, scoped to .form-actions, so no other small button moves. */
  .form-actions .button.small, .form-actions button.small { min-height: 44px; }
  /* BACKLOG 72 turned Generate EMS Sheet from an <a class="button small"> into
     a form button, so that one control started matching
     `form button[type="submit"]:not(.ghost)` (0-2-2) above and took the 44px
     floor, while `button.small` (0-1-1) lost. On a phone it came out SIX
     PIXELS taller than the four sibling links beside it in the same band --
     which is exactly what made documents-m and profile-m grow 6px in this
     round's screenshot diff. Matched at 0-4-2 rather than escalating, the same
     move line 552 already makes for this pair on padding and Round .19 made
     for .icon-btn-danger.
     Scoped to .profile-actions on purpose: an unscoped 0-3-2 rule would
     outrank the .form-actions 44px rule directly above (0-3-0) and undo Round
     XX-4's deliberate "Cancel matches its primary" choice. */
  .profile-actions form button.small[type="submit"]:not(.ghost) { min-height: 38px; }
}


/* --------------------------------------------------------------------------
   §6.6  INPUTS

   DESKTOP ONLY, and that is load-bearing rather than stylistic. style.css
   has THREE rules on the same eight-selector typed list, all at 0-1-1:
     4589  height: 40px   (one box model)
     5319  height: 32px   (Round .20 compact density -- the winner today)
     5357  height: 44px; font-size: 16px  -- inside @media (max-width: 760px)
   A media query adds NO specificity. An unscoped 0-1-1 rule here would load
   later than all three and beat the phone floor at 5357, taking iOS inputs
   below 16px and making the page zoom on focus -- a direct DESIGN-NOTES 9
   violation. So the geometry is wrapped in min-width: 761px and the phone
   floor is restated at the bottom.

   Checkboxes and radios are excluded from every rule below (DESIGN-NOTES 1).
   -------------------------------------------------------------------------- */
/* APPEARANCE is unscoped: border, radius, keyline, focus ring and colour are
   one visual language at every width. Only the GEOMETRY below is split, which
   is what DESIGN-NOTES 9 actually asks for -- a phone gets bigger controls,
   not different-looking ones.
   Nothing here sets height or font-size, so the two geometry blocks that
   follow (equal specificity, later in this file) each own their own width. */
input[type="text"], input[type="email"], input[type="tel"], input[type="number"],
input[type="date"], input[type="password"], input[type="search"], select {
  font-family: inherit;
  color: var(--text);
  background: var(--surface);
  border: 0;
  border-radius: var(--r-sm);
  box-sizing: border-box;
  box-shadow: var(--sh-key), var(--sh-1);
  transition: box-shadow var(--t-base) var(--ease);
}
textarea {
  font-family: inherit;
  color: var(--text);
  background: var(--surface);
  border: 0;
  border-radius: var(--r-sm);
  padding: 6px 8px;
  box-shadow: var(--sh-key), var(--sh-1);
  transition: box-shadow var(--t-base) var(--ease);
}
input[type="text"]:hover, input[type="email"]:hover, input[type="tel"]:hover,
input[type="number"]:hover, input[type="date"]:hover, input[type="password"]:hover,
input[type="search"]:hover, select:hover, textarea:hover {
  box-shadow: var(--sh-key-2), var(--sh-1);
}
/* Restated to match style.css:2180's 0-2-1, which sets the old mint glow.
   Round 1 left this alone on purpose; this is where it is unified, so every
   control in a form shares one focus indicator at 6.98:1. */
input[type="text"]:focus, input[type="email"]:focus, input[type="tel"]:focus,
input[type="number"]:focus, input[type="date"]:focus, input[type="password"]:focus,
input[type="search"]:focus, select:focus, textarea:focus {
  outline: none;
  border-color: transparent;
  background: var(--surface);
  box-shadow: var(--sh-ring);
}

/* DESKTOP GEOMETRY ONLY.
   style.css has THREE rules on this same eight-selector list, all at 0-1-1:
     4589  height: 40px   (one box model)
     5319  height: 32px   (Round .20 compact density -- the winner today)
     5357  height: 44px; font-size: 16px  -- inside @media (max-width: 760px)
   A media query adds NO specificity, so an UNSCOPED height here would load
   later than all three and beat the phone floor at 5357, taking iOS inputs
   below 16px and zooming the page on focus. Hence the split. */
@media (min-width: 761px) {
  input[type="text"], input[type="email"], input[type="tel"], input[type="number"],
  input[type="date"], input[type="password"], input[type="search"], select {
    height: 32px;
    padding: 0 8px;
    font-size: var(--fs-md);
  }
  textarea { font-size: var(--fs-md); }
}

/* Invalid. These three are the ONLY !important declarations in style-v2.css,
   and they exist solely to beat style.css:353's existing pair on
   `input.date-bad`, which is itself !important. Nothing in style.css after
   line 4440 uses one; that record is otherwise kept. */
.is-invalid, input.date-bad {
  box-shadow: 0 0 0 1px var(--red-500) !important;
  background: var(--danger-bg) !important;
}
.is-invalid:focus, input.date-bad:focus {
  box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--red-500) !important;
}

/* Button placement is a property of the container, not of each call site.
   CANCEL GOES LAST (Scott, 08/06/2026). Round 2 shipped Stripe's convention --
   primary on the right, Cancel to its left -- and he asked for it back the
   other way after seeing it live. His app, his muscle memory. The size parity
   from Round XX-4 is unaffected; only the order changed. */
.form-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.form-actions .button:not(.ghost), .form-actions button[type="submit"]:not(.ghost) { order: 1; }
.form-actions .button.ghost, .form-actions button.ghost { order: 2; }

/* The phone floor, restated (DESIGN-NOTES 9). 44px targets, 16px text so iOS
   does not zoom the page on focus -- textarea included. */
@media (max-width: 760px) {
  input[type="text"], input[type="email"], input[type="tel"], input[type="number"],
  input[type="date"], input[type="password"], input[type="search"], select {
    height: 44px;
    font-size: 16px;
    padding-top: 0; padding-bottom: 0;
  }
  textarea { font-size: 16px; min-height: 88px; }
}

/* ==========================================================================
   END ROUND 2. Rounds 3-8 append below this line, one round per deploy.
   ========================================================================== */


/* ==========================================================================
   ROUND 3 — TABLES + BADGES  (spec §6.2, §6.5)          08/06/2026
   Deploys as 2026.07.22.21. Rounds 1-2 above are unchanged.

   THREE THINGS THE SPEC ASKS FOR THAT THIS ROUND DOES NOT DO, EACH FOR A
   REASON THAT IS IN THE REPO RATHER THAN IN THE SPEC:

   1. ZEBRA STRIPING STAYS. §6.2 says "do not add zebra striping ... hairline +
      hover is Stripe's model". But style.css:2558 is Round AAA: "Zebra banding
      for every data table -- the EMR look, consistent site-wide." That is a
      deliberate, named, care-industry decision, and removing it site-wide is
      not something Scott asked for. So the banding stays and the competing
      per-row hairline goes instead -- the two systems were doing the same job
      twice. The header keyline stays, because that is structure, not
      separation.

   2. NO `border-collapse: separate`. The spec calls it "required", but only
      because it assumed per-row borders that would otherwise double. With the
      row hairlines gone there is nothing to double, and changing a table's box
      model is a layout risk with no visible payoff here.

   3. NO `.num` / `.n` CLASS FOR TABULAR FIGURES. `class="num"` appears ZERO
      times in app.py and zero times in style.css, so the spec's
      `td.num, th.num { font-variant-numeric: tabular-nums }` would match
      nothing at all. Applied to every `td` instead: it is one declaration,
      needs no markup in ~40 tables, and only affects digits.
   ========================================================================== */


/* --------------------------------------------------------------------------
   §6.2  TABLES  --  DESKTOP ONLY.

   Below 761px `table.data-table` is not a table: style.css:1324-1355 turns it
   into one card per row and `static/js/mobile-tables.js` copies each <th> into
   `td[data-label]`, which the CSS renders through `::before`. Those rules are
   0-1-2, the same as the ones here, so an unscoped v2 rule would win on
   cascade order and put a hairline and 8px/12px padding back on every field
   inside every card.

   Nothing below touches `content`, `text-indent` or `display` on a `th`, so
   the label extraction is unaffected.
   -------------------------------------------------------------------------- */
@media (min-width: 761px) {

  /* Header. Matches style.css:2132 `table.data-table thead th` at 0-1-3, which
     is the highest-specificity header rule (11px/700/0.09em/#7b8a90 with a 2px
     bottom border). The plain 0-1-2 form is named too, for the few tables that
     have no <thead>. */
  table.data-table thead th,
  table.data-table th {
    font-size: var(--fs-2xs);
    line-height: var(--lh-2xs);
    font-weight: var(--fw-semi);          /* was 700 */
    text-transform: uppercase;
    letter-spacing: var(--tracking-caps);
    color: var(--text-subtle);
    text-align: left;
    padding: 6px 12px;
    background: var(--surface);
    border: 0;
    box-shadow: inset 0 -1px 0 var(--line);
  }

  /* Cells. `border: 0` retires the per-row hairline (style.css:2143 and 5135);
     the zebra band already separates rows and running both reads as noise.
     tabular-nums makes every money and count column line up -- it is the
     difference between a column of figures that reads and one that doesn't. */
  table.data-table td {
    font-size: var(--fs-md);
    line-height: var(--lh-md);
    color: var(--text);
    padding: 8px 12px;
    border: 0;
    vertical-align: middle;
    font-variant-numeric: tabular-nums;
  }

  table.data-table tbody tr {
    transition: background var(--t-fast) var(--ease);
  }

  /* HOVER, and this one is measured rather than chosen.
     The winning hover today is style.css:5139 `#fbfdfc`, which is
       1.46 dE from white and 1.46 dE from the #f6faf8 zebra band
     -- so hovering a row is very nearly invisible, on every table in the app.
     Rounds .12 and .16 were both shipped bugs of exactly this shape, so this
     value was picked by measuring CIEDE2000 against every row tint that
     actually WINS in the cascade:
       vs white               6.04
       vs zebra #f6faf8       4.01
       vs today's hover       4.98
       vs proj-revenue        8.57   (Round .12's fix, preserved)
       vs pb-group            6.52   (Round .16's fix, preserved)
       vs due-over/today     10.97 / 10.43
       worst of all fifteen   3.37   (vs the house zebra and due-soon, which
                                      are other rows' RESTING states)
     0-2-3, matching style.css:5139. */
  table.data-table tbody tr:hover { background: #e6f2f4; }

  /* The house tables tuned their own hover (style.css:2293) and it is equal
     specificity, so without this the rule above would silently take it over.
     Restated verbatim -- consolidating it is not this round's job. */
  table.house-table tbody tr:hover { background: #ecf6f1; }

  /* style.css:2144 `tr.vacant-row:hover { background: transparent !important }`
     still wins, so an empty room keeps not lighting up. Nothing needed here. */
}


/* --------------------------------------------------------------------------
   §6.5  BADGES

   Stripe's geometry: 6px x 2px padding, 4px radius, medium weight -- not a
   pill, not bold, not uppercase. Every variant is a bg / text / line triplet,
   which is what makes a status pill read crisp rather than blobby.

   DESIGN-NOTES 14 BOUNDARY, AND IT IS A HARD ONE:
   only `.badge` gets this geometry. `.meta-bubble`, `.resident-name-bubble`,
   `.score-pill`, `.tier-pill`, `.side-tag-poa`, `.side-tag-evac`,
   `.forever-pill` and the four rate chips (`.meta-forever`,
   `.meta-fee-tpaf-k`, `.meta-fee-tpaf`, `.meta-fee-rrf`) keep their OWN hues
   AND their pill shape -- they are the "data points that stand out" Scott
   asked for. No rule below selects any of them. Do not merge the two systems.
   -------------------------------------------------------------------------- */
.badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 6px;
  border: 0;
  border-radius: var(--r-xs);
  font-size: var(--fs-xs);
  line-height: 16px;
  font-weight: var(--fw-medium);      /* was 700 */
  letter-spacing: 0;
  text-transform: none;
  vertical-align: middle;
  /* THE BASE KEEPS A CHIP, and this is not cosmetic. style.css:233 gives every
     .badge a default `background: #e7f4f1; color: var(--navy)`, and FIVE call
     sites rely on it -- app.py:1733 (document type), 5034 ("Excused"), 5196,
     and priority_badge() at 9693. Dropping the default here rendered those as
     bare text with no chip at all, which a screenshot caught on the Facility
     page. Enumerated variants below override all three properties. */
  background: var(--brand-sunk);
  color: var(--teal-700);
  /* An INSET hairline, so it reads correctly over a background this rule does
     not control -- priority_badge() and the "Unexcused" chip set theirs inline
     (app.py:9693, 5032) and a coloured outset ring would clash with them.
     Same technique as style.css:2189, one step darker. */
  box-shadow: inset 0 0 0 1px rgba(18, 41, 44, 0.10);
}

/* The variants app.py actually emits today. `active`, `moved_out`, `pending`
   and `never_moved_in` are the four values of residents.status, and
   STATUS_LABELS (app.py:10793) already maps them to their display text. */
.badge.active          { background: var(--ok-bg);      color: var(--ok);
                         box-shadow: 0 0 0 1px var(--ok-line); }
.badge.moved_out       { background: var(--neutral-bg); color: var(--neutral);
                         box-shadow: 0 0 0 1px var(--neutral-line); }
.badge.never_moved_in  { background: var(--neutral-bg); color: var(--neutral);
                         box-shadow: 0 0 0 1px var(--neutral-line); }
.badge.pending         { background: var(--warn-bg);    color: var(--warn);
                         box-shadow: 0 0 0 1px var(--warn-line); }
/* `.badge.primary` was `background: var(--navy); color: white` -- a solid teal
   chip that competed with the primary BUTTON beside it. Sunk, not solid. */
.badge.primary         { background: var(--brand-sunk); color: var(--teal-700);
                         box-shadow: 0 0 0 1px var(--teal-150); }
/* `.badge.gold` gets an explicit rule ON PURPOSE. Without it the chip falls
   through to the base and inherits the re-pointed --gold as a background under
   #4a3300 text. It is the Forever Pricing chip -- a DESIGN-NOTES 14 colour --
   so it keeps a gold identity.
   (`.badge.pending-date` had a rule here for the same reason until Round 8,
   which proved nothing in app.py has ever rendered that class.) */
.badge.gold            { background: var(--gold-50);    color: var(--gold-700);
                         box-shadow: 0 0 0 1px var(--gold-150); }
/* One real semantic name, used by the BADGE_CLASS fallback in app.py so a
   status nobody has a rule for still renders as a badge instead of bare text.
   The other five names the spec proposes (positive/negative/warning/urgent/
   info) are NOT declared here: nothing emits them, and five dead rules for
   five rounds is what Round 8 exists to clean up. */
.badge.neutral         { background: var(--neutral-bg); color: var(--neutral);
                         box-shadow: 0 0 0 1px var(--neutral-line); }
.badge.house-none      { background: var(--neutral-bg); color: var(--neutral);
                         box-shadow: 0 0 0 1px var(--neutral-line); }
/* House badges keep the --hN-* palette (DESIGN-NOTES 5). Only the ring is new. */
.badge.house-1 { background: var(--h1-bg); color: var(--h1-fg); box-shadow: 0 0 0 1px var(--h1-line); }
.badge.house-2 { background: var(--h2-bg); color: var(--h2-fg); box-shadow: 0 0 0 1px var(--h2-line); }
.badge.house-3 { background: var(--h3-bg); color: var(--h3-fg); box-shadow: 0 0 0 1px var(--h3-line); }

/* `.house-title-bubble` carries the `badge` class too
   (app.py: class="badge house-{idx} house-title-bubble"), so the geometry
   above would shrink the house headings -- a DESIGN-NOTES 14 identity
   surface. style.css:1137 and 5103 are restated here, after the badge rules,
   so the heading keeps its size, weight and shape. */
.house-title-bubble {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  font-size: var(--fs-base);
  font-weight: var(--fw-bold);
  padding: 0.45rem 1rem;
  border-radius: var(--r-pill);
  line-height: 1.2;
}

/* ==========================================================================
   END ROUND 3. Rounds 4-8 append below this line, one round per deploy.
   ========================================================================== */


/* ==========================================================================
   ROUND 4 — APP SHELL  (spec §6.1)                      08/06/2026
   Deploys as 2026.07.22.22. Rounds 1-3 above are unchanged.

   PROTOTYPED AGAINST THE RENDERED DOM, NOT WRITTEN BLIND -- the spec is
   emphatic about this and it was right to be. layout() actually emits:

     <body class="has-rail">
       <input  id="menu-toggle" class="menu-toggle-checkbox">   display:none
       <aside  class="sidebar">                                 position:fixed
       <label  class="sidebar-backdrop">                        display:none
       <header class="topbar">                                  position:sticky
       <main   class="container">
       <script> x4                                              display:none

   So `.sidebar` is a BODY-level sibling of `.container`, not inside it. The
   spec's earlier draft wrote `body > .container { display: grid }`, which
   would have left column one empty and auto-placed .container's own children
   alternately across both columns -- every other panel 232px wide.

   Turning BODY into the grid works because the other five children are already
   out of the running: the checkbox (style.css:641) and the backdrop
   (style.css:670) are `display: none`, and so are scripts. That leaves exactly
   three in-flow children for three named areas. `.sidebar` has to come off
   `position: fixed` to become a grid item at all.

   `class="has-rail"` is set by layout() only when a sidebar was rendered.
   Without it the LOGIN page -- which has no sidebar -- would get an empty
   232px first column and sit shoved to the right. `body:has(> .sidebar)`
   would say the same thing without touching markup, but if :has() ever fails
   the failure mode is a broken front door on a system holding PHI.

   VERIFIED, because the spec flagged it as an assumption: `.nav-search` is a
   SIBLING of `<nav>` inside `.topbar`, not a child of it. `.topbar nav
   { display: none }` therefore does NOT remove desktop resident search.
   ========================================================================== */

@media (min-width: 1025px) {
  body.has-rail {
    display: grid;
    grid-template-columns: 232px minmax(0, 1fr);
    grid-template-rows: auto minmax(0, 1fr);
    grid-template-areas:
      "rail top"
      "rail main";
    min-height: 100vh;
  }

  body.has-rail > .sidebar {
    grid-area: rail;
    /* Clear every offset the off-canvas drawer set (style.css:653-666), or the
       element stays pinned to the viewport and the rail column renders empty. */
    position: sticky;
    top: 0;
    left: auto;
    right: auto;
    bottom: auto;
    transform: none;
    width: auto;
    height: 100vh;
    z-index: 1;
    background: var(--surface);
    border: 0;
    box-shadow: 1px 0 0 var(--line);
    padding: 16px 12px;
    overflow-y: auto;
    overscroll-behavior: contain;
  }
  /* The drawer's open state must not translate the rail on a desktop. */
  body.has-rail > .menu-toggle-checkbox:checked ~ .sidebar { transform: none; }
  body.has-rail > .menu-toggle-checkbox:checked ~ .sidebar-backdrop { display: none; }

  body.has-rail > .topbar    { grid-area: top; }
  body.has-rail > .container { grid-area: main; }

  .hamburger-btn, .sidebar-close, .sidebar-backdrop { display: none; }
  /* The nav lives in the rail on desktop. .nav-search is a sibling and stays. */
  .topbar nav { display: none; }

  /* The rail's own links. */
  .sidebar-section-label {
    font-size: var(--fs-2xs);
    font-weight: var(--fw-semi);
    text-transform: uppercase;
    letter-spacing: var(--tracking-caps);
    color: var(--text-subtle);
    padding: 0 8px;
    margin: 16px 0 4px;
  }
  .sidebar a {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 6px 8px;
    border: 0;
    border-radius: var(--r-sm);
    font-size: var(--fs-md);
    line-height: var(--lh-md);
    font-weight: var(--fw-medium);
    color: var(--text-muted);
    text-decoration: none;
    transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
  }
  .sidebar a:hover  { background: var(--surface-hover); color: var(--text); }
  .sidebar a.active { background: var(--brand-sunk); color: var(--teal-700);
                      font-weight: var(--fw-semi); }
  .sidebar a.active .ico, .sidebar a.active .side-icon { color: var(--brand); }
  .sidebar .side-icon { display: inline-flex; align-items: center; }

  /* The topbar becomes a thin utility strip: the brand lockup, search, user. */
  .topbar {
    background: var(--surface);
    backdrop-filter: none;
    border: 0;
    box-shadow: 0 1px 0 var(--line);
    padding: 0 20px;
    min-height: 52px;
    align-items: center;
  }
  .topbar::after { display: none; }
}

/* ==========================================================================
   ROUND 4b — the "Saved" confirmation on an inline edit  (Scott, 08/06/2026)

   Separate block on purpose: this is a different feature from the rail and is
   revertible on its own (delete this block and the <script> line in layout()).
   The behaviour lives in static/js/saved-indicator.js, which explains why it
   has to survive a page reload rather than being a plain on-blur toast.

   `position: fixed` and appended to <body>, so it adds nothing to the layout
   and cannot reflow the table row it is pointing at.
   ========================================================================== */
.saved-chip {
  position: fixed;
  z-index: 60;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 8px;
  border-radius: var(--r-xs);
  background: var(--ok-bg);
  color: var(--ok);
  box-shadow: 0 0 0 1px var(--ok-line), var(--sh-1);
  font-size: var(--fs-xs);
  line-height: 18px;
  font-weight: var(--fw-medium);
  pointer-events: none;
  opacity: 1;
  transition: opacity var(--t-slow) var(--ease), transform var(--t-slow) var(--ease);
}
.saved-chip::before {
  content: "";
  width: 12px; height: 12px;
  flex: 0 0 auto;
  background: currentColor;
  /* a tick, drawn without needing an icon in the DOM */
  -webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'><path d='M20 6L9 17l-5-5'/></svg>") center / contain no-repeat;
          mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'><path d='M20 6L9 17l-5-5'/></svg>") center / contain no-repeat;
}
.saved-chip.is-going { opacity: 0; transform: translateY(-2px); }

@media (prefers-reduced-motion: reduce) {
  .saved-chip { transition: none; }
  .saved-chip.is-going { opacity: 0; transform: none; }
}

/* ==========================================================================
   END ROUND 4. Rounds 5-8 append below this line, one round per deploy.
   ========================================================================== */


/* ==========================================================================
   ROUND 4c — EVERY EDITABLE FIELD HOVERS THE SAME WAY   (Scott, 08/06/2026)
   Deploys as 2026.07.22.23. A correction to Round 2, not a new round.

   Scott: "hovering over some fields is lighter than others -- previous address
   is a darker green border but in resident contacts, full name is very light
   green border."

   He is right, and it was worse than two fields: an audit that hovers every
   input, select and textarea on nine pages found ELEVEN distinct appearances.
   The real split was two:

     ~76 fields  border: 0 + keyline shadow, hover = darker keyline   (Round 2)
      19 fields  border: 1px solid #dbe5e1 AND the keyline -- a DOUBLE line --
                 hover = border-color #00a384, a hard green

   WHY ROUND 2 MISSED THEM. Its input block restated style.css:2173's typed
   list to match at 0-1-1, which is right for `input[type="text"]` (attribute +
   element). But that same list ends in bare `select` and `textarea`, which are
   0-0-1 -- BELOW any container-scoped rule. So:

     .live-edit input     0-1-1  vs  input[type="text"]  0-1-1  -> v2 won
     .live-edit select    0-1-1  vs  select              0-0-1  -> v2 LOST
     .live-edit textarea  0-1-1  vs  textarea            0-0-1  -> v2 LOST

   "Previous address" is a textarea and "Full name" is a text input, which is
   exactly the pair he noticed. It was also a §4.6 violation the whole time --
   a border and a keyline drawn on the same element.

   THE FIX, and why it is not an escalation. `select:not([multiple])` and
   `textarea:not([hidden])` are 0-1-1, and their :hover/:focus forms are 0-2-1
   -- the same specificity as the container rules they need to sit beside
   (`.live-edit select` is 0-1-1, `.live-edit textarea:hover` is 0-2-1), won on
   cascade order rather than by out-specifying them. Container-agnostic, so it
   also covers `.contacts-add-form`, `.inline-form-grid`, `.inline-select-form`
   and anything added later, instead of naming each one and missing the next.
   ========================================================================== */

/* APPEARANCE is unscoped -- one visual language at every width. Only the
   GEOMETRY below is split. Round 2 made exactly this mistake in reverse: it
   wrapped the appearance in min-width: 761px, so the phone kept the old
   bordered look, and the first version of THIS fix repeated it. */
select:not([multiple]),
textarea:not([hidden]) {
  border: 0;
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--text);
  font-family: inherit;
  box-shadow: var(--sh-key), var(--sh-1);
}

/* One hover for every field, at every width. `border-color: transparent` is
   belt and braces: the width is already 0, but a leftover colour on a
   re-introduced border would bring the green back. */
input[type="text"]:hover, input[type="email"]:hover, input[type="tel"]:hover,
input[type="number"]:hover, input[type="date"]:hover, input[type="password"]:hover,
input[type="search"]:hover,
select:not([multiple]):hover,
textarea:not([hidden]):hover {
  border-color: transparent;
  box-shadow: var(--sh-key-2), var(--sh-1);
}

/* One focus ring for every field, at 6.98:1. */
input[type="text"]:focus, input[type="email"]:focus, input[type="tel"]:focus,
input[type="number"]:focus, input[type="date"]:focus, input[type="password"]:focus,
input[type="search"]:focus,
select:not([multiple]):focus,
textarea:not([hidden]):focus {
  outline: none;
  border-color: transparent;
  background: var(--surface);
  box-shadow: var(--sh-ring);
}

/* Desktop geometry only. */
@media (min-width: 761px) {
  select:not([multiple]) { height: 32px; padding: 0 8px; font-size: var(--fs-md); }
  textarea:not([hidden])  { padding: 6px 8px; font-size: var(--fs-md); }
}

/* `.note-live-textarea` is DELIBERATELY invisible at rest -- it is a note that
   should read as text, not as a boxed control (style.css:1477). That stays.
   Only its hover and focus join the system, so it stops being the one field
   that turns green. 0-2-0 and 0-2-1 respectively, matching style.css:1478-1479. */
.note-live-textarea { border: 0; box-shadow: none; background: transparent; }
.note-live-textarea:hover {
  background: var(--surface);
  box-shadow: var(--sh-key-2), var(--sh-1);
}
.note-live-textarea:focus {
  background: var(--surface);
  border-color: transparent;
  box-shadow: var(--sh-ring);
}

/* The phone floor, restated for the two selectors that just changed shape. */
@media (max-width: 760px) {
  select:not([multiple]) { height: 44px; font-size: 16px; padding-top: 0; padding-bottom: 0; }
  textarea:not([hidden])  { font-size: 16px; min-height: 88px; }
}

/* ==========================================================================
   END ROUND 4c.
   ========================================================================== */


/* ==========================================================================
   ROUND 5a — TIGHTER ROWS  (Scott, 08/06/2026)
   "condense row heights in the projections tables and elsewhere on the
   website so they are a tighter fit."

   MEASURED FIRST. Median row height across 133 rows on eight pages was 48px.
   That is not padding alone: a row carrying a 32px control (an input, a select
   or a 32px .icon-btn) plus Round 3's 8px top and bottom padding is 48px
   exactly. So the padding is what there is to win, and the controls stay at
   32px -- shrinking those would undo Round 2 and start clipping select text,
   which Round .20 already hit once (style.css:5322).

   DESKTOP ONLY. Below 761px these tables are not tables at all -- they are one
   card per row -- and DESIGN-NOTES 9 puts a 44px floor on anything tappable.
   Nothing here applies to a phone.
   ========================================================================== */
@media (min-width: 761px) {
  /* 8px -> 5px. A text-only row goes 34px -> 28px; a row with a control goes
     48px -> 42px, which is the floor while the control is 32px. */
  table.data-table td { padding: 5px 12px; }
  table.data-table thead th, table.data-table th { padding: 4px 12px; }

  /* The Projections grid is the one Scott named. `height: 44px` is an EXACT
     height (style.css:1451), not a minimum, so it held every row open even
     where the cell held nothing but a number. */
  .proj-table tbody td { height: 34px; }
  .proj-table td, .proj-table th { padding: 0.18rem 0.3rem; }

  /* A read-only pencil cell reserved 32px so the pencil had somewhere to go.
     The pencil is 24px, so 28px is enough and keeps the row from setting its
     height from an invisible button. */
  .ro-cell { min-height: 28px; }
}

/* ==========================================================================
   END ROUND 5a.
   ========================================================================== */


/* ==========================================================================
   ROUND 5b — LIST PAGE HEADER + CLEAR FILTERS  (spec §6.2)
   Residents first. The spec says to do one list page, deploy it, then the
   other two -- so Waitlist and Call-offs are deliberately not touched yet.

   WHAT THE SPEC ASKS FOR THAT IS ALREADY TRUE HERE, and therefore is not
   rebuilt: **filter state already lives in the URL.** `/residents` already
   carries `?filter=`, `?house=`, `?sort=` and `?dir=`, so a filtered list is
   already bookmarkable and shareable. That was listed as work; it is done.

   WHAT IS NOT BUILT, ON PURPOSE: **saved views** (`.view-tabs`). The spec
   defines one as "columns + filters + sorting, stored as one unit" with
   drag-reorder, rename and delete. That is a data model and a set of routes,
   not a restyle, and it needs Scott to decide whether a view is per-user or
   shared and whether it stores column choices too. Backlogged rather than
   guessed at.
   ========================================================================== */

.list-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  margin-bottom: 12px;
}
/* h1 carries a bottom margin from style.css:5160 (var(--sp-5)); inside the
   header row the row owns the spacing instead, or the action button floats
   20px above the filters it sits over. */
.list-head h1 { margin: 0; }
.list-head > .button, .list-head > a.button { flex: 0 0 auto; }

/* "Clear filters" sits at the END of the chip row, and app.py only renders it
   when a filter is actually on -- an always-present "clear" on an unfiltered
   list is a control that does nothing. */
.filter-clear {
  display: inline-flex;
  align-items: center;
  margin-left: 4px;
  font-size: var(--fs-sm);
  font-weight: var(--fw-semi);
  color: var(--link);
  text-decoration: none;
  white-space: nowrap;
}
.filter-clear:hover { text-decoration: underline; }
@media (max-width: 760px) {
  /* DESIGN-NOTES 9 — it is a tap target like any other. */
  .filter-clear { min-height: 44px; }
}

/* ==========================================================================
   END ROUND 5b. Rounds 6-8 append below this line.
   ========================================================================== */


/* ==========================================================================
   ROUND 6 — RESIDENT PROFILE HEADER  (spec §6.3)        08/06/2026
   Deploys as 2026.07.22.25.

   TWO CORRECTIONS TO THE SPEC, BOTH MEASURED.

   1. "654px of header before the first panel" DOUBLE-COUNTS. `<nav
      class="rp-tabs">` is nested INSIDE `.rp-band` (app.py, inside
      `.rp-band-main`), so the band's 446px already contains the tab strip's
      208px. Measured at 390px: band 446px, first panel at y=548px. 446+208 is
      the spec's 654, and it is the same pixels twice.

      The target is therefore restated: **the first panel starts under 500px**,
      which needs ~50px saved, not ~154px.

   2. The spec replaces the wrapping tab strip with a horizontally-scrolling
      one. That REVERSES a deliberate decision -- style.css:1412 says "Tabs WRAP
      into tidy rows of pills -- no sideways scrolling." Horizontal scroll on a
      phone also hides affordances behind a gesture with no visible cue, which
      on a care system means a house manager can miss a tab entirely.

      So the strip keeps wrapping and gets TIGHTER instead: `flex: 0 0 auto`
      with real padding rather than `flex: 1 1 30%; min-width: 96px`, which was
      forcing ten tabs into five rows of two. Every tab stays visible, nothing
      scrolls sideways, and the height comes down anyway.

   NOT TOUCHED, and deliberately: `.rp-band`'s padding. `.rp-photo`'s
   `margin-bottom: -56px` (style.css:1231) and `.rp-side-col`'s `padding: 72px`
   (2304) are both calibrated to the current overhang, and moving the band's
   bottom edge lands the photo on the tabs. Those are desktop-only anyway --
   the phone sets `margin-bottom: 0` and `padding-top: 1rem`.

   DESIGN-NOTES 14: `.rp-band.status-pending` (gold), `.status-moved_out` (red)
   and `.status-never` (grey) are 0-2-0 and are NOT selected here, so a resident
   who is not active keeps their own band colour. `.rp-side-label` is untouched.
   ========================================================================== */

/* ---- The tab strip, phone. Matches style.css:1420's `@media (max-width:
   860px)` block, which is the one that wins today. ---- */
@media (max-width: 860px) {
  .rp-tabs {
    flex-wrap: wrap;
    overflow: visible;
    width: 100%;
    gap: 4px;
    margin: 10px 0 2px;
  }
  .rp-tab {
    /* was `flex: 1 1 30%; min-width: 96px` -- which made every tab at least
       96px wide and stretched it, so ten tabs took five rows of two. Sizing to
       the label packs three or four per row instead. */
    flex: 0 0 auto;
    min-width: 0;
    text-align: center;
    background: rgba(255, 255, 255, 0.14);
    border-radius: var(--r-sm);
    padding: 6px 10px;
    font-size: var(--fs-xs);
    line-height: 16px;
  }
  .rp-tab.active { border-radius: var(--r-sm); }

  /* The name and the band's own padding are the rest of the 446px. */
  .rp-band { gap: 12px; }
  .rp-name { font-size: var(--fs-xl); line-height: var(--lh-xl);
             letter-spacing: var(--tracking-tight); }
}

/* ---- Desktop: the same strip was taking two rows for ten tabs. ---- */
@media (min-width: 861px) {
  .rp-tabs { gap: 4px; margin-top: 14px; }
  .rp-tab {
    padding: 7px 12px;
    font-size: var(--fs-sm);
    line-height: 18px;
    border-radius: var(--r-sm) var(--r-sm) 0 0;
  }
}

/* ---- The band itself. Flat, on the ramp, and off the 22% shadow.
   `.rp-band` is 0-1-0, so the three status variants at 0-2-0 still win. ---- */
.rp-band {
  background: var(--teal-700);
  background-image: none;
  border-radius: var(--r-lg) var(--r-lg) 0 0;
  box-shadow: none;
}
.rp-name {
  margin: 0;
  color: var(--text-onbrand);
  letter-spacing: var(--tracking-tight);
  font-weight: var(--fw-semi);
}

/* ==========================================================================
   END ROUND 6. Rounds 7-8 append below this line.
   ========================================================================== */


/* ==========================================================================
   ROUND 7 — STATE  (spec §7)                            08/06/2026
   Deploys as 2026.07.22.26.

   Stripe's section-level empty state ships literal styling: a dashed 1px
   neutral border, medium radius, 24px padding, centred, 4px gap, the title at
   BODY weight (not bold) and the description in caption/secondary. Copied,
   because the restraint is the point -- an empty list should read as calm,
   not as an error.

   The copy rules live in `empty_state()` in app.py, next to the callers who
   have to obey them.
   ========================================================================== */

.empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 24px;
  text-align: center;
  border: 1px dashed var(--line-strong);
  border-radius: var(--r-md);
  background: var(--surface);
}
.empty-title { font-size: var(--fs-md); line-height: var(--lh-md); color: var(--text); }
.empty-desc  { font-size: var(--fs-xs); line-height: var(--lh-xs);
               color: var(--text-muted); max-width: 38ch; }
.empty-action { margin-top: 8px; }

/* An empty state inside a <tbody> must not inherit the table chrome: no zebra
   band, no hover tint, no cell padding fighting the block's own 24px. */
.empty-row td,
table.data-table .empty-row td {
  background: transparent;
  padding: 12px 0;
  box-shadow: none;
}
table.data-table tbody tr.empty-row:hover { background: transparent; }

/* PHONE. style.css:1325 turns every `table.data-table tr` into a card and
   mobile-tables.js labels each cell from its <th>. An empty-state row has one
   cell spanning the table and no column of its own, so as a card it would
   render as a bordered box inside a bordered box -- and the script's own guard
   only skips rows whose single cell has a colspan AND is the only child, which
   this row satisfies, so it gets no label. Belt and braces: strip the card. */
@media (max-width: 760px) {
  table.data-table tr.empty-row {
    border: 0;
    background: transparent;
    box-shadow: none;
    padding: 0;
    margin-bottom: 0;
  }
  table.data-table tr.empty-row td { display: block; padding: 8px 0; }
  table.data-table tr.empty-row td::before { content: none; }
  .empty { padding: 20px 16px; }
}

/* ---- Banners. Stripe's dividing line is sharper than transient-vs-persistent:
   a TOAST is always triggered by the user, a BANNER can appear unprompted, and
   there is no error toast -- errors go in banners. `flash()` is this app's
   banner, so it is the one restyled here. ---- */
.flash {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  padding: 10px 12px;
  border: 0;
  border-radius: var(--r-md);
  margin-bottom: 16px;
  font-size: var(--fs-md);
  line-height: var(--lh-md);
  font-weight: var(--fw-normal);
  background: var(--surface);
  box-shadow: var(--sh-key);
}
.flash.info  { background: var(--info-bg);   color: var(--info);
               box-shadow: 0 0 0 1px var(--info-line); }
/* style.css:598 gives `.flash.warn` a real border; without `border: 0` it
   stacks under the keyline as two lines in two colours (§4.6). */
.flash.warn  { border: 0; background: var(--warn-bg);   color: var(--warn);
               box-shadow: 0 0 0 1px var(--warn-line); }
.flash.error { background: var(--danger-bg); color: var(--danger);
               box-shadow: 0 0 0 1px var(--danger-line); }
.flash.success, .flash.ok { background: var(--ok-bg); color: var(--ok);
               box-shadow: 0 0 0 1px var(--ok-line); }

/* ---- Stat tiles. The markup now puts the LABEL before the VALUE (WCAG 1.3.2
   -- a screen reader was reading "12, Residents with open tasks"). These
   tiles are `display: block` via `a.stat-card` at style.css:135, so children
   already stack in DOM order and no flex reordering is needed or wanted. ---- */
.stat-label { font-size: var(--fs-xs); line-height: var(--lh-xs);
              color: var(--text-muted); margin: 0; }
.stat-num   { font-size: var(--fs-3xl); line-height: var(--lh-3xl);
              font-weight: var(--fw-semi); letter-spacing: var(--tracking-tight);
              font-variant-numeric: tabular-nums; margin: 0; }

/* ==========================================================================
   END ROUND 7. Round 8 appends below this line.
   ========================================================================== */

/* ==========================================================================
   ROUND 9 — CALL-OFF RECENCY TILES                              (08/06/2026)

   Scott asked for bubbles at the top of the Call-Offs page showing how many
   call-offs there were in the last 7, 14 and 30 days.

   All three are the SAME colour, and that is the DESIGN-NOTES 14 decision, not
   an oversight. They are one measurement at three scales; giving 7 days a red
   and 30 days a green would say the short window is bad and the long one is
   good, which is not what the numbers mean. The only colour on the row is the
   one that carries information: which window the page is currently filtered to.

   They sit ABOVE `.co-hero` because `.co-hero`'s four tiles describe the
   selected timeframe and these describe recent history regardless of it. Two
   rows of tiles that answer different questions have to look different, so
   these are wider, quieter, and captioned.
   ========================================================================== */

.co-recent {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sp-3);
  margin: 0 0 var(--sp-2);
}

.co-recent-tile {
  display: flex; flex-direction: column; gap: 2px;
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--r-lg);
  background: var(--surface);
  box-shadow: 0 0 0 1px var(--border), var(--sh-1);
  color: inherit; text-decoration: none;
  transition: box-shadow var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}
.co-recent-tile:hover {
  background: var(--brand-sunk);
  box-shadow: 0 0 0 1px var(--teal-150), var(--sh-2);
}

/* The one window the page is filtered to. This is the only colour on the row,
   and it means "you are looking at this one" -- not "this number is bad". */
.co-recent-tile.is-current {
  background: var(--brand-sunk);
  box-shadow: inset 0 0 0 2px var(--teal-600), var(--sh-1);
}

/* Label first in the DOM (WCAG 1.3.2, the same fix Round 7 made to `.stat-card`)
   and value first on screen. `order` is safe here because the tile is a flex
   column that this file owns outright -- Round 7 could not use it on
   `.stat-card`, which is `display: block` from style.css:135. */
.co-recent-n {
  order: -1;
  font-size: var(--fs-2xl); line-height: var(--lh-tight);
  font-weight: var(--fw-semi); color: var(--teal-700);
  font-variant-numeric: tabular-nums;
}
.co-recent-l { font-size: var(--fs-sm); color: var(--text-muted); }

.co-recent-note { margin: 0 0 var(--sp-5); }

@media (max-width: 760px) {
  /* Three across still fits at 390px once the label wraps -- keeping them on
     one row is what makes 7/14/30 read as a trend. */
  .co-recent { gap: var(--sp-2); }
  .co-recent-tile { padding: var(--sp-2) var(--sp-3); }
  .co-recent-n { font-size: var(--fs-xl); }
  .co-recent-l { font-size: var(--fs-xs); }
}


/* ==========================================================================
   ROUND 9 — COMMUNICATION CHANNEL COLOURS                       (08/06/2026)

   Scott asked for calls, texts and voicemails to be colour-coded on the
   Communications timeline. Email is included because it is the fourth channel
   already on that timeline, and three of four colour-coded reads as a bug.

   DESIGN-NOTES 14, applied properly:

   * The colour is FUNCTIONAL -- it answers "what kind of contact was this?"
     from across the room, which is the actual question when you are scanning
     someone's history for the last time anyone phoned.
   * It is never the ONLY signal. Every card already states its channel in
     words, with an emoji, in the title: "Call (RingCentral)", "Welcome Email".
     So the colour is redundant reinforcement rather than the carrier -- which
     is what makes it safe for anyone who cannot separate the hues.
   * It reuses the LEFT EDGE that `.note-card` already draws (style.css:2201,
     `border-left: 4px solid var(--mint)`), so nothing moves and no new surface
     is introduced. Only the colour of an existing 4px edge changes.

   CHOOSING THE FOUR. The obvious pick -- the 400 step of four different ramps
   -- was measured and REJECTED. All four sit at almost the same lightness, so
   they were identical in greyscale (luminance 0.276-0.279), and text vs
   voicemail collapsed to CIEDE2000 2.1 under deuteranopia and call vs text to
   1.1 under tritanopia. This repo treats anything under 3 as "looks the same".

   The set below was searched for instead: every combination of ramp steps,
   scored on the WORST pairwise CIEDE2000 across normal vision, protanopia,
   deuteranopia and tritanopia at once, with a lightness spread required on top.

       call       --teal-700    #1d4b50   contrast 9.66:1 on white
       text       --blue-500    #3573c0            4.81:1
       voicemail  --violet-300  #bd9cef            2.29:1
       email      --mint-400    #00a384            3.20:1
       other      --slate-200   #bbcacc            1.69:1

   Worst pair, across all four vision models: ΔE 9.3. Smallest greyscale
   luminance gap: 0.110. The numbers are pinned in tests/v2_channels_test.py --
   change a step here and that suite recomputes them and fails if the set stops
   holding.

   NOT reused: red (destructive), gold (Forever Pricing), copper (Phosphor
   House). A channel edge must not be mistaken for any of those.

   Specificity: `.note-card` sets `border-left` at 0-1-0 and `.note-card.comm-
   manual` overrode it at 0-2-0. These are 0-2-0 and sit in a later sheet, so
   they win over both -- deliberately. A staff-logged phone call is a phone
   call; its provenance is stated in words by `.manual-badge` beside the title,
   which is where provenance belongs.
   ========================================================================== */

.note-card.comm-ch-call      { border-left-color: var(--teal-700); }
.note-card.comm-ch-text      { border-left-color: var(--blue-500); }
.note-card.comm-ch-voicemail { border-left-color: var(--violet-300); }
.note-card.comm-ch-email     { border-left-color: var(--mint-400); }
.note-card.comm-ch-other     { border-left-color: var(--slate-200); }

/* The staff-logged tint stays -- it is a background wash, not the edge, so it
   still says "someone typed this in" without competing with the channel. */
.note-card.comm-manual.comm-ch-call      { border-left-color: var(--teal-700); }
.note-card.comm-manual.comm-ch-text      { border-left-color: var(--blue-500); }
.note-card.comm-manual.comm-ch-voicemail { border-left-color: var(--violet-300); }
.note-card.comm-manual.comm-ch-email     { border-left-color: var(--mint-400); }
.note-card.comm-manual.comm-ch-other     { border-left-color: var(--slate-200); }

/* `.conv-card` sets its own 3px edge at style.css:1551. A text conversation is
   still a text, so it takes the text colour and the standard 4px. */
.note-card.conv-card.comm-ch-text { border-left: 4px solid var(--blue-500); }

/* The key. Without it the colours are a private code -- four hues nobody has
   been told the meaning of. It sits above the timeline, reads in words, and
   each swatch is the exact edge colour it explains. */
.comm-key { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4);
            align-items: center; margin: 0 0 var(--sp-3); }
.comm-key-item { display: inline-flex; align-items: center; gap: 6px;
                 font-size: var(--fs-sm); color: var(--text-muted); }
.comm-key-swatch { width: 4px; height: 14px; border-radius: 2px; flex: 0 0 auto; }
.comm-key-call      .comm-key-swatch { background: var(--teal-700); }
.comm-key-text      .comm-key-swatch { background: var(--blue-500); }
.comm-key-voicemail .comm-key-swatch { background: var(--violet-300); }
.comm-key-email     .comm-key-swatch { background: var(--mint-400); }


/* ==========================================================================
   WAGE REDUCTION  —  /wage-reduction  (built 08/07/2026)

   Server-rendered from claude/wage-reduction-mockup.html, agreed over eleven
   rounds with Scott on 08/07/2026. Every colour is a token off the nine ramps
   already in this file; nothing here invents one, and nothing here selects
   .kv-tone-*, .side-tag-* or .rp-side-label, so DESIGN-NOTES rule 14 is
   untouched.

   THE TABLE NEVER SCROLLS SIDEWAYS, AT ANY WIDTH. There is no overflow-x
   anywhere below, deliberately — the panel is a CSS container and columns drop
   out progressively as it narrows, then the whole table becomes labelled cards
   under 660px. Horizontal scroll was the single most-reported bug of the
   design session.

   TRAP, and it cost a round: a hidden <td> still leaves its <col> reserving
   width under `table-layout: fixed`. Every rule that hides a column MUST zero
   the matching col width in the same query, or the visible columns squeeze
   into nothing and 17.5% of the panel is dead space on the right.

   Everything is `wr-` prefixed on purpose. The mock-up used generic names
   (.panel, .hero, .stat-card, td.k-name) which would have collided with
   style.css.
   ========================================================================== */

/* --- A PRE-EXISTING BUG, found while measuring this page and fixed here ----
   Between roughly 760px and the rail breakpoint the sidebar is gone and the
   top bar shows every page link in one non-wrapping row. On `.34` that row was
   1088px wide, so ANY page in the app scrolled sideways at 800, 900 and 1000px
   — an iPad in portrait, or a half-width laptop window. It was measured
   against the previous app.py in the same container before being called a bug,
   because this round adds a thirteenth link and would otherwise have looked
   like the cause: control 1088px, this build 1187px, both overflowing.

   Same selector as style.css:58 (0-1-1), so this wins on cascade order without
   escalating specificity. `min-width: 0` is what lets a flex item shrink below
   its content; without it `flex-wrap` alone still overflows. */
.topbar nav { flex-wrap: wrap; row-gap: var(--sp-1); min-width: 0; }

.wr-root { min-width: 0; }

.wr-head { align-items: flex-start; }
.wr-sub { font-size: var(--fs-sm); line-height: var(--lh-sm);
          color: var(--text-muted); margin: 4px 0 0; max-width: 62ch; }
.wr-head-actions { display: flex; gap: var(--sp-2); flex-wrap: wrap; align-items: center; }
/* The last-synced line. It sits with the buttons rather than under the title
   because "when did this last look at BambooHR" is the question you ask at the
   moment you are deciding whether to press Sync. */
.wr-synced { font-size: var(--fs-xs); line-height: var(--lh-xs);
             color: var(--text-subtle); white-space: nowrap; }
@media (max-width: 760px) { .wr-synced { white-space: normal; flex: 1 0 100%; } }

/* --- period buttons and the paid / not-paid chips -------------------------
   Links, not buttons: the period and the filter are in the query string, so
   the view is shareable and the back button works. */
.wr-periods { display: flex; gap: 7px; flex-wrap: wrap; align-items: center;
              margin: 0 0 var(--sp-3); }
.wr-periods-tight { margin: 0; }
.wr-plabel { font-size: var(--fs-2xs); text-transform: uppercase;
             letter-spacing: var(--tracking-caps); font-weight: var(--fw-semi);
             color: var(--text-subtle); margin-right: 4px; }
.wr-pbtn, .wr-chip { font-size: var(--fs-sm); line-height: 20px;
                     background: var(--surface); color: var(--text-muted);
                     padding: 6px 13px; border-radius: var(--r-pill);
                     box-shadow: var(--sh-key); text-decoration: none;
                     display: inline-flex; align-items: center; min-height: 32px; }
.wr-pbtn:hover, .wr-chip:hover { background: var(--surface-hover); text-decoration: none; }
.wr-pbtn[aria-pressed="true"], .wr-chip[aria-pressed="true"] {
  background: var(--teal-600); color: #fff; box-shadow: none; font-weight: var(--fw-medium); }

/* --- the headline --------------------------------------------------------
   "Total Saved", no sub-line, and NO icons and no plus signs in the breakdown
   beside it. Both were tried in the mock-up and Scott had them removed. The
   stacked proportion bar was removed too — three unlabelled proportions of a
   number already on screen is not information. */
.wr-hero { background: linear-gradient(180deg, var(--teal-700), var(--teal-800));
           color: #fff; border-radius: var(--r-lg); padding: 22px;
           box-shadow: var(--sh-3); margin-bottom: var(--sp-3);
           display: flex; gap: 26px; align-items: stretch; flex-wrap: wrap; }
.wr-hero-main { flex: 1 1 300px; min-width: 0; }
.wr-hero-eyebrow { font-size: var(--fs-2xs); text-transform: uppercase;
                   letter-spacing: var(--tracking-caps); font-weight: var(--fw-semi);
                   color: var(--mint-150); margin: 0 0 6px; }
.wr-hero-num { font-size: 40px; line-height: 46px; font-weight: var(--fw-bold);
               letter-spacing: -.02em; font-variant-numeric: tabular-nums; margin: 0; }
.wr-hero-split { flex: 1 1 228px; min-width: 0; display: flex; flex-direction: column;
                 justify-content: center; gap: 9px; padding-left: 22px;
                 border-left: 1px solid rgba(255, 255, 255, .16); }
.wr-hero-cap { font-size: var(--fs-2xs); text-transform: uppercase;
               letter-spacing: var(--tracking-caps); font-weight: var(--fw-semi);
               color: var(--mint-150); margin: 0 0 2px; }
.wr-hero-line { display: flex; align-items: center; gap: 8px;
                font-size: var(--fs-sm); line-height: 20px; }
.wr-hero-line .wr-key { flex: 1 1 auto; min-width: 0; color: var(--teal-100); }
.wr-hero-line span:last-child { font-variant-numeric: tabular-nums;
                                font-weight: var(--fw-semi); white-space: nowrap; }
@media (min-width: 761px) { .wr-hero-num { font-size: 56px; line-height: 60px; } }
@media (max-width: 900px) {
  .wr-hero-split { border-left: 0; border-top: 1px solid rgba(255, 255, 255, .16);
                   padding: 14px 0 0; width: 100%; flex-basis: 100%; }
}

/* --- the three tiles -----------------------------------------------------
   THREE, not four. The "final hours" tile was removed in the mock-up: the
   hours total moved into the wages tile's foot line and the "needs hours"
   count into the background-check tile's. Rule 14 — each tile's number keeps
   a colour of its own, and each one also states what it is in words, so the
   colour is never the only signal. */
.wr-stat-row { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
               gap: var(--sp-3); margin-bottom: var(--sp-3); }
.wr-stat-card { background: var(--surface); border-radius: var(--r-lg);
                padding: 14px 16px; box-shadow: var(--sh-key), var(--sh-1); min-width: 0; }
.wr-stat-label { font-size: var(--fs-xs); line-height: var(--lh-xs); color: var(--text-muted);
                 margin: 0; display: flex; align-items: center; gap: 6px; }
.wr-stat-num { font-size: var(--fs-3xl); line-height: var(--lh-3xl); font-weight: var(--fw-semi);
               letter-spacing: var(--tracking-tight); font-variant-numeric: tabular-nums;
               margin: 2px 0 0; }
.wr-stat-foot { font-size: var(--fs-xs); line-height: var(--lh-xs);
                color: var(--text-subtle); margin: 3px 0 0; }
.wr-tone-wage .wr-stat-num { color: var(--mint-500); }
.wr-tone-tax  .wr-stat-num { color: var(--gold-700); }
.wr-tone-bg   .wr-stat-num { color: var(--blue-700); }
@media (max-width: 860px) { .wr-stat-row { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 430px) { .wr-stat-row { grid-template-columns: minmax(0, 1fr); } }

/* --- panels --------------------------------------------------------------- */
.wr-panel { background: var(--surface); border-radius: var(--r-lg);
            box-shadow: var(--sh-key), var(--sh-1); margin-bottom: var(--sp-3); min-width: 0; }
.wr-panel-head { padding: 13px 16px; box-shadow: inset 0 -1px 0 var(--line-subtle);
                 display: flex; align-items: center; justify-content: space-between;
                 gap: var(--sp-3); flex-wrap: wrap; }
.wr-panel-title { font-size: var(--fs-lg); line-height: var(--lh-lg);
                  font-weight: var(--fw-semi); margin: 0;
                  display: flex; align-items: center; gap: 8px; }
.wr-panel-title .ico { color: var(--teal-600); }
.wr-panel-hint { font-size: var(--fs-xs); line-height: var(--lh-xs);
                 color: var(--text-subtle); margin: 2px 0 0; }
.wr-panel-body { padding: 16px; }

.wr-ways { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-3); }
@media (max-width: 820px) { .wr-ways { grid-template-columns: minmax(0, 1fr); } }
.wr-way { border-radius: var(--r-md); padding: 13px 15px; min-width: 0; }
.wr-way-np { background: var(--mint-50); box-shadow: inset 0 0 0 1px var(--mint-150); }
.wr-way-p  { background: var(--slate-50); box-shadow: inset 0 0 0 1px var(--line); }
.wr-way-h { margin-bottom: 7px; }
.wr-way-copy { font-size: var(--fs-sm); line-height: 19px; color: var(--text-muted); margin: 0 0 9px; }
.wr-way-copy b { color: var(--text); font-weight: var(--fw-semi); }
.wr-way-f { font-size: var(--fs-sm); line-height: 18px; color: var(--text);
            background: var(--surface); border-radius: var(--r-sm); padding: 7px 10px;
            box-shadow: var(--sh-key); font-variant-numeric: tabular-nums;
            overflow-wrap: anywhere; }
.wr-way-f b { color: var(--teal-700); font-weight: var(--fw-semi); }
.wr-pmtag { font-size: var(--fs-2xs); font-weight: var(--fw-semi); padding: 1px 8px;
            border-radius: var(--r-pill); text-transform: uppercase;
            letter-spacing: var(--tracking-caps); white-space: nowrap; }
.wr-pm-np { background: var(--mint-100); color: var(--mint-700); }
.wr-pm-p  { background: var(--slate-100); color: var(--slate-700); }

/* --- the five editable settings ------------------------------------------
   They live in app_meta, not in Python, so they can be changed without a
   deploy. Each one submits on change and the whole page recalculates from the
   reload — the same shape as every other pencil-first form in the app. */
.wr-rates-cap { font-size: var(--fs-2xs); text-transform: uppercase;
                letter-spacing: var(--tracking-caps); font-weight: var(--fw-semi);
                color: var(--text-subtle); margin: 16px 0 0; }
.wr-rates { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 8px; }
.wr-rate-f { background: var(--surface); border-radius: var(--r-md); padding: 9px 12px;
             box-shadow: var(--sh-key); min-width: 148px; flex: 1 1 148px; }
.wr-rate-f label { display: block; font-size: var(--fs-2xs); text-transform: uppercase;
                   letter-spacing: var(--tracking-caps); color: var(--text-subtle);
                   font-weight: var(--fw-semi); margin-bottom: 3px; }
.wr-fin { display: flex; align-items: baseline; gap: 3px; }
.wr-rates input { font: inherit; font-size: var(--fs-lg); font-weight: var(--fw-semi);
                  border: 0; width: 100%; max-width: 80px; padding: 0; color: var(--text);
                  background: transparent; font-variant-numeric: tabular-nums;
                  box-shadow: none; min-height: 0; }
.wr-rates input:focus { outline: 2px solid var(--teal-400); outline-offset: 3px;
                        border-radius: 2px; }
.wr-pct { font-size: var(--fs-sm); color: var(--text-subtle); }
.wr-rn { font-size: var(--fs-2xs); line-height: var(--lh-2xs);
         color: var(--text-subtle); margin-top: 2px; }
.wr-rate-f.wr-total { background: var(--teal-700); box-shadow: none; }
.wr-rate-f.wr-total label { color: var(--mint-150); }
.wr-rate-f.wr-total .wr-tv { font-size: var(--fs-lg); font-weight: var(--fw-bold);
                             color: #fff; font-variant-numeric: tabular-nums; }
.wr-rate-f.wr-total .wr-rn { color: var(--teal-150); }
.wr-rate-f.wr-bgf { background: var(--blue-50); box-shadow: inset 0 0 0 1px var(--blue-150); }
.wr-rate-f.wr-bgf label { color: var(--blue-700); }
.wr-rate-f.wr-deff { background: var(--gold-50); box-shadow: inset 0 0 0 1px var(--gold-150); }
.wr-rate-f.wr-deff label { color: var(--gold-700); }

/* A year whose unemployment rate has not been entered yet. Amber, and it says
   so in words underneath as well -- colour is never the only signal
   (DESIGN-NOTES 14). */
.wr-rate-f.wr-unset { background: var(--gold-50); box-shadow: inset 0 0 0 1px var(--gold-150); }
.wr-rate-f.wr-unset label { color: var(--gold-700); }
.wr-rate-f.wr-unset .wr-rn b { color: var(--gold-700); }

/* --- add employee --------------------------------------------------------- */
.wr-addform { display: none; padding: 14px 16px; background: var(--teal-50);
              box-shadow: inset 0 -1px 0 var(--line); }
.wr-addform.wr-open { display: block; }
.wr-af-grid { display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-end; margin: 0; }
.wr-af { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.wr-af label { font-size: var(--fs-2xs); text-transform: uppercase;
               letter-spacing: var(--tracking-caps); font-weight: var(--fw-semi);
               color: var(--text-subtle); }
.wr-af input, .wr-af select { font: inherit; font-size: var(--fs-md); padding: 6px 9px;
                              border: 0; border-radius: var(--r-sm); background: var(--surface);
                              box-shadow: var(--sh-key); color: var(--text);
                              min-height: 34px; max-width: 100%; }
.wr-af input:focus, .wr-af select:focus { outline: 2px solid var(--teal-400); outline-offset: 1px; }
.wr-af-name input { width: 180px; }
.wr-af-date input { width: 150px; }
.wr-af-n input { width: 90px; }
.wr-af-note input { width: 200px; }

/* The row-edit <form> elements. A <form> cannot span table cells, so each
   row's inputs reference one of these by id. They render nothing. */
.wr-forms { display: none; }
.wr-inline { display: inline-flex; align-items: center; margin: 0; padding: 0; }

/* ==========================================================================
   THE TABLE
   ========================================================================== */
.wr-tbl { container-type: inline-size; }
.wr-table { width: 100%; border-collapse: separate; border-spacing: 0; table-layout: fixed; }
.wr-table th, .wr-table td { padding: 7px 10px; text-align: left; min-width: 0;
                             overflow-wrap: normal; word-break: normal; }
.wr-table th { font-size: var(--fs-2xs); line-height: var(--lh-2xs);
               font-weight: var(--fw-semi); text-transform: uppercase;
               letter-spacing: var(--tracking-caps); color: var(--text-subtle);
               background: var(--surface); box-shadow: inset 0 -1px 0 var(--line); }
.wr-table td { font-size: var(--fs-md); line-height: var(--lh-md);
               box-shadow: inset 0 -1px 0 var(--line-subtle); vertical-align: middle; }
.wr-table td.wr-num, .wr-table th.wr-num { text-align: right;
                                           font-variant-numeric: tabular-nums;
                                           white-space: nowrap; }
.wr-table tbody tr:hover { background: var(--surface-hover); }
.wr-table tr.wr-editing, .wr-table tr.wr-editing:hover { background: var(--gold-50); }
.wr-table .wr-k-name, .wr-table .wr-k-notes, .wr-prov { overflow-wrap: break-word; }
.wr-pos { color: var(--mint-500); font-weight: var(--fw-semi); }
.wr-nm { font-weight: var(--fw-medium); }
.wr-c-date { color: var(--text-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
/* Row sub-lines: the termination date and the note, revealed under the name
   only when their own column has dropped out. Deliberately NOT .wr-sub, which
   is the page subtitle. */
.wr-table .wr-rowsub { display: none; font-size: var(--fs-2xs); line-height: var(--lh-2xs);
                       color: var(--text-subtle); margin-top: 1px; }

/* Read-only value and its edit input are both rendered; the class on the <tr>
   decides which one is on screen. */
.wr-edit { display: none; }
.wr-table tr.wr-editing .wr-ro { display: none; }
.wr-table tr.wr-editing .wr-edit { display: inline-flex; }
.wr-table tr.wr-editing input.wr-edit { display: inline-block; }
/* Both of these are scoped to .wr-table (0-2-0) ON PURPOSE. At 0-1-0 they tie
   with `.wr-iconbtn { display: inline-flex }` further down the file, and the
   later rule wins — which put the tick on screen on every row, next to the
   pencil, all the time. Matching the winning specificity rather than
   escalating past it is DESIGN-NOTES 13c. */
.wr-table .wr-savebtn { display: none; }
.wr-table tr.wr-editing .wr-savebtn { display: inline-flex; }
/* WHILE A ROW IS OPEN, THE PENCIL GOES AWAY. Only the tick remains.
   It used to stay on screen beside the tick, and clicking it — which reads as
   "done" — ran the CANCEL path and silently threw away everything typed into
   the row. Scott lost a set of hours to this on 08/08/2026 and reported it as
   "her hours are filled out" when the database had never received them.
   The approved mock-up swapped one button from pencil to tick for exactly this
   reason; rendering both was the mistake. Escape is still the way to abandon
   an edit, and it is deliberate rather than a click away from the save. */
.wr-table tr.wr-editing .wr-editbtn { display: none; }

.wr-ei { font: inherit; font-size: var(--fs-md); padding: 3px 6px; border: 0;
         border-radius: var(--r-xs); background: var(--surface);
         box-shadow: inset 0 0 0 1px var(--gold-150); color: var(--text);
         font-variant-numeric: tabular-nums; min-height: 28px; width: 100%; max-width: 100%; }
.wr-ei:focus { outline: 2px solid var(--teal-400); outline-offset: 0; }
/* Fixed widths do not work in a fixed-layout table: the rate cell holds TWO
   inputs and an arrow in about 125px, and 2 x 62px + the arrow does not fit, so
   both boxes clipped their own values. Let them SHARE whatever the column has
   instead, and trim the padding so more of the number shows. */
.wr-ei.wr-n { max-width: none; min-width: 0; }
.wr-table .wr-ei { padding: 3px 2px; }
.wr-rate-edit { align-items: center; gap: 2px; width: 100%; }
.wr-rate-edit .wr-ei { flex: 1 1 0; min-width: 0; }
.wr-rate-edit .wr-ar { flex: 0 0 auto; margin: 0; }

/* THE ARROWS (08/08/2026). Scott asked for a dollar a press on the background
   check and a quarter on hours and both rates, which means the stepper is no
   longer decoration — he is going to click it. Chrome only paints it on hover,
   so it appears from nowhere and takes ~16px off the number as it does. Paint
   it always: it is discoverable, and the width it needs is accounted for at
   every width instead of arriving when the pointer does.

   That 16px is why the boxes below are what they are. "16.00" is 35px at this
   font; the rate cell held TWO boxes plus an arrow glyph in ~43px each, so the
   number was already clipped to "16." before the stepper took its share. The
   arrow glyph between the rates is dropped while the row is open — it says
   nothing you cannot see from two adjacent boxes — and the padding is trimmed
   to give the digits the rest. */
.wr-table .wr-ei.wr-n::-webkit-inner-spin-button,
.wr-table .wr-ei.wr-n::-webkit-outer-spin-button { opacity: 1; }
.wr-table tr.wr-editing .wr-rate-edit .wr-ar { display: none; }
.wr-k-hrs .wr-ei, .wr-k-bg .wr-ei { width: 100%; }
/* The background-check cell shares its width with the tick, so the amount box
   takes what is left rather than a fixed 62px it may not have. */
.wr-bgwrap .wr-ei { flex: 1 1 0; min-width: 0; }

/* layout() upgrades EVERY input[type=date] in the app into a type-or-pick
   combo: it injects a `.date-combo` wrapper holding a visible MM/DD/YYYY text
   box and a calendar button, and hides the real date input inside it. That is
   a good sitewide behaviour and the row edit should have it — but the wrapper
   is injected as a sibling in the <td>, NOT inside .wr-edit, so hiding the
   date input alone left a stray text box and calendar button sitting in the
   Terminated column of every row, permanently, squeezing every column after
   it. Hide the WRAPPER, and reveal it with the rest of the row. */
.wr-table td .date-combo { display: none; }
.wr-table tr.wr-editing td .date-combo { display: inline-flex; align-items: center; }

.wr-rate-cell { font-variant-numeric: tabular-nums; white-space: nowrap; }
.wr-ar { color: var(--slate-300); margin: 0 2px; }
.wr-to { color: var(--teal-600); font-weight: var(--fw-semi); }
.wr-needhrs { font-size: var(--fs-2xs); font-weight: var(--fw-semi);
              background: var(--gold-100); color: var(--gold-700); padding: 1px 8px;
              border-radius: var(--r-pill); white-space: nowrap; }

/* Rate provenance. "Rate from BambooHR" with NO effective date, or an amber
   chip plus the default. NEVER the word "assumed" — it was tried and Scott had
   to ask what it meant. A hand-typed rate shows nothing at all. */
/* `break-word`, not `anywhere`. `anywhere` breaks INSIDE a word the moment a
   line is short, and BACKLOG 91's column took enough width off the name column
   that "using the $15.00 default" started rendering as "u / sing the $15.00
   default" next to the amber chip. break-word breaks a word only when it
   cannot fit a line of its own, which is the behaviour this line wanted all
   along; the browser check measures the provenance line against its cell at
   all thirteen widths so a long one still cannot spill. */
.wr-prov { font-size: var(--fs-2xs); line-height: var(--lh-2xs); color: var(--text-subtle);
           margin-top: 1px; white-space: normal; overflow-wrap: break-word; }
.wr-prov .wr-bh { color: var(--violet-700); font-weight: var(--fw-semi); }
.wr-prov .wr-noRate { background: var(--gold-100); color: var(--gold-700);
                      font-weight: var(--fw-semi); padding: 0 6px;
                      border-radius: var(--r-pill); margin-right: 5px; white-space: nowrap; }

/* BACKLOG 92 -- Scott, 08/08/2026: *"the payment method where it says not
   payroll/payroll, those boxes should be the same size as the not paid/paid
   boxes under final check in the table."* Every box metric below is copied
   from `.wr-paidbtn` on purpose -- type scale, weight, line-height, vertical
   padding, min-height and the pill radius are the SAME NUMBERS, so the two
   chips are one size. It shrank from --fs-sm/28px/--r-sm.

   The one difference is the right padding: this is a <select> and it keeps a
   caret, so it carries 17px on the right where the Paid chip carries 10. Take
   the caret away and there is nothing on screen saying the box can be changed.
   Everything else matches to the pixel, and the browser check measures the two
   heights against each other rather than against a number.

   THE NUMBERS ARE COPIED FROM WHAT THE PAID CHIP *RENDERS*, NOT FROM WHAT IT
   DECLARES, and those are not the same thing. `.wr-paidbtn` asks for 3px of
   vertical padding and a pill corner and gets neither: it is a submit button
   inside a form, so `form button[type="submit"]:not(.ghost)` (0-2-2, and see
   DESIGN-NOTES 13c -- this is its fourth appearance) wins with 6px and 6px.
   The chip on screen is 28px tall with a 6px corner, so this one is too.
   Matching the declarations instead would have produced two boxes that agree
   in the stylesheet and differ on the page. §9b of the browser check measures
   the two rendered boxes against EACH OTHER, which is the assertion that
   actually holds this together.

   Colour is functional here (DESIGN-NOTES 14) -- mint for Not Payroll, slate
   for Payroll -- and survives the resize untouched below. */
/* `select.wr-pm`, NOT `.wr-pm` -- DESIGN-NOTES 13c, for the fourth time in
   this file. `select:not([multiple])` (0-1-1, above) and its min-width:761px
   partner set border-radius, font-size, padding AND `height: 32px`, and a
   0-1-0 class rule loses to every one of them. `.wr-pm` has therefore never
   set its own font size, padding or corner since the day it was written: the
   chip was rendering at --fs-md in a 6px box while the stylesheet said
   --fs-sm and --r-sm. Matching the winning 0-1-1 and relying on cascade order
   is the fix the round-4c note above prescribes; escalating past it is not.
   `height: auto` is the one that has to be said out loud -- a min-height
   cannot beat a height. */
select.wr-pm { font: inherit; font-size: var(--fs-2xs); padding: 6px 17px 6px 10px;
         border: 0; height: auto;
         border-radius: var(--r-sm); cursor: pointer; font-weight: var(--fw-semi);
         min-height: 24px; line-height: 16px; appearance: none; width: auto;
         max-width: 100%; white-space: nowrap;
         background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='8' height='5'><path d='M1 1l3 3 3-3' fill='none' stroke='%23607679' stroke-width='1.5' stroke-linecap='round'/></svg>");
         background-repeat: no-repeat; background-position: right 6px center; }
.wr-pm[data-v="Not Payroll"] { background-color: var(--mint-100); color: var(--mint-700);
                               box-shadow: inset 0 0 0 1px var(--mint-150); }
.wr-pm[data-v="Payroll"] { background-color: var(--slate-100); color: var(--slate-700);
                           box-shadow: inset 0 0 0 1px var(--slate-200); }
.wr-pm:focus-visible { outline: 2px solid var(--teal-400); outline-offset: 1px; }

/* Paid / Not paid is one click, and defaults to Not paid. Colour AND the word
   both change — rule 14's "never let colour be the only signal". */
.wr-paidbtn { font: inherit; font-size: var(--fs-2xs); font-weight: var(--fw-semi);
              border: 0; cursor: pointer; padding: 3px 10px; border-radius: var(--r-pill);
              min-height: 24px; line-height: 16px; white-space: nowrap; }
.wr-paidbtn[data-v="1"] { background: var(--green-50); color: var(--green-700);
                          box-shadow: inset 0 0 0 1px var(--green-150); }
.wr-paidbtn[data-v="0"] { background: var(--gold-50); color: var(--gold-700);
                          box-shadow: inset 0 0 0 1px var(--gold-150); }
.wr-paidbtn:hover { filter: brightness(.97); }
.wr-paidbtn:focus-visible { outline: 2px solid var(--teal-400); outline-offset: 2px; }

/* BACKLOG 91 -- the PRIOR PAYROLL tick. A direct click, like the
   background-check tick beside it and unlike the typed fields, which are
   behind the pencil (DESIGN-NOTES 7). It reuses `.wr-ck`/`.wr-bx` outright so
   the two ticks in one row cannot drift apart. It records history and moves no
   money. */
.wr-k-prior .wr-inline { display: inline-flex; }

/* THE BACKGROUND-CHECK CELL. The amount is a SIBLING of the label, never a
   child — rendered inside it, clicking the amount to type only toggled the
   tick and the field could never take focus. That cost a whole round. */
.wr-bgwrap { display: flex; align-items: center; gap: 7px; min-width: 0; }
.wr-ck { display: inline-flex; align-items: center; cursor: pointer;
         user-select: none; position: relative; }
.wr-ck input { position: absolute; left: 0; top: 0; margin: 0; opacity: 0;
               width: 1px; height: 1px; }
.wr-bx { width: 17px; height: 17px; border-radius: var(--r-xs); background: var(--surface);
         flex: 0 0 auto; box-shadow: inset 0 0 0 1px var(--line-strong); position: relative; }
.wr-ck input:checked + .wr-bx { background: var(--blue-700); box-shadow: none; }
.wr-ck input:checked + .wr-bx::after { content: ""; position: absolute; left: 5px; top: 2px;
                                       width: 5px; height: 9px; border: solid #fff;
                                       border-width: 0 2px 2px 0; transform: rotate(42deg); }
.wr-ck input:focus-visible + .wr-bx { box-shadow: inset 0 0 0 1px var(--line-strong),
                                                  0 0 0 3px var(--blue-150); }
.wr-bgamt { font-size: var(--fs-sm); font-variant-numeric: tabular-nums;
            color: var(--text-subtle); white-space: nowrap; }
.wr-bgamt.wr-on { color: var(--blue-700); font-weight: var(--fw-semi); }

.wr-note { font: inherit; font-size: var(--fs-sm); line-height: var(--lh-sm);
           color: var(--text-muted); width: 100%; min-width: 0; border: 0;
           background: transparent; padding: 3px 6px; border-radius: var(--r-xs);
           box-shadow: inset 0 0 0 1px transparent; min-height: 26px; }
.wr-note:hover { box-shadow: inset 0 0 0 1px var(--line); }
.wr-note:focus { box-shadow: inset 0 0 0 1px var(--teal-400), 0 0 0 3px var(--teal-100);
                 background: var(--surface); color: var(--text); outline: none; }
.wr-k-notes .wr-inline { display: flex; width: 100%; }

.wr-k-act { text-align: right; white-space: nowrap; }
.wr-iconbtn { border: 0; background: transparent; color: var(--slate-400); cursor: pointer;
              padding: 5px; border-radius: var(--r-sm); display: inline-flex; line-height: 0; }
.wr-iconbtn:hover { background: var(--slate-100); color: var(--text); }
.wr-iconbtn.wr-del:hover { background: var(--red-50); color: var(--red-700); }
.wr-iconbtn:focus-visible { outline: 2px solid var(--teal-400); outline-offset: 1px; }

.wr-table tfoot td { font-weight: var(--fw-semi); background: var(--slate-50);
                     box-shadow: inset 0 1px 0 var(--line); }
.wr-cardtot { display: none; padding: 12px 14px; background: var(--slate-50);
              box-shadow: inset 0 1px 0 var(--line); font-size: var(--fs-md);
              font-weight: var(--fw-semi); justify-content: space-between; }

/* COLUMN WIDTHS. Declared on the <th>, not on a <col>, and each set below sums
   to 100 across the columns VISIBLE at that width.

   Why not a <colgroup>: `display: none` on a <td> removes the cell from the
   row rather than hiding it in place, so every cell after a hidden one slides
   one column left and takes the <col> width of the column before it. Hiding
   Cost before and Cost after therefore gave Saved and Final check the two
   zeroed widths and stacked them on top of each other, while Notes and the row
   actions quietly took Saved's and Final check's. Zeroing the hidden column's
   <col> width — the fix the spec called for — addresses the reserved space and
   nothing at all about the shift.

   Under `table-layout: fixed` with no <colgroup> the widths come from the first
   row, and a hidden <th> simply takes its width out of the row with it. */
.wr-table th.wr-k-name   { width: 8%; }
.wr-table th.wr-k-date   { width: 8%; }
.wr-table th.wr-k-rate   { width: 10%; }
.wr-table th.wr-k-hrs    { width: 6.5%; }
.wr-table th.wr-k-ot     { width: 5.5%; }
.wr-table th.wr-k-prior  { width: 6.5%; }
.wr-table th.wr-k-pm     { width: 8%; }
.wr-table th.wr-k-bg     { width: 7.5%; }
.wr-table th.wr-k-would  { width: 8%; }
.wr-table th.wr-k-actual { width: 7%; }
.wr-table th.wr-k-saved  { width: 5.5%; }
.wr-table th.wr-k-paid   { width: 7.5%; }
.wr-table th.wr-k-notes  { width: 5.5%; }
.wr-table th.wr-k-act    { width: 6.5%; }

/* --- 1279px: the two derived cost columns go, and the note moves under the
       name. Both totals are still in the footer and in the headline, and the
       note is still on the row, so nothing leaves the page. 10 columns, 100.

       EVERY NUMBER IN THESE FOUR BLOCKS IS A MEASUREMENT, NOT A PREFERENCE.
       tests/wr_browser_check.py §8g measures, at all thirteen widths, the
       longest unbreakable word in every header, the rendered text of every
       number box against the room left after its padding and stepper, and the
       provenance line under the name — and fails on a single pixel. Adding
       BACKLOG 91's tick column pushed three of these bands past 100% of what
       their columns actually need, which is why two columns moved out of this
       one and two more out of the next. Re-derive, do not nudge. --- */
@container (max-width: 1279px) {
  .wr-table .wr-k-would, .wr-table .wr-k-actual { display: none; }
  .wr-table .wr-k-notes { display: none; }
  .wr-table .wr-r-note { display: block; }
  .wr-table th.wr-k-name  { width: 9.5%; }  .wr-table th.wr-k-date  { width: 10%; }
  .wr-table th.wr-k-rate  { width: 12%; }   .wr-table th.wr-k-hrs   { width: 8.5%; }
  .wr-table th.wr-k-ot    { width: 7%; }
  .wr-table th.wr-k-prior { width: 8%; }    .wr-table th.wr-k-pm    { width: 10%; }
  .wr-table th.wr-k-bg    { width: 10%; }   .wr-table th.wr-k-saved { width: 6.5%; }
  .wr-table th.wr-k-paid  { width: 9%; }    .wr-table th.wr-k-act   { width: 9.5%; }
}
/* --- 999px: the termination date and the SAVED figure move under the name.
       9 columns since .77 added OT hours, sum 100. Every band was re-derived
       when that column arrived rather than nudged: the OT column's share was
       taken back out of name, rate, pay method and background check, which
       are the four with real slack in them. `wage_reduction_test.py` asserts
       each band sums to exactly 100 and that is what caught the first cut
       sitting at 107 and 108.

       Saved is not lost and this is the point: it becomes a sub-line under the
       employee's name, so every row still states what it saved, and the total
       is in the footer and the headline as before. The columns that remain are
       the six that CANNOT move — the rate, the hours, the two ticks, the pay
       method and the Paid chip are all controls, and the last three of those
       are the widest things on the row.

       Why it had to give: at an 813px panel (which is what an 1100px window
       produces — the sidebar takes 250px, DESIGN-NOTES/STATE trap 13) the nine
       columns that used to be here need 102% of the width between them before
       anything is allowed to clip. Every one of those nine was already at its
       own minimum, so there was no arrangement of percentages left to try. --- */
@container (max-width: 999px) {
  .wr-table .wr-k-date { display: none; }
  .wr-table .wr-r-date { display: block; }
  .wr-table .wr-k-saved { display: none; }
  .wr-table .wr-r-saved { display: block; }
  .wr-table th.wr-k-name  { width: 11.5%; } .wr-table th.wr-k-rate  { width: 14%; }
  .wr-table th.wr-k-hrs   { width: 9.5%; }  .wr-table th.wr-k-prior { width: 10%; }
  .wr-table th.wr-k-ot    { width: 8%; }
  .wr-table th.wr-k-pm    { width: 12%; }   .wr-table th.wr-k-bg    { width: 11%; }
  .wr-table th.wr-k-paid  { width: 11.5%; } .wr-table th.wr-k-act   { width: 12.5%; }
}
/* --- 799px: labelled cards. Every field named, nothing hidden, nothing
       clipped, and still no horizontal scroll.

       THE CARDS NOW START AT 799px, NOT 659px, and BACKLOG 91 is why. The
       band between them used to be a nine-column table; add a tenth and the
       columns need 118% of a 660px panel before a single value is allowed to
       clip. Nine columns in 660px was already within two pixels of failing —
       PAY METHOD's header had 64px of room and wanted 62 — and there is no
       arrangement of percentages that fixes an over-subscription. A panel
       under 800px is a card now: every field carries its own label, the row
       actions keep their 44px targets (DESIGN-NOTES 8/9) and nothing is
       hidden. An iPad in portrait is the width this changes. --- */
@container (max-width: 799px) {
  .wr-table, .wr-table thead, .wr-table tbody, .wr-table tfoot, .wr-table tr,
  .wr-table td, .wr-table th { display: block; }
  .wr-table thead, .wr-table tfoot { display: none; }
  .wr-table tbody tr { padding: 12px 14px; box-shadow: inset 0 -1px 0 var(--line);
                       position: relative; }
  .wr-table tbody tr:hover { background: transparent; }
  .wr-table td { box-shadow: none; padding: 3px 0; display: flex; align-items: center;
                 justify-content: space-between; gap: 14px; }
  .wr-table td::before { content: attr(data-l); font-size: var(--fs-2xs);
                         text-transform: uppercase; letter-spacing: var(--tracking-caps);
                         font-weight: var(--fw-semi); color: var(--text-subtle);
                         flex: 0 0 auto; }
  .wr-table td.wr-k-name { display: block; padding-bottom: 6px; }
  .wr-table td.wr-k-name::before { display: none; }
  .wr-table td.wr-k-name .wr-nm { font-size: var(--fs-lg); font-weight: var(--fw-semi); }
  /* Everything comes BACK as a card field. The three columns hidden by the
     wider queries above are still hidden by them at this width — a container
     query at max-width 999px matches at 390px too — so each one has to be
     named again here, at a specificity that beats it. Missing .wr-k-date off
     this list dropped the termination date off the phone entirely, on a page
     about terminations. */
  .wr-table td.wr-k-would, .wr-table td.wr-k-actual,
  .wr-table td.wr-k-date, .wr-table td.wr-k-saved { display: flex; }
  .wr-table td.wr-k-notes { display: block; }
  .wr-table td.wr-k-notes::before { display: block; margin-bottom: 2px; }
  .wr-table td.wr-k-act { display: flex; justify-content: flex-end; gap: 4px; padding-top: 6px; }
  .wr-table td.wr-k-act::before { content: ""; display: none; }
  /* The fold in card mode. `table-row-group` means nothing here — every part
     of the table is a block — so the revealed tbody has to be re-stated, and
     the disclosure bar's cell must not try to be a flex row of its own. */
  .wr-table tbody.wr-more.wr-open { display: block; }
  .wr-table tbody.wr-more-bar td { display: block; }
  .wr-more-inner { display: block; }
  .wr-table tbody.wr-more-bar td::before { content: ""; display: none; }
  .wr-more-note { display: block; margin-top: 6px; }
  .wr-table .wr-r-date, .wr-table .wr-r-note, .wr-table .wr-r-saved { display: none; }
  /* BACKLOG 92: on a card it hugs its own text, exactly like the Paid chip
     on the line below it. The 130px floor it used to carry is what made the
     two look like different controls. */
  select.wr-pm { max-width: 100%; width: auto; min-width: 0; }
  /* The rate cell carries two lines as a card. Let it wrap, or a 320px phone
     gains 44px of horizontal scroll from the provenance line alone. */
  .wr-table td.wr-k-rate { flex-wrap: wrap; justify-content: flex-start; gap: 6px; }
  .wr-table td.wr-k-rate .wr-rate-cell, .wr-rate-cell { white-space: normal; }
  .wr-ei { max-width: none; }
  .wr-ei.wr-n { max-width: 110px; }
  .wr-cardtot { display: flex; }
}

/* --- SPECIFICITY. `form button[type="submit"]:not(.ghost)` is 0-2-2 and lives
       at style-v2.css:459; it paints every submit button inside a <form> as a
       solid teal pill. The row's delete button and the Paid chip are both
       submit buttons inside their own little form, so both came out as teal
       pills — the bin looked like a primary action, and Paid and Not paid
       stopped being green and gold, which is a rule 14 failure as well as an
       ugly one. This is the third round in this repo where that selector has
       quietly won (DESIGN-NOTES 13c).

       Both rules below are SCOPED to `.wr-table .wr-k-act` / `.wr-k-paid` so
       they cannot outrank anything outside this table. --- */
.wr-table td.wr-k-act button.wr-iconbtn {
  background: transparent; background-image: none; color: var(--slate-400);
  box-shadow: none; padding: 5px; min-height: 0; font-size: inherit; }
.wr-table td.wr-k-act button.wr-iconbtn:hover {
  background: var(--slate-100); color: var(--text); box-shadow: none; }
.wr-table td.wr-k-act button.wr-iconbtn.wr-del:hover {
  background: var(--red-50); color: var(--red-700); }
.wr-table td.wr-k-act button.wr-savebtn { color: var(--mint-500); }
.wr-table td.wr-k-act button.wr-savebtn:hover {
  background: var(--mint-50); color: var(--mint-700); }

.wr-table td.wr-k-paid button.wr-paidbtn[data-v="1"] {
  background: var(--green-50); background-image: none; color: var(--green-700);
  box-shadow: inset 0 0 0 1px var(--green-150); }
.wr-table td.wr-k-paid button.wr-paidbtn[data-v="0"] {
  background: var(--gold-50); background-image: none; color: var(--gold-700);
  box-shadow: inset 0 0 0 1px var(--gold-150); }
.wr-table td.wr-k-paid button.wr-paidbtn:hover { filter: brightness(.97); }

/* Same story for the pay-method select's two states, which are set with an
   attribute selector at 0-2-0 and would lose to any form-scoped rule. */
.wr-table td.wr-k-pm select.wr-pm[data-v="Not Payroll"] {
  background-color: var(--mint-100); color: var(--mint-700);
  box-shadow: inset 0 0 0 1px var(--mint-150); }
.wr-table td.wr-k-pm select.wr-pm[data-v="Payroll"] {
  background-color: var(--slate-100); color: var(--slate-700);
  box-shadow: inset 0 0 0 1px var(--slate-200); }

@media (max-width: 760px) {
  .wr-table td.wr-k-act button.wr-iconbtn { padding: 9px; }
}

/* --- the fold ------------------------------------------------------------
   The 15 most recent are open; the rest of the period sits in a SECOND
   <tbody> in the same table, so the columns cannot drift apart from the ones
   above them. Several <tbody> elements in one table is valid HTML and is the
   only way to do this without either a second table or JS that rewrites rows.

   Hidden by default and revealed by `.wr-open` on that tbody. The state is
   also a query parameter, so the fold survives a reload and works with the
   script off. --- */
.wr-table tbody.wr-more { display: none; }
.wr-table tbody.wr-more.wr-open { display: table-row-group; }
/* The bar's <td> must stay a TABLE CELL. `display: flex` on it takes the cell
   out of the column model, `colspan` stops applying, and the bar shrinks to
   the width of its own text instead of spanning the table — which looks like a
   stray button floating under the first column. The flex goes on a div inside.
   Only a screenshot showed this; the button was visible and clickable, just
   the wrong width, so every assertion passed. */
.wr-table tbody.wr-more-bar td { background: var(--slate-50);
  box-shadow: inset 0 -1px 0 var(--line), inset 0 1px 0 var(--line);
  padding: 9px 10px; }
.wr-more-inner { display: flex; align-items: center; gap: var(--sp-3);
  flex-wrap: wrap; }
.wr-more-btn { font-size: var(--fs-sm); font-weight: var(--fw-semi);
  color: var(--teal-700); text-decoration: none; background: var(--surface);
  padding: 5px 12px; border-radius: var(--r-pill); box-shadow: var(--sh-key);
  display: inline-flex; align-items: center; min-height: 30px; cursor: pointer; }
.wr-more-btn:hover { background: var(--surface-hover); text-decoration: none; }
.wr-more-btn:focus-visible { outline: 2px solid var(--teal-400); outline-offset: 2px; }
.wr-more-note { font-size: var(--fs-xs); color: var(--text-subtle); }
@media (max-width: 760px) { .wr-more-btn { min-height: 40px; font-size: var(--fs-base); } }

/* --- the people you have removed ----------------------------------------- */
.wr-blocked { list-style: none; margin: 0; padding: 0; }
.wr-blocked-item { display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-3); padding: 10px 16px; box-shadow: inset 0 -1px 0 var(--line-subtle);
  flex-wrap: wrap; }
.wr-blocked-name { font-weight: var(--fw-medium); }
.wr-blocked-when { font-size: var(--fs-xs); color: var(--text-subtle); margin-left: 8px; }
.wr-blocked-foot { font-size: var(--fs-sm); line-height: var(--lh-sm);
  color: var(--text-muted); margin: 0; padding: 12px 16px;
  box-shadow: inset 0 1px 0 var(--line); }

/* --- the Undo offer ------------------------------------------------------- */
.wr-undo { position: fixed; left: 50%; transform: translateX(-50%); bottom: 20px;
           background: var(--slate-800); color: #fff; border-radius: var(--r-md);
           padding: 10px 14px; box-shadow: var(--sh-3); display: flex; align-items: center;
           gap: 14px; font-size: var(--fs-sm); z-index: 20; max-width: 92vw; }
.wr-undo.wr-gone { display: none; }
.wr-undo-btn { font: inherit; font-size: var(--fs-sm); font-weight: var(--fw-semi);
               background: transparent; border: 0; color: var(--mint-300);
               cursor: pointer; padding: 2px 4px; display: inline-flex;
               align-items: center; gap: 6px; }

/* --- the collapsed explainer at the foot ---------------------------------- */
.wr-collapsible > summary { cursor: pointer; list-style: none; align-items: center; }
.wr-collapsible > summary::-webkit-details-marker { display: none; }
.wr-collapsible > summary:hover { background: var(--surface-hover); }
.wr-collapsible[open] > summary { box-shadow: inset 0 -1px 0 var(--line); }
.wr-collapsible > summary:focus-visible { outline: 2px solid var(--teal-400); outline-offset: -2px; }
.wr-chev { width: 9px; height: 9px; flex: 0 0 auto; margin-right: 4px;
           border-right: 2px solid var(--slate-400); border-bottom: 2px solid var(--slate-400);
           transform: rotate(45deg) translate(-2px, -2px);
           transition: transform var(--t-fast) ease; }
.wr-collapsible[open] .wr-chev { transform: rotate(-135deg) translate(-2px, -2px); }
.wr-flags { list-style: none; margin: 0; padding: 0; }
.wr-flag { padding: 12px 16px 12px 18px; box-shadow: inset 0 -1px 0 var(--line-subtle);
           position: relative; }
.wr-flag:last-child { box-shadow: none; }
.wr-flag::before { content: ""; position: absolute; left: 0; top: 12px; bottom: 12px;
                   width: 3px; border-radius: var(--r-pill); }
.wr-flag-warn::before { background: var(--gold-300); }
.wr-flag-neutral::before { background: var(--slate-300); }
.wr-flag-info::before { background: var(--blue-700); }
.wr-flag-t { font-size: var(--fs-md); line-height: var(--lh-md);
             font-weight: var(--fw-semi); margin-bottom: 3px; }
.wr-flag-b { font-size: var(--fs-sm); line-height: var(--lh-sm);
             color: var(--text-muted); overflow-wrap: break-word; }

/* --- phone: 44px targets, 16px inputs. DESIGN-NOTES 8 and 9 — mobile is not
       secondary, and --fs-base must stay 16px, the iOS no-zoom floor. --- */
@media (max-width: 760px) {
  .wr-pbtn, .wr-chip { min-height: 40px; font-size: var(--fs-base); }
  .wr-af input, .wr-af select { font-size: var(--fs-base); min-height: 44px; }
  .wr-rates input { font-size: var(--fs-base); }
  .wr-ei { font-size: var(--fs-base); min-height: 38px; }
  .wr-note { font-size: var(--fs-base); min-height: 38px; }
  /* BACKLOG 92 on the phone: the same HEIGHT, which is what "the same size"
     means once both are on their own line in a card. The font differs on
     purpose -- --fs-base is the 16px iOS no-zoom floor and it applies to a
     <select>, which is a form control; the Paid chip is a button and zooms
     nothing. Matching the type here would have made the Paid chip huge. */
  select.wr-pm { min-height: 38px; height: auto; font-size: var(--fs-base); }
  .wr-paidbtn { min-height: 38px; }
  .wr-iconbtn { padding: 9px; }
  .wr-bx { width: 22px; height: 22px; }
  .wr-ck input:checked + .wr-bx::after { left: 7px; top: 4px; width: 6px; height: 11px; }
}

/* ==========================================================================
   AUTOMATIONS  —  /automations                    (BACKLOG 99, 08/08/2026)

   Ported from `claude/automation-inventory-mockup.html`, which was approved
   with Scott. Every value here is a token from the ramps above; nothing new
   was invented. The mockup carried its own :root copy of those ramps because
   it was a standalone file — that copy is deliberately NOT brought across.

   EVERYTHING IS `.au-*`. The mockup used generic names — .card, .chip,
   .badge, .grid, .detail, .note, .steps, .meta — and every one of those would
   have collided with the ~5,400 lines already in style.css. The four things
   that are NOT prefixed are reuses on purpose: `.list-head`, `.list-sub`,
   `.stat-row`/`.stat-card`/`.stat-label`/`.stat-num`, `.filter-clear` and
   `empty_state()`'s `.empty`.

   ⚠️ RULE 14. Colour on this page is functional and is measured, not chosen:
   three platform hues (copper / teal / violet) and six function hues (blue /
   red / gold / mint / violet / slate). tests/automations_test.py scores every
   pair inside each set on the worst CIEDE2000 across normal, protan, deutan
   and tritan vision AT ONCE and also requires a greyscale lightness gap.
   **Do not consolidate these onto one hue** — two redesign mockups in this
   codebase were already rejected for exactly that. Colour is never the only
   signal: every badge carries its text label, which is what makes it safe.

   Nothing here selects `.kv-tone-*`, `.side-tag-*`, `.waitlist-panel`,
   `.maint-panel`, `.rate-panel` or `.rp-side-label`.
   ========================================================================== */

.au-root .list-head { align-items: baseline; }
.au-stamp { font-size: var(--fs-xs); color: var(--text-subtle);
            font-variant-numeric: tabular-nums; white-space: nowrap; }

/* ---- stat tiles. The shared .stat-card carries the box; only the accent
       colour and the caption are new here. ---- */
.au-stat-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
               gap: var(--sp-3); margin-bottom: 22px; }
.au-stat-row .stat-card { display: block; }
.au-stat-foot { font-size: var(--fs-2xs); line-height: var(--lh-2xs);
                color: var(--text-subtle); margin-top: 2px; }
.au-t-zapier .stat-num { color: var(--copper-600); }
.au-t-mac    .stat-num { color: var(--teal-600); }
.au-t-watch  .stat-num { color: var(--violet-600); }
.au-t-mail   .stat-num { color: var(--blue-600); }

/* ---- filter chips. Anchors, not buttons: the filter state is in the query
       string, so every chip is a real link that works with the script off and
       can be sent to somebody. ---- */
.au-filters { margin-bottom: 10px; }
.au-filter-row { display: flex; flex-wrap: wrap; align-items: center;
                 gap: 6px; margin-bottom: 8px; }
.au-legend { font-size: var(--fs-2xs); line-height: var(--lh-2xs);
             font-weight: var(--fw-semi); text-transform: uppercase;
             letter-spacing: var(--tracking-caps); color: var(--text-subtle);
             width: 64px; flex: 0 0 64px; }
.au-chip { display: inline-flex; align-items: center; gap: 5px; padding: 3px 9px;
           border: 0; border-radius: var(--r-xs); font-family: inherit;
           font-size: var(--fs-sm); line-height: 18px; font-weight: var(--fw-medium);
           color: var(--text-muted); background: var(--surface);
           box-shadow: var(--sh-key); cursor: pointer; text-decoration: none;
           transition: box-shadow var(--t-fast) var(--ease),
                       color var(--t-fast) var(--ease); }
.au-chip:hover { color: var(--text); box-shadow: var(--sh-key-2); text-decoration: none; }
.au-chip-n { font-size: var(--fs-2xs); color: var(--text-subtle);
             font-variant-numeric: tabular-nums; }
.au-chip-on { color: var(--teal-700); background: var(--teal-50);
              box-shadow: 0 0 0 1px var(--teal-150); font-weight: var(--fw-semi); }
.au-chip-on .au-chip-n { color: var(--teal-600); }
.au-dot { width: 7px; height: 7px; border-radius: var(--r-pill); flex: 0 0 7px; }

/* THE PLATFORM HUES — three, and they must stay three.
   Steps chosen by SEARCH, not by eye: tests/automations_test.py scores every
   pair on the worst CIEDE2000 across normal, protan, deutan and tritan vision
   at once AND on the greyscale gap between adjacent luminances, and these
   clear both by a margin (ΔE 25.6, gap 0.133). The mock-up's copper-400 /
   teal-500 / violet-500 did NOT: teal-500 and violet-500 are 0.0002 apart in
   luminance, so they were the same shade in greyscale. */
.au-dot-zapier { background: var(--copper-300); }
.au-dot-mac    { background: var(--teal-600); }
.au-dot-watch  { background: var(--violet-400); }
/* THE FUNCTION HUES — six, and they must stay six.
   The mock-up had hiring blue-500 next to facility violet-500, which is
   ΔE 2.3 under deuteranopia — under this repo's own "below 3 is a bug" line,
   i.e. not colour-coded at all for a red-green colourblind reader. Six hues
   that survive four kinds of vision AND keep a lightness spread is a tight
   search; this set clears ΔE 8.0 with a 0.043 greyscale gap. Re-run the
   search if you change one — do not nudge a step. */
.au-dot-hiring     { background: var(--blue-600); }
.au-dot-compliance { background: var(--red-500); }
.au-dot-staffing   { background: var(--gold-200); }
.au-dot-filing     { background: var(--mint-400); }
.au-dot-facility   { background: var(--violet-700); }
.au-dot-monitor    { background: var(--slate-300); }

.au-searchwrap { flex: 1; min-width: 200px; max-width: 340px; }
.au-search { width: 100%; height: 30px; padding: 0 9px; border: 0;
             border-radius: var(--r-sm); font-family: inherit;
             font-size: var(--fs-md); line-height: 18px; color: var(--text);
             background: var(--surface); box-shadow: var(--sh-key), var(--sh-1); }
.au-search:hover { box-shadow: var(--sh-key-2), var(--sh-1); }
.au-search:focus { outline: none; box-shadow: var(--sh-ring); }
.au-resultline { font-size: var(--fs-xs); color: var(--text-subtle);
                 margin: 2px 0 0; }

/* ---- group heads ---- */
.au-group { margin-top: 26px; }
.au-group-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 4px; }
.au-group-head h2 { margin: 0; font-size: var(--fs-lg); line-height: var(--lh-lg);
                    font-weight: var(--fw-semi); letter-spacing: var(--tracking-tight); }
.au-g-count { font-size: var(--fs-xs); color: var(--text-subtle);
              font-variant-numeric: tabular-nums; }
.au-group-note { font-size: var(--fs-sm); line-height: 19px; color: var(--text-muted);
                 margin: 0 0 12px; max-width: 88ch; }

/* ---- the cards. The only genuinely new component on the page. ---- */
.au-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
           gap: var(--sp-3); align-items: start; }
.au-card { background: var(--surface); border: 0; border-radius: var(--r-lg);
           box-shadow: var(--sh-key), var(--sh-1); overflow: hidden;
           display: flex; flex-direction: column;
           transition: box-shadow var(--t-base) var(--ease); }
.au-card:hover { box-shadow: var(--sh-key), var(--sh-2); }
.au-card:has(.au-details[open]) { box-shadow: 0 0 0 1px var(--teal-150), var(--sh-3); }
.au-rail { height: 3px; width: 100%; }
/* The rail and the dot are the SAME step on purpose — the stripe at the top
   of a card and the dot on the chip that filters to it have to read as one
   colour, or the chip does not obviously belong to the card. */
.au-p-zapier .au-rail { background: var(--copper-300); }
.au-p-mac    .au-rail { background: var(--teal-600); }
.au-p-watch  .au-rail { background: var(--violet-400); }
.au-card-body { padding: 13px 15px 12px; display: flex; flex-direction: column;
                gap: 8px; flex: 1; }
.au-card-head { display: flex; align-items: flex-start; justify-content: space-between;
                gap: 8px; }
.au-card-title { font-size: var(--fs-md); line-height: 19px;
                 font-weight: var(--fw-semi); letter-spacing: var(--tracking-tight);
                 margin: 0; }
.au-what { font-size: var(--fs-sm); line-height: 18px; color: var(--text-muted);
           margin: 0; }
.au-badges { display: flex; flex-wrap: wrap; gap: 4px; }
.au-badge { display: inline-flex; align-items: center; gap: 4px; padding: 2px 6px;
            border: 0; border-radius: var(--r-xs); font-size: var(--fs-xs);
            line-height: 16px; font-weight: var(--fw-medium); white-space: nowrap; }
.au-plat-zapier { background: var(--copper-50); color: var(--copper-700);
                  box-shadow: 0 0 0 1px var(--copper-150); }
.au-plat-mac    { background: var(--teal-50); color: var(--teal-700);
                  box-shadow: 0 0 0 1px var(--teal-150); }
.au-plat-watch  { background: var(--violet-50); color: var(--violet-700);
                  box-shadow: 0 0 0 1px var(--violet-150); }
.au-cat-hiring     { background: var(--blue-50); color: var(--blue-700);
                     box-shadow: 0 0 0 1px var(--blue-150); }
.au-cat-compliance { background: var(--red-50); color: var(--red-700);
                     box-shadow: 0 0 0 1px var(--red-150); }
.au-cat-staffing   { background: var(--gold-50); color: var(--gold-700);
                     box-shadow: 0 0 0 1px var(--gold-150); }
.au-cat-filing     { background: var(--mint-50); color: var(--mint-700);
                     box-shadow: 0 0 0 1px var(--mint-150); }
.au-cat-facility   { background: var(--violet-50); color: var(--violet-700);
                     box-shadow: 0 0 0 1px var(--violet-150); }
.au-cat-monitor    { background: var(--slate-100); color: var(--slate-700);
                     box-shadow: 0 0 0 1px var(--slate-200); }
.au-live { background: var(--ok-bg); color: var(--ok);
           box-shadow: 0 0 0 1px var(--ok-line); }
.au-flag { background: var(--warn-bg); color: var(--warn);
           box-shadow: 0 0 0 1px var(--warn-line); }

.au-meta { display: grid; grid-template-columns: max-content minmax(0, 1fr);
           gap: 3px 10px; margin: 1px 0 0; }
.au-meta dt { font-size: var(--fs-2xs); line-height: 16px; font-weight: var(--fw-semi);
              text-transform: uppercase; letter-spacing: var(--tracking-caps);
              color: var(--text-subtle); white-space: nowrap; }
.au-meta dd { margin: 0; font-size: var(--fs-sm); line-height: 16px; color: var(--text);
              overflow-wrap: break-word; }

/* ---- the disclosure. A real <details>, so it opens with the script off and
       is keyboard-operable without a single line of JavaScript. ---- */
.au-details { box-shadow: inset 0 1px 0 var(--line); }
.au-disclose { list-style: none; cursor: pointer; display: flex; align-items: center;
               gap: 5px; padding: 9px 15px; font-size: var(--fs-sm);
               font-weight: var(--fw-semi); color: var(--link); }
.au-disclose::-webkit-details-marker { display: none; }
.au-disclose:hover { background: var(--surface-hover); }
.au-disclose:focus-visible { outline: none; box-shadow: var(--sh-ring); }
.au-caret { display: inline-flex; line-height: 0;
            transition: transform var(--t-base) var(--ease); }
.au-caret .ico { width: 13px; height: 13px; }
.au-details[open] .au-caret { transform: rotate(90deg); }

/* ---- BACKLOG 159. Scott's own notes, one collapsed section per card, sitting
       under How it works. Same <details> mechanics as the disclosure above it
       and the same .au-disclose summary, so it is a 44px target on a phone and
       opens with the script off. ---- */
.au-notes { box-shadow: inset 0 1px 0 var(--line); }
.au-notes[open] .au-caret { transform: rotate(90deg); }
.au-notes .au-disclose { color: var(--text-subtle); }
.au-notes[open] .au-disclose { color: var(--link); }
.au-notes .au-g-count { margin-left: 6px; }
/* A note is somebody's typed text: keep the line breaks they typed. */
.au-note-body { margin: 10px 0 4px; font-size: var(--fs-sm); line-height: 18px;
                color: var(--text); white-space: pre-wrap;
                overflow-wrap: anywhere; }
.au-note-empty { margin: 10px 0 4px; font-size: var(--fs-sm);
                 color: var(--text-subtle); font-style: italic; }
.au-note-stamp { margin: 0 0 8px; font-size: var(--fs-2xs);
                 line-height: var(--lh-2xs); color: var(--text-subtle); }
.au-note-form { margin: 6px 0 0; }
.au-note-label { display: block; margin: 0 0 4px; font-size: var(--fs-2xs);
                 line-height: var(--lh-2xs); font-weight: var(--fw-semi);
                 text-transform: uppercase;
                 letter-spacing: var(--tracking-caps); color: var(--text-subtle); }
/* NO font-size HERE, AND THAT IS DELIBERATE. `textarea:not([hidden])` is
   0-1-1 and a bare `.au-note-input` is 0-1-0, so a size set here would LOSE
   and sit in the file looking like it worked -- the same specificity trap the
   comment at the top of the shared input block describes. The shared rules
   already do the right thing, including the 16px iOS no-zoom floor on a
   phone, so this box inherits them and only sets what they do not cover.
   Measured 08/21/2026: 16px at 390px, 14px at 1440px, same as every other
   textarea in the app. */
.au-note-input { width: 100%; box-sizing: border-box; resize: vertical;
                 min-height: 76px; }
.au-note-actions { display: flex; align-items: center; gap: 10px;
                   flex-wrap: wrap; margin-top: 8px; }
.au-note-hint { font-size: var(--fs-2xs); line-height: var(--lh-2xs);
                color: var(--text-subtle); }
.au-detail, .au-notebody { padding: 2px 15px 15px; background: var(--slate-50);
             box-shadow: inset 0 1px 0 var(--line); }
.au-detail h4 { margin: 11px 0 5px; font-size: var(--fs-2xs); line-height: var(--lh-2xs);
                font-weight: var(--fw-semi); text-transform: uppercase;
                letter-spacing: var(--tracking-caps); color: var(--text-subtle); }
.au-steps { margin: 0; padding: 0; list-style: none; counter-reset: austep; }
.au-step { counter-increment: austep; position: relative; padding: 3px 0 3px 24px;
           font-size: var(--fs-sm); line-height: 17px; color: var(--text); }
.au-step::before { content: counter(austep); position: absolute; left: 0; top: 3px;
                   width: 17px; height: 17px; border-radius: var(--r-xs);
                   background: var(--surface); box-shadow: var(--sh-key);
                   font-size: 10px; line-height: 17px; text-align: center;
                   color: var(--text-subtle); font-weight: var(--fw-semi);
                   font-variant-numeric: tabular-nums; }
.au-step-app { font-weight: var(--fw-semi); color: var(--teal-700); }
/* A branch step is one of two outcomes, not the next thing that happens. */
.au-branch::before { background: var(--gold-50); box-shadow: 0 0 0 1px var(--gold-150);
                     color: var(--gold-700); }
.au-pillrow { display: flex; flex-wrap: wrap; gap: 4px; }
.au-apppill { font-size: var(--fs-xs); line-height: 16px; padding: 2px 6px;
              border-radius: var(--r-xs); background: var(--surface);
              box-shadow: var(--sh-key); color: var(--text-muted); }
.au-kv { display: grid; grid-template-columns: max-content minmax(0, 1fr);
         gap: 4px 10px; margin: 0; }
.au-kv dt { font-size: var(--fs-xs); color: var(--text-subtle); white-space: nowrap; }
.au-kv dd { margin: 0; font-size: var(--fs-sm); line-height: 17px; }
.au-note { margin-top: 10px; padding: 8px 10px; border-radius: var(--r-md);
           background: var(--warn-bg); box-shadow: 0 0 0 1px var(--warn-line);
           font-size: var(--fs-sm); line-height: 17px; color: var(--warn); }
.au-linkrow { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 11px; }
.au-linkrow a { font-size: var(--fs-sm); font-weight: var(--fw-medium); }
.au-root code, .au-mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
                          font-size: 12px; background: var(--surface); padding: 1px 4px;
                          border-radius: var(--r-xs); box-shadow: var(--sh-key);
                          color: var(--slate-700); overflow-wrap: anywhere; }
.au-group-note code { background: var(--slate-100); box-shadow: none; }

.au-foot { margin-top: 34px; padding-top: 14px; box-shadow: inset 0 1px 0 var(--line);
           font-size: var(--fs-xs); color: var(--text-subtle); line-height: 18px; }
.au-foot strong { color: var(--text-muted); font-weight: var(--fw-semi); }

/* --- phone. DESIGN-NOTES 8/9: 44px targets, 16px inputs, and the two
       densities stay different on purpose. --- */
@media (max-width: 760px) {
  .au-grid { grid-template-columns: 1fr; }
  .au-legend { width: auto; flex: 0 0 100%; }
  .au-searchwrap { max-width: none; }
  .au-stat-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .au-search { height: 44px; font-size: var(--fs-base); }
  .au-chip { min-height: 32px; padding: 5px 10px; }
  .au-disclose { padding: 13px 15px; min-height: 44px; }
  .au-stamp { white-space: normal; }
}

/* --- Automations: switched off. Scott turned a Zap off on 08/08/2026 to see
       what this page would do, and it went on saying "Active" — the badge was
       a hardcoded word rather than a reading. It comes from the record now,
       and a switched-off card is quiet without being hidden: he needs to see
       that the page KNOWS it is off, which is the whole point of having
       looked. It is greyed rather than removed because a Zap that is off
       during a changeover is still a thing he is managing. --- */
.au-off { background: var(--slate-100); color: var(--slate-700);
          box-shadow: 0 0 0 1px var(--slate-200); }
.au-card.au-is-off { background: var(--slate-50); }
.au-card.au-is-off .au-rail { background: var(--slate-300); }
.au-card.au-is-off .au-card-title,
.au-card.au-is-off .au-what,
.au-card.au-is-off .au-meta dd { color: var(--text-subtle); }
.au-caveat { font-size: var(--fs-xs); line-height: var(--lh-xs);
             color: var(--text-muted); margin: -14px 0 18px; max-width: 76ch; }

/* --- BACKLOG 103: the on/off switch, the fold, and delete. -----------------
       The switch IS the status word -- a control next to a badge saying the
       same thing reads as two facts that could disagree. It reuses the card
       palette (au-live / au-off) rather than introducing a colour, because
       that palette was searched against four kinds of colour vision plus a
       greyscale spread and a new hue would not have been. --- */
.au-tg-form { margin: 0; flex: 0 0 auto; }
.au-tg { display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
         font: inherit; font-size: var(--fs-xs); line-height: var(--lh-xs);
         font-weight: var(--fw-semi); padding: 3px 8px 3px 4px;
         border: 0; border-radius: 999px; white-space: nowrap; }
.au-tg-track { position: relative; width: 26px; height: 14px; flex: 0 0 auto;
               border-radius: 999px; background: transparent; }
/* The dimming lives on a ::before that FILLS the track, NOT on the track
   itself. `opacity` on .au-tg-track applied to the knob as well -- the knob
   is its child -- so the switched-off knob rendered at half strength on top
   of a half-strength track. Scott, 09/08/2026: "you can't tell that there is
   a button there." A ::before is a sibling of the knob, so the knob now
   stays at full strength in both states. */
.au-tg-track::before { content: ""; position: absolute; inset: 0;
                       border-radius: 999px; background: currentColor;
                       opacity: .55; transition: opacity .12s ease; }
.au-tg-knob { position: absolute; top: 2px; left: 2px; width: 10px; height: 10px;
              border-radius: 50%; background: var(--surface, #fff);
              box-shadow: 0 0 0 1px rgba(23, 29, 29, .12);
              transition: transform .12s ease; }
.au-tg[aria-pressed="true"] .au-tg-track::before { opacity: 1; }
.au-tg[aria-pressed="true"] .au-tg-knob { transform: translateX(12px); }
.au-tg:focus-visible { outline: 2px solid var(--focus, #2563eb); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
  .au-tg-track::before, .au-tg-knob { transition: none; }
}

.au-cardfoot { padding: 0 16px 14px; }

/* The fold. Same shape as the Wage Reduction fold on purpose -- item 101
   (archive documents) needs the identical thing and this one sets it. */
.au-fold { margin: 26px 0 8px; border: 1px solid var(--border);
           border-radius: 12px; background: var(--slate-50); }
.au-fold-head { display: flex; align-items: center; gap: 8px; cursor: pointer;
                padding: 12px 16px; list-style: none;
                font-size: var(--fs-sm); font-weight: var(--fw-semi);
                color: var(--text-muted); }
.au-fold-head::-webkit-details-marker { display: none; }
.au-fold[open] .au-caret { transform: rotate(90deg); }
.au-fold-body { padding: 2px 16px 18px; }
.au-fold-sub { font-size: var(--fs-sm); color: var(--text-muted);
               margin: 22px 0 4px; }
.au-fold-note { font-size: var(--fs-xs); line-height: var(--lh-xs);
                color: var(--text-subtle); margin: 0 0 10px; max-width: 76ch; }
.au-gone-list { list-style: none; margin: 0; padding: 0;
                display: flex; flex-direction: column; gap: 6px; }
.au-gone-row { display: flex; align-items: center; gap: 12px; padding: 8px 12px;
               background: var(--surface, #fff); border: 1px solid var(--border);
               border-radius: 8px; }
.au-gone-name { font-size: var(--fs-sm); font-weight: var(--fw-semi);
                color: var(--text-muted); }
.au-gone-plat { font-size: var(--fs-xs); color: var(--text-subtle);
                margin-right: auto; }

@media (max-width: 760px) {
  /* DESIGN-NOTES 8/9: 44px targets on a phone. The fold's summary is the
     control that reveals everything switched off, so it is one. */
  .au-fold-head { min-height: 44px; padding: 12px 12px; }
  .au-fold-body { padding: 2px 10px 16px; }
  .au-gone-row { flex-wrap: wrap; gap: 6px 10px; }
  .au-gone-plat { margin-right: 0; width: 100%; }
  .au-tg { padding: 6px 10px 6px 6px; }
}


/* --- THE SWITCH LOOKED LIKE NOTHING. DESIGN-NOTES 13c, FIFTH OCCURRENCE. ---
       Scott, 09/08/2026, with a screenshot: "you can't tell that there is a
       button there."

       Measured in a browser rather than guessed at. `button.au-tg` was
       rendering background rgb(30,98,105), color #fff, padding 6px 12px,
       border-radius 6px. It asks for none of those. It is a submit button
       inside a <form>, so `form button[type="submit"]:not(.ghost)` (0-2-2,
       style-v2.css:459) beat `.au-tg.au-live` (0-2-0) on every one of them
       and painted the control as a solid teal primary pill. The track is
       `currentColor` -- which that rule had just set to WHITE -- and the knob
       is white too. A white knob on a white track is a white blob, which is
       exactly what the screenshot showed.

       This is the same selector that has quietly won in three earlier rounds
       (the Wage Reduction bin and Paid chip, `.wr-pm`, `.icon-btn-danger`).
       Per 13c the answer is to MATCH the winning specificity in a scoped way,
       not to escalate past it: every rule below starts `.au-root
       form.au-tg-form button.au-tg` (0-3-2), which cannot reach a single
       element outside the Automations page.

       No new colour is introduced. au-live / au-off are the palette that was
       searched against four kinds of colour vision plus a greyscale spread
       (DESIGN-NOTES 14) -- the switch was always meant to wear it and never
       got the chance. The one deliberate departure from the badges is a
       one-step-darker keyline (green-200 / slate-300 instead of the badges'
       -150), so the control does not read as a fourth badge sitting in a row
       of badges. `tests/au_toggle_browser_check.py` section 1b now measures
       the rendered pixels of the knob against the track in both states and
       fails under 3:1, so this cannot silently revert again. --- */
.au-root form.au-tg-form button.au-tg {
  background-image: none;
  padding: 3px 9px 3px 4px;
  border-radius: 999px;
  min-height: 0;
  font-size: var(--fs-xs);
  line-height: var(--lh-xs);
  font-weight: var(--fw-semi);
  transform: none;
  filter: none;
  cursor: pointer;
}
.au-root form.au-tg-form button.au-tg.au-live {
  background: var(--ok-bg); color: var(--ok);
  box-shadow: 0 0 0 1px var(--green-200);
}
.au-root form.au-tg-form button.au-tg.au-off {
  background: var(--slate-100); color: var(--slate-700);
  box-shadow: 0 0 0 1px var(--slate-300);
}
/* Hover has to be restated here. `form button[type="submit"]:not(.ghost):hover`
   is 0-3-2 and the rules above are 0-3-2 + a class, so they now outrank the
   incumbent's hover as well -- without these two the control would have no
   hover at all, which is half of what tells you a thing is pressable. */
.au-root form.au-tg-form button.au-tg.au-live:hover {
  background: var(--green-100); box-shadow: 0 0 0 1px var(--ok); }
.au-root form.au-tg-form button.au-tg.au-off:hover {
  background: var(--slate-150); box-shadow: 0 0 0 1px var(--slate-400); }
.au-root form.au-tg-form button.au-tg:active { transform: none; }

@media (max-width: 760px) {
  /* Same reason: the 0-1-0 mobile padding rule above now loses to the block
     above it, so the phone padding is restated at matching specificity.
     DESIGN-NOTES 9 -- this stays a real target on a phone. */
  .au-root form.au-tg-form button.au-tg { padding: 6px 12px 6px 8px;
                                          min-height: 44px; }
}


/* --- BACKLOG 101: the archived-documents fold. ----------------------------
       Scott, 08/08/2026: archive documents, never delete them; a collapsed
       section at the bottom holds the archived ones and is the ONLY place a
       permanent delete lives.

       Deliberately the same shape as the Automations fold (`.au-fold`) rather
       than a shared class: the two live on different pages with different
       table markup, and one selector serving both is how a change to one page
       silently moves the other. The SHAPE is shared; the rules are not.

       DESIGN-NOTES 13c, which cost a round on 09/08/2026: every control in
       here is a `<button type="submit">` inside a `<form>`, so
       `form button[type="submit"]:not(.ghost)` (0-2-2) paints it teal unless
       it carries `.ghost`. Archive and Delete permanently both do, on purpose
       -- Restore is the only primary action in the fold. --- */
/* The icon sits flush against the word without this -- `.ico` is inline-block
   and the whitespace between the two collapses inside a flex button. */
.docs-arc .ico { margin-right: 4px; }

.docs-fold { margin: 22px 0 6px; border: 1px solid var(--border);
             border-radius: 12px; background: var(--slate-50); }
.docs-fold-head { display: flex; align-items: center; gap: 8px; cursor: pointer;
                  padding: 12px 16px; list-style: none;
                  font-size: var(--fs-sm); font-weight: var(--fw-semi);
                  color: var(--text-muted); }
.docs-fold-head::-webkit-details-marker { display: none; }
.docs-fold-head:hover { background: var(--surface-hover); border-radius: 12px; }
.docs-fold-head:focus-visible { outline: none; box-shadow: var(--sh-ring); }
.docs-fold-caret { display: inline-flex; line-height: 0;
                   transition: transform .12s ease; }
.docs-fold-caret .ico { width: 13px; height: 13px; }
.docs-fold[open] .docs-fold-caret { transform: rotate(90deg); }
.docs-fold-body { padding: 0 14px 14px; }
.docs-fold-note { font-size: var(--fs-xs); line-height: var(--lh-xs);
                  color: var(--text-subtle); margin: 0 0 8px; max-width: 76ch; }
/* `.docs-count` is white-on-teal for the section bands; on the fold's pale
   head that reads as an empty box. */
.docs-fold-head .docs-count { background: var(--slate-150); color: var(--text);
                              margin-left: auto; }
.docs-fold table.docs-table { background: var(--surface); border-radius: 8px; }

/* DESKTOP ONLY, and the media query is the whole point. -------------------
   The archived row carries TWO controls where a live row carries one, and the
   sitewide cell is `width: 14%; white-space: nowrap`. Two nowrap buttons in a
   14% cell do not shrink the cell -- they widen the TABLE, and the table then
   hung 77px off the right of the fold at 1440px and 122px at 1100px.

   Re-widening the columns fixes that and WRECKS the phone, because below
   761px `mobile-tables.js` and style.css:1445 turn every row into a card and
   set the widths back to auto. Holding a 26% name column through that
   produced a 60px-wide cell breaking "medication-list.pdf" across five lines
   and pushing Restore off the left edge. So these only exist above 760px, and
   the phone keeps the card treatment it already had.
   `tests/docs_archive_browser_check.py` now measures overflow in BOTH
   directions at four widths -- the first version only looked right, and the
   phone was broken to the left. --- */
@media (min-width: 761px) {
  .docs-fold table.docs-table { table-layout: fixed; width: 100%; }
  .docs-fold table.docs-table td.docs-name { width: 26%; }
  .docs-fold table.docs-table td.docs-date { width: 11%; }
  .docs-fold table.docs-table td.docs-type { width: 14%; }
  .docs-fold table.docs-table td.docs-meta { width: 22%; white-space: normal; }
  .docs-fold table.docs-table td.doc-actions-cell { width: 27%; white-space: normal;
                                                    padding-right: 12px !important; }
  /* ...and the sitewide rule zeroes those forms' margins with !important, so
     without this they would touch. */
  .docs-fold table.docs-table td.doc-actions-cell form + form { margin-left: 6px !important; }
}

@media (max-width: 760px) {
  /* DESIGN-NOTES 8/9: the summary is the control that reveals everything
     archived, so it is a 44px target. */
  .docs-fold-head { min-height: 44px; padding: 12px 12px; }
  .docs-fold-body { padding: 0 9px 12px; }
  .docs-fold table.docs-table td.doc-actions-cell { white-space: normal; }
  .docs-fold table.docs-table td.doc-actions-cell form + form { margin-left: 0 !important;
                                                                margin-top: 6px !important; }
}


/* ==========================================================================
   BACKLOG 105 — CALL BELLS  (round 1, 09/08/2026)

   Everything here is prefixed `.cb-`. The reuses are deliberate and are the
   same ones the Automations page makes: `.list-head`, `.list-sub`,
   `.stat-row` / `.stat-card` / `.stat-label` / `.stat-num`, `.data-table` and
   `empty_state()`'s `.empty`.

   ⚠️ DESIGN-NOTES 13c, SEVENTH OCCURRENCE — WRITTEN IN THE SAME ROUND AS THE
   CONTROLS, NOT AFTER SOMEBODY NOTICED. `form button[type="submit"]:not(.ghost)`
   (0-2-2, style-v2.css:459) has now beaten a component rule six times:
   `.icon-btn-danger`, the Wage Reduction bin, the Paid chip, `select.wr-pm`,
   the Automations on/off switch and the archived-documents fold. This page
   adds SIX new submit buttons inside forms — listening on, listening off,
   Claim, Changed battery, Retire, Put back — so every one of them would have
   started life as a solid teal primary pill. The standing rule now is to
   assume that and write the scoped rule the same day.

   Per 13c the answer is to MATCH the winning specificity in a scoped way, not
   to escalate past it. Every button rule below is
   `.cb-root form.cb-form button.cb-btn` (0-3-2) — one class more specific
   than the incumbent, and unable to reach a single element off this page.
   Hovers are restated for the same reason the Automations block restates
   them: the incumbent's :hover is 0-3-2, so without these the controls would
   have no hover at all, and hover is half of what tells you a thing is
   pressable.

   ⚠️ DESIGN-NOTES 13f. The column widths are inside `@media (min-width: 761px)`
   only. Below 761px `mobile-tables.js` and `style.css:1445` turn these rows
   into cards and set widths back to auto; a held percentage there produces a
   sliver of a cell and pushes a control off the LEFT edge of the screen.

   ⚠️ DESIGN-NOTES 14. The five silence states carry the app's existing
   semantic tokens (ok / warn / danger / neutral), which are already measured,
   rather than a new palette invented for this page. Colour is never the only
   signal: every state pill states its word — Normal, Quiet, Silent, Learning,
   Retired — and the sentence beside it says why.
   ========================================================================== */

.cb-root .list-head { align-items: baseline; }
.cb-stamp { font-size: var(--fs-xs); color: var(--text-subtle); white-space: nowrap; }

/* minmax(160px, 1fr), NOT minmax(0, 1fr). The Automations row uses 0 and gets
   away with it because its labels are one short word; "Unclaimed codes" and
   "heard in the last 7 days" in a 90px column on a phone break one word per
   line and the tile becomes unreadable — DESIGN-NOTES 13f's second half, the
   part about content still having room to READ. A real minimum wraps to two
   columns at 390px instead. */
.cb-stat-row { display: grid;
               grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
               gap: var(--sp-3); margin-bottom: 22px; }
.cb-stat-row .stat-card { display: block; }
.cb-stat-foot { font-size: var(--fs-2xs); line-height: var(--lh-2xs);
                color: var(--text-subtle); margin-top: 2px; }
.cb-t-buttons .stat-num { color: var(--teal-600); }
.cb-t-calls   .stat-num { color: var(--blue-600); }
.cb-t-quiet   .stat-num { color: var(--gold-700); }
.cb-t-unknown .stat-num { color: var(--violet-600); }

.cb-msg { background: var(--info-bg); color: var(--info);
          box-shadow: 0 0 0 1px var(--info-line); border-radius: var(--r-sm);
          padding: 8px 12px; margin-bottom: 14px; font-size: var(--fs-sm); }

/* ---- the listening banner ---- */
.cb-listen { display: flex; flex-wrap: wrap; align-items: center; gap: 12px;
             justify-content: space-between; background: var(--surface);
             box-shadow: var(--sh-key), var(--sh-1); border-radius: var(--r-md);
             padding: 12px 14px; margin-bottom: 20px; }
.cb-listen-text { flex: 1 1 320px; font-size: var(--fs-sm);
                  line-height: var(--lh-sm); color: var(--text-muted); }
.cb-listen-on { background: var(--teal-50); box-shadow: 0 0 0 1px var(--teal-150); }
.cb-listen-left { display: block; margin-top: 4px; font-size: var(--fs-2xs);
                  color: var(--text-subtle); }
.cb-listen-form { flex: 0 0 auto; }

/* ---- blocks ---- */
.cb-block { margin-bottom: 26px; }
.cb-block-head { display: flex; align-items: baseline; justify-content: space-between;
                 gap: 10px; flex-wrap: wrap; margin-bottom: 4px; }
.cb-block-head h2 { font-size: var(--fs-xl); line-height: var(--lh-xl); margin: 0; }
.cb-block-note { font-size: var(--fs-sm); line-height: var(--lh-sm);
                 color: var(--text-muted); margin: 0 0 10px; max-width: 78ch; }
.cb-warn { display: block; margin-top: 4px; color: var(--warn-text); }
.cb-sub { display: block; font-size: var(--fs-2xs); line-height: var(--lh-2xs);
          color: var(--text-subtle); }
.cb-code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
           font-size: var(--fs-sm); }
.cb-num { font-variant-numeric: tabular-nums; }

.cb-chip { display: inline-flex; align-items: center; padding: 3px 9px; border: 0;
           border-radius: var(--r-xs); font-size: var(--fs-sm); line-height: 18px;
           font-weight: var(--fw-medium); color: var(--text-muted);
           background: var(--surface); box-shadow: var(--sh-key);
           text-decoration: none; }
.cb-chip:hover { color: var(--text); box-shadow: var(--sh-key-2); text-decoration: none; }
.cb-chip-on { color: var(--teal-700); background: var(--teal-50);
              box-shadow: 0 0 0 1px var(--teal-150); font-weight: var(--fw-semi); }

.cb-tag { display: inline-block; margin-left: 6px; padding: 1px 6px;
          border-radius: var(--r-xs); font-size: var(--fs-2xs);
          background: var(--slate-100); color: var(--slate-700); }
.cb-tag-odd { background: var(--gold-50); color: var(--gold-700);
              box-shadow: 0 0 0 1px var(--gold-150); }

/* ---- the five silence states. The WORD is in the pill; the colour is a
       second signal, never the only one. ---- */
.cb-state { display: inline-block; padding: 1px 8px; border-radius: var(--r-pill);
            font-size: var(--fs-2xs); line-height: 17px; font-weight: var(--fw-semi); }
.cb-s-normal   { background: var(--ok-bg);      color: var(--ok);
                 box-shadow: 0 0 0 1px var(--ok-line); }
.cb-s-quiet    { background: var(--warn-bg);    color: var(--warn-text);
                 box-shadow: 0 0 0 1px var(--warn-border); }
.cb-s-silent   { background: var(--danger-bg);  color: var(--danger);
                 box-shadow: 0 0 0 1px var(--danger-line); }
.cb-s-learning { background: var(--info-bg);    color: var(--info);
                 box-shadow: 0 0 0 1px var(--info-line); }
.cb-s-retired  { background: var(--neutral-bg); color: var(--neutral);
                 box-shadow: 0 0 0 1px var(--neutral-line); }

/* ---- the fold ---- */
.cb-fold { background: var(--surface); box-shadow: var(--sh-key);
           border-radius: var(--r-md); padding: 10px 14px; margin-bottom: 26px; }
.cb-fold summary { cursor: pointer; display: flex; align-items: center; gap: 8px; }
.cb-fold-title { font-weight: var(--fw-semi); }
.cb-fold-n { font-size: var(--fs-2xs); color: var(--text-subtle);
             background: var(--slate-100); border-radius: var(--r-pill);
             padding: 0 7px; }
.cb-fold-note { font-size: var(--fs-sm); color: var(--text-muted);
                margin: 8px 0; max-width: 78ch; }

/* ---- forms and controls ---- */
.cb-form { margin: 0; display: inline-flex; align-items: center; gap: 6px; }
.cb-actions { display: flex; flex-wrap: wrap; gap: 6px; }
.cb-claim { flex-wrap: wrap; }

/* THE SIX SUBMIT BUTTONS. 0-3-2 — see the header of this block. */
.cb-root form.cb-form button.cb-btn {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  background: var(--surface);
  background-image: none;
  color: var(--text-muted);
  box-shadow: var(--sh-key), var(--sh-1);
  border-radius: var(--r-sm);
  padding: 4px 10px;
  min-height: 0;
  font-family: inherit;
  font-size: var(--fs-sm);
  line-height: var(--lh-sm);
  font-weight: var(--fw-medium);
  filter: none;
  transform: none;
  cursor: pointer;
}
.cb-root form.cb-form button.cb-btn:hover {
  background: var(--surface-hover); color: var(--text);
  box-shadow: var(--sh-key-2), var(--sh-1); }
.cb-root form.cb-form button.cb-btn:active { transform: none; }
.cb-root form.cb-form button.cb-btn:focus-visible {
  outline: 2px solid var(--teal-600); outline-offset: 2px; }

/* The one control on the page that IS meant to look primary. Stated rather
   than inherited, so it cannot drift when the incumbent changes. */
.cb-root form.cb-form button.cb-btn.cb-btn-primary {
  background: var(--brand); color: var(--text-onbrand); box-shadow: var(--sh-1); }
.cb-root form.cb-form button.cb-btn.cb-btn-primary:hover {
  background: var(--brand-hover); box-shadow: var(--sh-2); }

.cb-root form.cb-form button.cb-btn.cb-btn-quiet {
  background: var(--slate-100); color: var(--slate-700);
  box-shadow: 0 0 0 1px var(--slate-200); }
.cb-root form.cb-form button.cb-btn.cb-btn-quiet:hover {
  background: var(--slate-150); box-shadow: 0 0 0 1px var(--slate-400); }

.cb-root form.cb-form button.cb-btn.cb-btn-stop {
  background: var(--surface); color: var(--danger);
  box-shadow: 0 0 0 1px var(--danger-line); }
.cb-root form.cb-form button.cb-btn.cb-btn-stop:hover {
  background: var(--danger-bg); box-shadow: 0 0 0 1px var(--danger); }

/* BACKLOG 110 — "Clear all unclaimed codes". THE SEVENTH TIME the 0-2-2 rule
   would have won, and it is written before it wins rather than after.
   Without this, `form button[type="submit"]:not(.ghost)` renders a wipe of
   production data as a solid teal pill IDENTICAL to Claim, sitting a few
   inches from it. Same 0-3-2 scoping as every rule above.

   It is the danger OUTLINE and not a filled red button on purpose: red fill
   is this app's "primary action here" shape, and the primary action on this
   panel is claiming a code, not destroying one. Colour is not the only
   signal — the button says the word "Clear", it carries the bin icon, and
   the confirm dialog states the count before anything happens
   (DESIGN-NOTES 14). */
.cb-root form.cb-form button.cb-btn.cb-btn-danger {
  background: var(--surface); color: var(--danger);
  box-shadow: 0 0 0 1px var(--danger-line); }
.cb-root form.cb-form button.cb-btn.cb-btn-danger:hover {
  background: var(--danger-bg); box-shadow: 0 0 0 1px var(--danger); }

/* The block head is `justify-content: space-between` with two children; the
   tools have to be one child or the heading and the chip drift apart when
   the Clear button appears and disappears. `flex-wrap` because at 390px the
   chip and the button do not fit on one line beside the heading. */
.cb-block-tools { display: flex; flex-wrap: wrap; align-items: center;
                  gap: 8px; }

/* The icons inside them draw in currentColor, so each button must set one --
   DESIGN-NOTES 13. Every rule above does. */
.cb-root form.cb-form button.cb-btn .ico { width: 1em; height: 1em; }

/* The Claim box. `input[type="text"]` is 0-1-1; this is 0-3-1, so it wins
   without escalating, and the WIDTH is desktop-only (13f). */
.cb-root form.cb-claim input.cb-room { font-size: var(--fs-sm); }

@media (min-width: 761px) {
  /* Desktop column widths only. Below 761px these rows become cards and
     `width` on a cell fights the card treatment — that is exactly what broke
     the archived-documents row on 09/08/2026. */
  .cb-button-table th:nth-child(1), .cb-button-table td:nth-child(1) { width: 15%; }
  .cb-button-table th:nth-child(2), .cb-button-table td:nth-child(2) { width: 10%; }
  .cb-button-table th:nth-child(3), .cb-button-table td:nth-child(3) { width: 8%; }
  .cb-button-table th:nth-child(4), .cb-button-table td:nth-child(4) { width: 20%; }
  .cb-button-table th:nth-child(5), .cb-button-table td:nth-child(5) { width: 10%; }
  /* The battery cell now holds a badge, a sparkline, a figure and a sentence,
     so it is the widest of the six. Re-derived, not nudged: the six widths
     below still total 84% and leave the Actions column its 16%. */
  .cb-button-table th:nth-child(6), .cb-button-table td:nth-child(6) { width: 22%; }
  .cb-unknown-table th:nth-child(1), .cb-unknown-table td:nth-child(1) { width: 20%; }
  .cb-unknown-table th:nth-child(2), .cb-unknown-table td:nth-child(2) { width: 16%; }
  .cb-unknown-table th:nth-child(3), .cb-unknown-table td:nth-child(3) { width: 16%; }
  .cb-log-table th:nth-child(1), .cb-log-table td:nth-child(1) { width: 26%; }
  .cb-log-table th:nth-child(2), .cb-log-table td:nth-child(2) { width: 20%; }
  .cb-log-table th:nth-child(4), .cb-log-table td:nth-child(4) { width: 18%; }
  .cb-root form.cb-claim input.cb-room { width: 120px; }
}

@media (max-width: 760px) {
  /* DESIGN-NOTES 9 — a real target on a phone. Restated at 0-3-2 because the
     block above now outranks the sitewide 44px floor. */
  .cb-root form.cb-form button.cb-btn { min-height: 44px; padding: 8px 14px; }
  .cb-root form.cb-claim input.cb-room { width: 100%; font-size: 16px; }
  .cb-claim { width: 100%; }
  /* `flex: 1 1 320px` on the text sets a 320px flex-basis, and in a COLUMN
     the basis is the HEIGHT — which left a 320px hole between the sentence
     and the button on a phone. Reset it in the direction it is wrong in. */
  .cb-listen { flex-direction: column; align-items: flex-start; }
  .cb-listen-text { flex: 0 0 auto; }

  /* THE CARD TREATMENT MAKES EVERY CELL A FLEX ROW, and a cell here holds
     four things — a badge, a sparkline, a figure and a sentence. With the
     label taking 40% and no wrapping, the sentence was squeezed into a 50px
     column and hung 74px off the RIGHT edge of the screen. Found by measuring,
     not by looking: DESIGN-NOTES 13f says measure both directions, and this is
     the direction that check exists for.

     So the value side wraps, and anything that is a line of its own gets a
     line of its own. */
  .cb-table td { flex-wrap: wrap; }
  .cb-table td > .cb-sub,
  .cb-table td > .cb-spark { flex: 1 1 100%; min-width: 0; }
  .cb-table td > .cb-state { flex: 0 0 auto; }
}

/* ==========================================================================
   END BACKLOG 105.
   ========================================================================== */

/* --- BACKLOG 105, the battery column. Added 09/08/2026, replacing a battery
       AGE column that lived for about an hour before `callbell-backend-spec-v2.md`
       was read and Scott chose it: v2 rejects calendar-based replacement
       "not as a primary approach, not as a fallback, not anywhere", and makes
       per-button health a hard requirement measured as encoder timing drift.

       FOUR STATES, AND GREY IS THE REAL ONE TODAY. Grey means "no
       measurement", and every button is grey until the firmware can report
       pulse timing and the calibration run produces thresholds. It must NOT
       look like a disabled or broken control — it is the correct answer, so
       it reads as a normal neutral badge, not a faded one. Each badge carries
       its word (No readings / No baseline / No thresholds / Good / Watch /
       Replace), so colour is never the only signal (DESIGN-NOTES 14). --- */
.cb-b-grey  { background: var(--neutral-bg); color: var(--neutral);
              box-shadow: 0 0 0 1px var(--neutral-line); }
.cb-b-ok    { background: var(--ok-bg); color: var(--ok);
              box-shadow: 0 0 0 1px var(--ok-line); }
.cb-b-amber { background: var(--warn-bg); color: var(--warn-text);
              box-shadow: 0 0 0 1px var(--warn-border); }
.cb-b-red   { background: var(--danger-bg); color: var(--danger);
              box-shadow: 0 0 0 1px var(--danger-line); }

/* The sparkline: median pulse width per call, baseline as a reference line
   (v2 §5.2). It renders only when there are two or more readings — a
   one-point line is a dot dressed up as a trend, and this page's whole
   problem is numbers looking more certain than they are. `.ico` sizing does
   not apply: this is not an icon, it is a chart, and DESIGN-NOTES 6 wants
   every inline SVG to carry its own size. It has width/height attributes. */
.cb-spark { display: block; margin: 3px 0 1px; overflow: visible; }
.cb-spark path { fill: none; stroke: var(--teal-600); stroke-width: 1.5;
                 stroke-linejoin: round; stroke-linecap: round; }
.cb-spark-base { stroke: var(--slate-300); stroke-width: 1;
                 stroke-dasharray: 3 3; }
.cb-batt-cell .cb-sub { margin-top: 1px; }


/* ==========================================================================
   BACKLOG 96 — PROJECTIONS: TIGHTEN THE TWO SUMMARY TABLES  (`2026.07.22.58`)

   Scott, 08/08/2026: "for the occupancy scenarios and annual figures tables on
   that page, can you make less space in between each column, there is a lot of
   space between number of residents and gross/month for example, all those
   columns on both tables need to be tightened up a lot."

   MEASURED FIRST, on the untouched `.57` build, at 1920 / 1440 / 1100 / 900 /
   800 / 780 / 761 / 390 — tests/proj_spacing_measure.py. The number that
   matters is not the cell padding; it is the white space between the last
   glyph of one column and the first glyph of the next, measured with a Range
   over each cell's real ink:

                            Occupancy Scenarios      Annual Figures
     1920   before / after     157   ->  51.4        173.6  ->  29.8
     1440   before / after    92.8   ->  51.4         71.2  ->  29.8

   WHAT WAS ACTUALLY WRONG. `table.data-table { width: 100% }` (style.css:202)
   makes the table fill the panel and auto layout hands the surplus out evenly
   across all seven columns. At 1440 that was 214px of pure slack on Occupancy
   Scenarios, and it lands BETWEEN the columns, which is exactly the thing
   Scott can see. Trimming cell padding alone would have won 84px of the 300
   and left the mechanism in place — padding was never the main term.

   SO: every column except the last shrinks to its own content (`width: 1px`
   with `white-space: nowrap` is the standard shrink-to-fit idiom in an auto
   layout table), and the last column absorbs the leftover. The table still
   spans the panel, so the zebra banding and the header rule still read as one
   block — a `width: auto` table stopped its stripes 500px short of the panel
   edge and looked unfinished.

   DESKTOP ONLY, matching Round 5a above and DESIGN-NOTES: below 761px
   style.css:1095-1126 turns `table.data-table` into one card per row, there
   are no columns to space, and `mobile-tables.js` is driving `td[data-label]`.
   Measured at 390px before and after: identical.

   ⚠️ TWO THINGS THIS ROUND FOUND THE HARD WAY, both by measuring rather than
   by reasoning:

   1. `.annual-cell-wrap` is `flex-wrap: wrap` (style.css:2422). Squeezing the
      Annual Figures columns to min-content dropped every `$733/mo` chip onto a
      second line and made those rows TALLER — a "tightening" that added
      height. `flex-wrap: nowrap` keeps the value and its chip on one line,
      and as a side effect raises the column's min-content so it cannot be
      squeezed that far in the first place.

   2. Nowrap cells make the table wider than the panel below ~1100px, which
      pushed the whole PAGE into horizontal scroll — a real regression at
      780px, caught only because the check measures document scrollWidth at
      eight widths rather than at 1440 alone. `.proj-tight-wrap` contains it,
      the same shape as `.proj-table-wrap` on this page's house grids.

   SPECIFICITY. `table.data-table.proj-tight td` is 0-2-2; the rule it has to
   beat is `table.data-table td` at 0-1-2 (style-v2.css Round 5a and
   style.css:4785), so this wins on specificity and not on source order.
   ========================================================================== */
/* ⚠️ The scroll frame is INSIDE the desktop media query, and that is not
   tidiness. `overflow-x: auto` makes the wrapper a block formatting context,
   which stops the table's own `margin-top: 0.25rem` (style.css:202) collapsing
   through it. Left unscoped it moved the phone layout by 11px — measured, by
   diffing the full-page 390px screenshot before and after, which is the only
   reason it was noticed. Below 761px these tables are one card per row and can
   never overflow, so the frame has nothing to do there. */
@media (min-width: 761px) {
  .proj-tight-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }

  table.data-table.proj-tight th,
  table.data-table.proj-tight td {
    padding-left: 6px; padding-right: 6px;
    white-space: nowrap;
  }

  /* The shrink. Not the last column: it takes the leftover width, which is
     what keeps the table spanning the panel. */
  table.data-table.proj-tight th:not(:last-child),
  table.data-table.proj-tight td:not(:last-child) { width: 1px; }

  /* See note 1 above. Without this the /mo chips wrap and the rows grow. */
  table.data-table.proj-tight .annual-cell-wrap { flex-wrap: nowrap; }
}

/* ==========================================================================
   BACKLOG 111 — Dashboard (A): Important Links + Termination Verbiage.
   Written 12/08/2026, same round as the markup (DESIGN-NOTES 13c/13e/13f).

   ⚠️ THE COPY BUTTON IS `<button type="button">` AND SITS OUTSIDE ANY <form>,
   so `form button[type="submit"]:not(.ghost)` (0-2-2) — the rule that has
   beaten a component rule seven times — cannot match it. Checked, not
   assumed: there is no <form> anywhere on /admin-dashboard. It is also
   `.ghost`, which the rule excludes anyway. Belt and braces, and it stays
   `type="button"` if a form is ever added to this page.

   ⚠️ THE FAVICON FALLBACK IS THIS FILE'S JOB, not the JavaScript's. The <img>
   carries `onerror="this.remove()"` and nothing else; the sibling rule below
   hides the app icon only WHILE an <img> is present. Remove the rule and every
   tile shows two pictures stacked; remove the onerror and a dead favicon shows
   a broken-image box. Both halves are asserted.
   ========================================================================== */

/* ---- the two folds ---- */
.ad-fold { background: var(--surface); box-shadow: var(--sh-key);
           border-radius: var(--r-lg); margin-bottom: 18px; overflow: hidden; }
.ad-fold > summary { cursor: pointer; list-style: none; display: flex;
                     align-items: center; gap: 10px; padding: 14px 16px; }
.ad-fold > summary::-webkit-details-marker { display: none; }
.ad-fold > summary:hover { background: var(--surface-hover); }
.ad-fold[open] > summary { box-shadow: inset 0 -1px 0 var(--line); }
.ad-fold > summary:focus-visible { outline: 2px solid var(--teal-400);
                                   outline-offset: -2px; }
.ad-fold-title { font-weight: var(--fw-semi); display: inline-flex;
                 align-items: center; gap: 8px; }
.ad-fold-title .ico { width: 17px; height: 17px; color: var(--text-subtle); }
.ad-chev { width: 9px; height: 9px; flex: 0 0 auto; margin-left: auto;
           margin-right: 4px;
           border-right: 2px solid var(--slate-400);
           border-bottom: 2px solid var(--slate-400);
           transform: rotate(45deg) translate(-2px, -2px);
           transition: transform var(--t-fast) ease; }
.ad-fold[open] .ad-chev { transform: rotate(-135deg) translate(-2px, -2px); }
.ad-fold-body { padding: 16px; }

/* ---- link groups ---- */
.ad-fold-body .alink-group + .alink-group { margin-top: 22px; }
.alink-group-title { font-size: var(--fs-sm); font-weight: var(--fw-semi);
                     text-transform: uppercase; letter-spacing: 0.04em;
                     color: var(--text-subtle); margin: 0 0 10px; }
.alink-grid { display: grid; gap: 10px;
              grid-template-columns: repeat(auto-fill, minmax(232px, 1fr)); }

/* ---- one tile. Same 40px icon box as the Add a Resident action card. ---- */
a.alink-card { display: flex; align-items: center; gap: 12px;
               padding: 10px 12px; border-radius: var(--r-lg);
               background: var(--surface); box-shadow: var(--sh-key);
               color: var(--text); text-decoration: none;
               transition: transform 0.13s ease, box-shadow 0.13s ease; }
a.alink-card:hover { transform: translateY(-1px);
                     box-shadow: var(--sh-key), var(--sh-2); }
a.alink-card:focus-visible { outline: 2px solid var(--teal-400);
                             outline-offset: 2px; }
.alink-icon { width: 40px; height: 40px; flex: 0 0 auto;
              border-radius: var(--r-md); background: var(--slate-100);
              display: inline-flex; align-items: center; justify-content: center;
              overflow: hidden; }
.alink-icon img { width: 24px; height: 24px; object-fit: contain;
                  display: block; }
.alink-icon .ico { width: 20px; height: 20px; color: var(--text-subtle); }
/* See the header: the app icon shows ONLY when the <img> has removed itself. */
.alink-icon img ~ .ico { display: none; }
.alink-label { font-size: var(--fs-sm); font-weight: var(--fw-medium);
               line-height: 1.3; }

/* ==========================================================================
   BACKLOG 113 — editing the links. OWNER ONLY, so none of this is ever drawn
   for an administrator or a house manager.

   ⚠️ THE 0-2-2 RULE, ASSUMED RATHER THAN OBSERVED — DESIGN-NOTES 13c.
   `form button[type="submit"]:not(.ghost)` has now beaten a component rule
   SEVEN times, and this round adds SIX new `<button type="submit">` controls
   inside forms in one go (delete a link, save a name, section up, section
   down, delete a section, and the two add buttons). Every one of them would
   start life as a solid teal gradient pill. The scoped rule below is written
   at (0,3,2) — `.ad-fold` + `[type="submit"]` + `.alink-tool` = 3 classes,
   `form` + `button` = 2 elements — which BEATS 0-2-2 outright rather than
   tying with it and depending on source order.
   ⚠️ It is written in the SAME round as the markup, and the browser check
   strips this block out in flight and requires the buttons to come back
   looking like the primary ones, so the proof is itself proven able to fail.
   That is the shape 13c asks for.
   ========================================================================== */

.alink-group-head { display: flex; align-items: center; gap: 8px;
                    margin-bottom: 10px; }
.alink-group-head .alink-group-title { margin: 0; }
.alink-rename { display: flex; align-items: center; gap: 4px; margin: 0; }
/* ⚠️ 30ch, not 22ch. The section title is UPPERCASE with 0.04em tracking, so
   "Staffing & scheduling" is 21 characters needing far more than 21ch of box --
   at 22ch it rendered as "STAFFING & SCHEDUI". Caught by LOOKING at the page:
   a clipped input is still a valid input and every geometry check was green.
   `ch` is measured on the element's own font, which is exactly what makes the
   uppercase transform invisible to it. */
input.alink-title-input { border: 0; background: transparent; padding: 2px 6px;
                          border-radius: var(--r-sm); width: 30ch;
                          max-width: 100%; text-overflow: ellipsis;
                          box-shadow: none; }
input.alink-title-input:hover { background: var(--slate-100); }
input.alink-title-input:focus { background: var(--surface);
                                box-shadow: var(--sh-key);
                                outline: 2px solid var(--teal-400);
                                outline-offset: -2px; }
.alink-group-tools { display: flex; align-items: center; gap: 4px;
                     margin-left: auto; }
.alink-group-tools form, .alink-secdel { display: flex; align-items: center;
                                         gap: 4px; margin: 0; }
.alink-secdel select { font-size: var(--fs-xs); padding: 2px 6px; }
.alink-secdel-label { font-size: var(--fs-xs); color: var(--text-muted);
                      text-transform: none; letter-spacing: 0; }

/* The scoped rule 13c asks for. (0,3,2) — see the header. */
.ad-fold form button[type="submit"].alink-tool,
.ad-fold form button[type="submit"].alink-tool:hover {
  background: transparent; background-image: none; border: 0;
  box-shadow: none; color: var(--text-subtle);
  padding: 4px; min-width: 0; min-height: 0; border-radius: var(--r-sm);
  display: inline-flex; align-items: center; justify-content: center;
  font: inherit; cursor: pointer; }
.ad-fold form button[type="submit"].alink-tool:hover {
  background: var(--slate-100); color: var(--text); }
.ad-fold form button[type="submit"].alink-tool:focus-visible {
  outline: 2px solid var(--teal-400); outline-offset: 1px; }
.ad-fold form button[type="submit"].alink-tool-del:hover {
  background: var(--red-50, #fee2e2); color: var(--red-600, #dc2626); }
.alink-tool .ico { width: 15px; height: 15px; }
.alink-tool-off { padding: 4px; opacity: 0.35; display: inline-flex;
                  align-items: center; }
.alink-tool-off .ico { width: 15px; height: 15px; }

/* ---- one tile is a wrapper: the anchor, and the delete button beside it ----
   ⚠️ The button is a SIBLING of the <a>, never a child. A <button> inside an
   <a> is invalid HTML and browsers reparent it out of the anchor, which puts
   the delete control somewhere the CSS is not expecting and makes clicking the
   tile follow the link instead. */
.alink-tile { position: relative; display: block; }
.alink-tile .alink-del { position: absolute; top: 6px; right: 6px; margin: 0;
                         opacity: 0; transition: opacity var(--t-fast) ease; }
.alink-tile:hover .alink-del,
.alink-tile:focus-within .alink-del { opacity: 1; }
.alink-tile[draggable="true"] { cursor: grab; }
.alink-tile.is-dragging { opacity: 0.4; cursor: grabbing; }
.alink-grid.is-over { outline: 2px dashed var(--teal-400); outline-offset: 6px;
                      border-radius: var(--r-lg); }
.alink-empty { grid-column: 1 / -1; font-size: var(--fs-sm); margin: 0;
               padding: 14px 12px; border-radius: var(--r-lg);
               border: 1px dashed var(--line); }

/* ---- the add forms at the bottom of the fold ---- */
.alink-editor { margin-top: 22px; padding-top: 16px;
                border-top: 1px solid var(--line); }
.alink-add { display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
             margin: 0 0 10px; }
.alink-add-title { font-size: var(--fs-sm); font-weight: var(--fw-semi);
                   color: var(--text-subtle); display: inline-flex;
                   align-items: center; gap: 6px; }
.alink-add-title .ico { width: 15px; height: 15px; }
.alink-add input[type="text"] { width: 20ch; }
.alink-add input[type="url"] { width: 30ch; }
.alink-hint { font-size: var(--fs-xs); margin: 4px 0 0; }

/* ⚠️ DESIGN-NOTES 13f — fixing the desktop is half a fix. Below 761px the add
   row must stack instead of squeezing four controls onto one line, and the
   delete button must be permanently visible: there is no hover on a phone, so
   an opacity-0-until-hover control is an invisible control. */
@media (max-width: 760px) {
  .alink-add input[type="text"], .alink-add input[type="url"] { width: 100%; }
  .alink-tile .alink-del { opacity: 1; }
  .alink-group-head { flex-wrap: wrap; }
  .alink-group-tools { margin-left: 0; }
}

/* ---- the termination wording ---- */
.ad-verbiage-hint { font-size: var(--fs-sm); color: var(--text-muted);
                    margin: 0 0 10px; max-width: 78ch; }
textarea.ad-verbiage { width: 100%; max-width: 82ch; display: block;
                       font: inherit; font-size: var(--fs-sm);
                       line-height: 1.55; padding: 12px 14px;
                       border: 0; border-radius: var(--r-md);
                       background: var(--slate-50);
                       box-shadow: var(--sh-key); color: var(--text);
                       resize: vertical; }
textarea.ad-verbiage:focus-visible { outline: 2px solid var(--teal-400);
                                     outline-offset: 1px; }
.ad-verbiage-actions { display: flex; align-items: center; gap: 12px;
                       margin-top: 10px; }
.ad-copy-said { font-size: var(--fs-sm); color: var(--ok);
                font-weight: var(--fw-semi); }

@media (max-width: 760px) {
  .alink-grid { grid-template-columns: 1fr; }
  .ad-fold-body { padding: 12px; }
}

/* ==========================================================================
   BACKLOG 121 — the "Not saved" state on a live inline edit.
                 (Scott, 13/08/2026. Pilot: the resident profile only.)

   Sits beside .saved-chip above and is the other half of the same story: the
   green chip says the server took it, this says the server did NOT and the
   value on screen is not what is stored. It is deliberately IN the layout —
   .saved-chip is position:fixed and fades, this is a block that pushes the
   row down and STAYS. Nothing removes it on a timer; live-edit.js clears it
   only when a later save of the same form actually succeeds.

   The Retry control is type="button", not a submit, so the standing 0-2-2
   rule (`form button[type="submit"]:not(.ghost)`) does not reach it and it
   does not arrive as a solid teal primary button. The scoped rule is written
   here in the same round as the control, per the standing rule.

   16px on the Retry button, not because iOS zooms on buttons (it does not —
   only on focusable text inputs, DESIGN-NOTES 9) but because a failure
   message that has to be squinted at on a phone is not loud.
   ========================================================================== */
.live-error {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex-wrap: wrap;
  margin: var(--sp-2) 0 0;
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--r-sm);
  background: var(--danger-bg);
  color: var(--danger);
  box-shadow: 0 0 0 1px var(--red-200);
  font-size: var(--fs-sm);
  line-height: var(--lh-sm);
  font-weight: var(--fw-semi);
}
.live-error-text { flex: 1 1 auto; min-width: 0; }

.live-error .live-error-retry,
form.live-edit-failed + .live-error .live-error-retry {
  flex: 0 0 auto;
  padding: 6px 14px;
  border: 1px solid var(--danger);
  border-radius: var(--r-xs);
  background: #fff;
  color: var(--danger);
  font-size: 16px;
  line-height: 20px;
  font-weight: var(--fw-semi);
  cursor: pointer;
}
.live-error .live-error-retry:hover { background: var(--danger); color: #fff; }

/* The field itself is ringed as well, so the box and the control it belongs
   to read as one thing when several fields are stacked on a phone. */
form.live-edit-failed input,
form.live-edit-failed select,
form.live-edit-failed textarea {
  border-color: var(--danger);
  box-shadow: 0 0 0 2px var(--danger-bg);
}


/* ---- BACKLOG 124, 2026.07.22.67: a real space after the Sync icon ----
   Scott: "the circle with the arrow in it icon needs a space after it so there
   is a space between that icon and the word sync."

   ⚠️ THE SPACE IN THE MARKUP PAINTS ZERO PIXELS AND ALWAYS HAS. `.button` is
   `display: inline-flex` (style-v2.css:446, added so `min-height` could reach
   `a.button`), `.ico` computes to `display: block`, and a flex container
   STRIPS the leading whitespace of an anonymous text run. Measured in Chromium
   on the deployed 2026.07.22.66: the icon-to-glyph distance was 0.0px on BOTH
   Sync buttons — including the Call-Offs one, whose source has carried a
   literal space since the day it was written. Reading the source would have
   said that button was already correct. It was not.

   Same cause and same shape as `.docs-arc .ico` above, which found this first.
   ⚠️ THIS RULE IS WHAT PAINTS THE GAP. The space in app.py is kept because it
   is the honest markup and it is what a non-flex fallback would use, but
   deleting this rule takes the gap to 0 while every source-level check still
   passes. `wording_batch_browser_check.py` §2 is the one that would notice. */
.sync-btn .ico { margin-right: 4px; }

/* ==========================================================================
   2026.07.22.68 — BACKLOG 126, 127, 132, 133, 134, 135. The layout batch.
   Written 14/08/2026, same round as the markup.

   ⚠️ EVERY NUMBER BELOW WAS MEASURED IN A REAL BROWSER AT 1440px AND 390px,
   ON THIS BUILD AND ON THE DEPLOYED .67, by tests/layout_batch_measure.py.
   .67 (trap 64) is why: a character in the markup and a gap on the screen are
   different things, and so are a width in a comment and a column that fits.
   ========================================================================== */

/* --- BACKLOG 133: the four Projections hero values, evenly spread ---------
   ⚠️ THE ITEM SAID "SHIFTED LEFT". IT WAS NOT AN ALIGNMENT BUG, IT WAS A WRAP.
   `.proj-hero` is `display: flex` with `flex-wrap: wrap` (style.css:2510 and
   :2558) holding ELEVEN loose children — four icons, four stats and three
   dividers. MEASURED on the deployed .67 at a 1440px browser, where the
   Projections content column is 1086px wide: the fourth stat sat on a SECOND
   ROW with 863.5px of empty space to its right, and the gap between the third
   and fourth labels read -783.5px. Nothing was aligned left; the row had run
   out of width and wrapped.

   `justify-content: space-between` would NOT have fixed that — a wrapped row
   still wraps, and it would have spread the dividers rather than the stats.
   The fix is to make the four stats one flex item each, sized `1 1 0` so they
   share the width equally. app.py wraps each icon WITH its stat in
   `.proj-hero-cell` for exactly this reason.

   ⚠️ THERE IS NO `min-width: 0` HERE, AND THAT IS A CORRECTION. The first cut
   carried one with a comment calling it load-bearing — "without it a flex item
   refuses to shrink below its content and the wrap comes straight back". The
   mutation harness removed it and NOTHING FAILED, so it was swept out
   deliberately: every 20px from 1920 to 400, cell overflow, hero overflow and
   document overflow all read 0 without it. It is not needed because the 2×2
   band below already takes over at every width where a cell would be squeezed
   under its own content. The declaration is gone rather than left in with a
   claim nobody had checked. */
.proj-hero-cell {
  display: flex;
  align-items: center;
  gap: 0.8rem;
  flex: 1 1 0;
}
.proj-hero-divider { flex: 0 0 1px; }
/* The icon is the one thing in the cell that must not shrink — it is a fixed
   54px square and squashing it makes the glyph inside it oval. */
.proj-hero-cell .proj-hero-icon { flex: 0 0 auto; }
/* Below about 1500px of browser the four cells are narrow enough that the two
   long money figures need one step down from --fs-3xl (30px) to --fs-2xl
   (24px). MEASURED across eight widths, not guessed:
     1440px browser -> 1086px hero -> 234.8px cell. "$123,703/mo" paints
     147.0px at 30px and 123.3px at 24px; the value BOX is 168px.
   ⚠️ THE LOWER BOUND IS 721px AND NOT 861px, AND THAT IS THE WHOLE POINT OF
   SWEEPING WIDTHS. A first cut stopped at 861px, which left the band 721–860
   at 30px with four cells still in a ROW — style.css:2567 does not stack the
   hero into a column until 720px. MEASURED at 760px: the value box was 150.2px
   inside a 134.4px cell, i.e. the figure painted outside its own cell, and the
   document scrollWidth said 0 the whole time. A page-level overflow check
   would have called that clean. */
@media (min-width: 721px) and (max-width: 1500px) {
  .proj-hero .proj-hero-value,
  .proj-hero .proj-hero-stat-value { font-size: var(--fs-2xl); }
}
/* ⚠️ FOUR ACROSS DOES NOT FIT AT EVERY WIDTH, AND FORCING IT MADE A CELL PAINT
   OVER ITS NEIGHBOUR. `flex: 1 1 0` shrinks a cell only as far as its
   MIN-CONTENT — the 54px icon, its 0.8rem gap and the longest unbreakable run
   of the figure — MEASURED at 193px. Below that the value overflows the cell
   silently: `overflow` is visible, so the number simply prints across the next
   cell's icon and NOTHING AT PAGE LEVEL COMPLAINS.

   ⚠️ AND THE DOCUMENT-OVERFLOW CHECK IS NOT THE ONE THAT CATCHES IT. Swept
   every 40px from 1920 down to 400, comparing each cell's scrollWidth against
   its clientWidth:

     >= 1280px   cell 197px and up   fits
     920–1240px  cell 140–187px      OVERFLOWS by up to 53px
     <= 900px    two by two          fits

   The document scrollWidth was 0 through most of that band and reported only
   2px at 800 and 7px at 780 — a page-level check would have passed it.

   ⚠️ THE BAND IS NOT MONOTONIC IN VIEWPORT WIDTH. The sidebar collapses around
   1010px, which HANDS the hero 150px back: the hero's inner width goes 944px
   at a 1240px browser, down to 754px at 1040px, then back UP to 948px at
   1000px. Reading one width, or reading the ends, would have found a clean
   number either side of a broken middle.

   The cutover is 1320px and not 1280px on purpose: at 1280 the cell is 197px
   against a 193px need — four pixels of margin, which any change to the figure
   or the font would eat. At 1320 it is 206px. */
@media (min-width: 721px) and (max-width: 1319px) {
  .proj-hero-cell { flex: 1 1 44%; }
  .proj-hero-divider { display: none; }
}
/* Below 720px style.css:2567 already stacks the hero into a column and hides
   the dividers. A column of four `flex: 1 1 0` items would share the HEIGHT
   equally, which is not what "evenly spread across the top" means, so the
   basis is released there. */
@media (max-width: 720px) {
  .proj-hero-cell { flex: 0 0 auto; width: 100%; }
}

/* --- BACKLOG 134: Occupancy Scenarios — Scenario left, the rest centred ---
   ⚠️ TRAP 66, and it applies here directly: `table.data-table.proj-tight` is
   on BOTH tables on this page. The Annual Figures table below shares every
   class the scenarios table had, so this rule is written on `proj-scenarios`,
   which app.py puts on the scenarios table alone.

   SPECIFICITY. `table.data-table.proj-scenarios td` is 0-3-2 and beats
   `table.data-table.proj-tight td` (0-3-2 as well, but earlier in the file)
   and `table.data-table td` (0-2-2 / style.css:4785). The `:not(:first-child)`
   adds nothing to the count, so this wins on source order over the sibling
   proj-tight rule and on specificity over the base rule. Checked, not assumed:
   the base rules set no `text-align` at all today — both tables inherit the
   UA default `left`, MEASURED as `left` on all seven columns of .67.

   ⛔ REVERSED IN 2026.07.22.69. `.68` centred the HEADERS along with the data
   and said so; Scott looked at the deployed page on 14/08/2026 and answered
   that the headers go back to LEFT and the figures stay centred. That is his
   ruling on his own page, not a bug — do not re-raise the "a centred column
   under a left-hung header reads as a mistake" argument, it was put to him and
   he decided against it.

   ⚠️ EVERY `th` is now left, including the first — one selector rather than a
   :first-child pair, because there is no longer anything to distinguish. The
   `td` half is unchanged: first column left, the other six centred. */
table.data-table.proj-scenarios td:not(:first-child) { text-align: center; }
table.data-table.proj-scenarios th,
table.data-table.proj-scenarios td:first-child { text-align: left; }

/* --- BACKLOG 135: Total Expenses red, Profit a better green --------------
   The colour used to be an inline `style="color:var(--ok)"` on every Profit
   CELL, and Total Expenses had no colour at all. Both are rules now, so the
   two lines are one decision each.

   THE TWO REDS ARE DIFFERENT ON PURPOSE. A loss keeps `var(--danger)`
   (--red-700, #7f251b); Total Expenses is `var(--red-600)` (#a82a20), because
   a routine expenses line and a month that lost money must not read as the
   same alarm. Contrast measured against the row fills those rows actually
   carry (#f5f7f6 and #fdfaf2): expenses 6.47, loss 8.94, profit 6.45 — all
   above the 4.5 floor, and DESIGN-NOTES 14 (colour here is functional) is why
   they are separated at all rather than both being "red".

   ⚠️ `proj_row_colour_test.py` asserts BACKGROUNDS, not text: it says in its
   own docstring that Total Expenses and Profit are "near-white on purpose …
   marked out by their 2px navy borders, not by their fill". Nothing here
   touches a background. That suite was already RED on an untouched tree — its
   failure text was diffed against the deployed build, not counted. */
.proj-table tr.proj-total td { color: var(--red-600); }
.proj-table tr.proj-total td:first-child strong { color: var(--red-600); }
.proj-table tr.proj-profit td { color: var(--green-600); font-weight: 800; }
.proj-table tr.proj-profit td:first-child strong { color: var(--green-600); }
/* A loss. app.py puts `proj-neg` on the cell because a stylesheet cannot see
   the sign of a number. */
.proj-table tr.proj-profit td.proj-neg { color: var(--danger); }
/* style.css:2578 sets `tr.proj-driven td { color: var(--text) !important }`
   for the annual-driven rows. That rule names `.proj-driven` only, so it
   cannot reach either of these two — checked in the sheet, and the painted
   colour is asserted in the browser check rather than inferred from it. */

/* --- BACKLOG 132: Current Waitlist column widths -------------------------
   Scott: selecting "Seminole House I" drops the trailing "I" onto a second
   line, and Phone and Family contact do the same.

   MEASURED on the deployed .67 at 1440px: the table is 1104.5px inside a
   1086px column and FIVE columns wrapped — Prospective resident 3 lines,
   Family contact 3, Phone 3, Preferred house 2, Note 4. A phone number on
   three lines is worse than the item describes.

   `.62`'s pattern, and the reasons are `.62`'s reasons:
     · the floors are MEASURED, not written down;
     · they live inside `@media (min-width: 761px)` — below that these tables
       are one card per row and a floor there pushes the whole PAGE sideways
       into `overflow-x: hidden` and gets clipped;
     · the scroll frame is what makes a floor safe at all. A table pinned to
       `width: 100%` can never be wider than the thing that would scroll it,
       which is why `.house-panel { overflow-x: auto }` had never fired before
       `.62` gave it a `min-width`.

   ⚠️ TWO OF THE THREE COLUMNS CAN BE GUARANTEED AND ONE CANNOT.
   Preferred house is a FINITE SET — WAITLIST_HOUSE_CHOICES, longest string
   "Seminole House I" — so `nowrap` plus a floor is a promise that holds for
   every possible value. Phone is a formatted 10-digit number, also bounded.
   FAMILY CONTACT IS FREE TEXT AND IS NOT BOUNDED: the floor below fits an
   ordinary two-word name, and a genuinely long double-barrelled one still
   wraps. That is said out loud here because the browser check asserts exactly
   that and no more. */
@media (min-width: 761px) {
  .wl-list-panel .wl-table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
  /* No `min-width` on the table itself. The column floors below already take
     it to a MEASURED 1375px, and a second number written here would be a
     number nobody checks — `.62`'s house tables needed one because they had no
     per-column floors to add up. */
  /* ⚠️ EVERY FLOOR HERE IS THE MEASURED PAINTED TEXT WIDTH PLUS THE CELL'S OWN
     24px OF PADDING (12px each side, measured, not read off a rule) PLUS A
     LITTLE SLACK. `.62`'s lesson: a column width that is written down is not a
     column that fits. A first cut of this block used 124px for Preferred house
     and PASSED — because the table happened to hand that column 143.9px on
     this fixture. The floor was 10px under what the text needs and nothing
     said so. */
  /* Preferred house: "Seminole House I", the longest member of
     WAITLIST_HOUSE_CHOICES, paints 110.5px. 110.5 + 24 = 134.5. */
  .wl-list-panel table.wl-table th:nth-child(7),
  .wl-list-panel table.wl-table td:nth-child(7) { min-width: 136px; white-space: nowrap; }
  /* Phone: a formatted 10-digit number, "(985) 555-0142", paints 95.8px.
     95.8 + 24 = 119.8. */
  .wl-list-panel table.wl-table th:nth-child(3),
  .wl-list-panel table.wl-table td:nth-child(3) { min-width: 122px; white-space: nowrap; }
  /* Family contact: FREE TEXT, so this is a floor and not a promise.
     "Bartholomew Vandersteen" paints 160.1px and needs 184.1; 190px clears
     that. A longer double-barrelled name still wraps, and the browser check
     asserts only what this can actually deliver. */
  .wl-list-panel table.wl-table th:nth-child(2),
  .wl-list-panel table.wl-table td:nth-child(2) { min-width: 190px; }
  /* The resident's own name is the column the eye goes to first and it wrapped
     onto THREE lines beside the two Scott named. Same free-text caveat. */
  .wl-list-panel table.wl-table th:nth-child(1),
  .wl-list-panel table.wl-table td:nth-child(1) { min-width: 190px; }
  /* Email is the one column that was never short of room — it took 264.8px of
     a 1104.5px table while Phone was squeezed onto three lines. Capping it
     gives that width back to the columns Scott named instead of adding it all
     to the table's total.
     ⚠️ THE CAP NEEDS A FLOOR UNDER IT. A first cut of this rule was
     `max-width: 210px; overflow-wrap: anywhere` with no `min-width`, and the
     other columns' floors then squeezed this one to its min-content — MEASURED
     at TWENTY-SIX lines, one character per line. `anywhere` will break a word
     at any character, so there is no natural floor to stop it. `break-word`
     breaks only when a whole word cannot fit, and the min-width is what keeps
     an ordinary address on one or two lines. */
  .wl-list-panel table.wl-table th:nth-child(4),
  .wl-list-panel table.wl-table td:nth-child(4) {
    min-width: 200px; max-width: 230px; overflow-wrap: break-word;
  }
  /* First contact is a formatted date and never needs two lines. */
  .wl-list-panel table.wl-table th:nth-child(5),
  .wl-list-panel table.wl-table td:nth-child(5) { white-space: nowrap; }
}

/* --- BACKLOG 127: more vertical separation in the profile sidebar --------
   Scott: the fields "are too close to each other".

   MEASURED on the deployed .67 at 1440px, in the painted boxes: consecutive
   `.rp-vital` rows sat 0.0px apart, a label sat 7.2px above its first value,
   and a section label sat 13.6px below the block before it. The 0.0px is what
   he is looking at — `.rp-vital { padding: 0.28rem 0 }` (style.css:2226) is
   4.5px of padding each side and no margin at all, so two consecutive vitals
   share an edge.

   ⚠️ `.rp-side-col` IS ONE OF THE THREE REGIONS `.66`'s live-edit SWAPS IN
   from the server's response, so this spacing has to survive an inline save
   and not only a fresh load. The browser check saves a field and re-measures.

   ⚠️ ON A PHONE THE SIDEBAR IS BEHIND A FOLD (`.rp-side-rest { display:none }`
   until `#rp-side-toggle` is checked, style.css:5209). MEASURING AT 390px
   WITHOUT OPENING IT RETURNS 0.0px FOR EVERY GAP AND LOOKS LIKE A PASS ON
   THE OLD BUILD TOO. The measure script opens the fold first. */
.rp-side .rp-vital { padding: 0.45rem 0; }
.rp-side .rp-side-label { margin-top: 1.25rem; }
.rp-side .rp-side-label:first-child { margin-top: 0.1rem; }
/* The allergy callout sits OUTSIDE `.rp-side-rest`, between the chips and the
   Vitals label, and had no space under it at all. */
.rp-side .rp-vital-allergy { margin-bottom: 0.55rem; }


/* ==========================================================================
   BACKLOG 123 — the Waitlist stat bar.  MOCK-UP C, Scott's pick 14/08/2026
   2026.07.22.69
   ==========================================================================

   ⛔ 123 IS NOT AN ICON PROBLEM. His words were about the page's "icon
   situation", and the item was filed that way. MEASURED in a real Chromium at
   1440px on 14/08/2026: the Waitlist page has exactly TWO icon-and-label sites
   and BOTH paint 1.9px, identical to the same `.section-heading` pills on
   Repairs, Call-Offs and the Community Playbook. Nothing on that page is
   jammed. Do not re-derive this. Full record:
   `claude/ITEM-123-WAITLIST-MEASURED-2026-08-14.md`.

   THE THREE THINGS THAT WERE ACTUALLY WRONG, all measured, all fixed here:
     1. a gold rgb(236,223,176) border no other card on the site wore — the
        leftover of a cream card whose cream a later rule repainted white.
        Removed at source in style.css, not overridden from here.
     2. 975.1px of the card's 1136.0px was EMPTY. Every other stat card in the
        app measures 275–560px wide.
     3. the label was 12px sentence-case with `letter-spacing: normal`, where
        every other stat label on the site is uppercase and tracked, and the
        icon was a naked 22px glyph where Projections uses a rounded cream tile.

   MOCK-UP C = the card sized to its content, sitting on a tinted band like the
   Projections hero. The band is what stops a content-width card from looking
   marooned in the empty 975px.

   ⚠️ THE 390px PHONE IS THE WHOLE RISK HERE. `.wl-stat-row`'s
   `grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr))`
   (style.css:5252) is itself a DELIBERATE FIX — DESIGN-NOTES #1 — stopping a
   390px phone from stranding this single card in a 170px track with its label
   wrapped to "On the / Waitlist". Switching the row to `display: flex` retires
   that rule rather than deleting it: `grid-template-columns` does nothing on a
   flex container, so the declaration below is what makes it inert and the
   original stays readable at its own site. The card is `flex: 0 0 auto`, so it
   is content-width on every viewport and cannot be stretched OR stranded.
   ⚠️ MEASURE AT 390px AS WELL AS 1440px — the mock-ups were only ever shot at
   1440px. */
.wl-stat-row {
  display: flex;
  align-items: center;
  gap: 1.1rem;
  flex-wrap: wrap;
  row-gap: 0.7rem;
  /* Inert on a flex container. Named here so a reader who finds the auto-fit
     grid rule in style.css knows what happened to it. */
  grid-template-columns: none;
  background:
    radial-gradient(500px 160px at 90% -20%, rgba(243, 185, 41, 0.12), transparent 60%),
    linear-gradient(135deg, #eaf6f2, #dcefe8);
  border: 1px solid #bcdcd4;
  border-radius: var(--r-xl);
  padding: 1.15rem 1.5rem;
  margin-bottom: 1.5rem;
  box-shadow: var(--shadow);
}

/* The card itself stops being a card — the band is the surface now. Written on
   `.wl-stat-row .stat-card` (0,2,0) in style-v2.css, which layout() links
   AFTER style.css, so it beats both `.stat-row .stat-card` (0,2,0,
   style.css:4364) and the earlier `.wl-stat-row .stat-card` blocks on source
   order alone. No !important is needed anywhere in this block, because the one
   !important that was in play was deleted at its source. */
.wl-stat-row .stat-card {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 0.9rem;
  padding: 0;
  border: 0;
  background: none;
  box-shadow: none;
  text-align: left;
}
/* `.stat-row .stat-card:hover` lifts and shadows the Residents FILTER cards.
   This one is not a filter and not a link — it must not answer the mouse. */
.wl-stat-row .stat-card:hover { transform: none; box-shadow: none; }

/* The icon tile, matching `.proj-hero-icon` (style.css:2523) at the smaller
   44px this bar wants. The cream #fdf6e3 → #fbeec9 is the SAME pair the old
   `.wl-card-waiting` background used — the site moved that cream onto icon
   tiles, and this puts the Waitlist's last trace of it where the rest of the
   site keeps it. */
.wl-stat-row .stat-icon {
  width: 44px; height: 44px;
  border-radius: 14px;
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
  margin: 0;
  align-self: center;
  background: linear-gradient(140deg, #fdf6e3, #fbeec9);
  box-shadow: inset 0 0 0 1px rgba(217, 158, 20, 0.25), 0 2px 6px rgba(18, 41, 44, 0.08);
}
/* style.css:5013 gives this glyph `margin: 0 auto 0.15rem`, which off-centres
   it inside a 44px tile by 2.4px. Centred by the flexbox instead. */
.wl-stat-row .stat-icon .ico { width: 22px; height: 22px; margin: 0; display: block; }

/* The DOM restack's wrapper — app.py puts the label and the value inside it so
   the label reads ABOVE the value, the way `.proj-hero-label` sits above
   `.proj-hero-value`. */
.wl-stat-text { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }

/* Uppercase and tracked, like every other stat label on the site. 1.2px is the
   figure MEASURED off `.proj-hero-label` and the Call-Offs tiles, not a guess.
   `white-space: nowrap` is safe BECAUSE the card is content-width now: the
   label cannot be squeezed by a stretched track, which is what wrapped it to
   two lines on a phone in the first place. */
.wl-stat-row .stat-label {
  font-size: var(--fs-xs);
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 1.2px;
  color: #2b7880;
  margin: 0;
  white-space: nowrap;
}
.wl-stat-row .stat-num {
  font-size: var(--fs-3xl);
  font-weight: 800;
  line-height: 1.05;
  color: var(--navy-dark, #1d5c55);
}

/* The band keeps its padding on a phone but loses a little of it, the way
   `.proj-hero` does. The tile stays 44px — style.css:5045 shrinks the GLYPH to
   20px inside it below 760px, which still centres. */
@media (max-width: 480px) {
  .wl-stat-row { padding: 0.95rem 1.05rem; gap: 0.85rem; }
}


/* ==========================================================================
   BACKLOG 141 — Wage Reduction: the custom date range.  2026.07.22.69
   ==========================================================================
   "Year to date" needed no CSS at all — it is one more `.wr-pbtn` in the same
   row. This block is only the From/To form beside those chips.

   It is a GET <form>, not another chip, because a range is two values he TYPES
   and there is no href that can carry a value that does not exist yet. That is
   also why it cannot be inside `.wr-periods`' flex flow as three loose
   children — it is one item, so it gets its own inline-flex box and the row
   wraps around it as a unit.

   ⚠️ DESIGN-NOTES 8 and 9. On a phone the inputs must be --fs-base (16px, the
   iOS no-zoom floor) with a 44px target, exactly like `.wr-af input` beneath.
   The sibling `.wr-pbtn` rule at the top of the phone block is 40px, not 44 —
   these are form controls, so they take the taller one. */
.wr-custom-range {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  flex-wrap: wrap;
  margin-left: 0.35rem;
  padding-left: 0.7rem;
  border-left: 1px solid var(--border, #e3ebe8);
}
.wr-custom-range label { display: inline-flex; align-items: center; gap: 0.3rem; margin: 0; }
.wr-custom-range input[type="date"] {
  min-height: 32px;
  padding: 3px 8px;
  border-radius: var(--r-md);
}
.wr-custom-clear { font-size: var(--fs-sm); color: var(--text-muted); text-decoration: underline; }
.wr-custom-clear:hover { color: var(--text); }

/* Only rendered while a range is actually in force — see app.py. It exists
   because a custom range leaves EVERY period chip unpressed, and a page that
   is filtering itself with nothing on screen looking selected is the one state
   here that could be read as a bug. */
.wr-custom-note { margin: -0.35rem 0 var(--sp-3); }

@media (max-width: 760px) {
  .wr-custom-range {
    margin-left: 0;
    padding-left: 0;
    border-left: 0;
    width: 100%;
  }
  .wr-custom-range input[type="date"] { font-size: var(--fs-base); min-height: 44px; }
}

/* ---------------------------------------------------------------------------
   Round 2026.07.22.77 — overtime on the Wage Reduction page.
   --------------------------------------------------------------------------- */

/* ⛔ NO WIDTH HERE. The OT column's widths live with every other column's, up
   in the wage-reduction block, on the <th> and as percentages. The first cut
   put `width: 78px` on the <td> as well, which leaked into the 799px CARD
   layout where every cell is a block -- the OT value sat 78px from the left
   while Regular hours beside it aligned right. Caught by looking at a real
   390px render, not by reading the rule. */
.wr-table td.wr-k-ot { white-space: nowrap; }

/* "check OT" — a row entered before this column existed, over the threshold,
   with nothing in the OT box. Amber, never red: it is a prompt to look, not a
   fault. Same treatment as the two-phone-numbers marker on Hiring. */
.wr-otflag { display: inline-block; margin-left: 6px; padding: 1px 6px;
             border-radius: var(--r-xs); font-size: var(--fs-2xs);
             font-weight: var(--fw-medium); cursor: help;
             background: var(--gold-50); color: var(--gold-700);
             box-shadow: 0 0 0 1px var(--gold-150); }

/* The overtime card in the "how this is worked out" panel. */
.wr-pmtag.wr-pm-ot { background: var(--violet-50); color: var(--violet-700);
                     box-shadow: 0 0 0 1px var(--violet-150); }
.wr-way-eg { font-variant-numeric: tabular-nums; }

/* Round 2026.07.22.78 — Finalized, and the never-paid final check. */

/* The Paid/Not paid button and the Finalized tick stack in one cell. They are
   two different questions about the same check, so they belong together. */
.wr-finwrap { display: flex; flex-direction: column; align-items: flex-start;
              gap: 4px; }
.wr-finck { display: inline-flex; align-items: center; gap: 5px; cursor: pointer; }
.wr-finlbl { font-size: var(--fs-2xs); color: var(--text-subtle); }
.wr-finck:hover .wr-finlbl { color: var(--text); }

/* "never paid out — whole cost saved". Teal, not amber: this is a GOOD
   outcome being explained, not a thing to check. It exists because a row
   quietly counting its whole cost as a saving, with nothing on screen saying
   why, is a number nobody can audit a month later. */
.wr-neverpaid { display: block; margin-top: 3px; font-size: var(--fs-2xs);
                line-height: 1.25; color: var(--teal-700);
                font-weight: var(--fw-medium); }

.wr-pmtag.wr-pm-never { background: var(--teal-50); color: var(--teal-700);
                        box-shadow: 0 0 0 1px var(--teal-150); }

/* ---------------------------------------------------------------------------
   Round 2026.07.22.87 — BACKLOG 83, address autocomplete.

   ⛔ `.86` SHIPPED GOOGLE'S OWN `<gmp-place-autocomplete>` ELEMENT AND IT NEVER
   DREW A SUGGESTION LIST ON THIS SITE. It renders into a CLOSED shadow root, so
   there was nothing to inspect and nothing to style. This is our own list,
   drawn from the Places Autocomplete Data API, and every rule below is one we
   control.

   ⚠️ IT IS `position: fixed` AND PARENTED AT THE BODY, ON PURPOSE. The
   Previous Address field sits in a table cell inside a live-edit form that is
   `display: inline-flex`. A list in normal flow there is at the mercy of every
   ancestor's overflow and stacking context, which is the exact class of
   problem `.86` lost a round to. Nothing clips a fixed element at the body.
   --------------------------------------------------------------------------- */
.addr-ac-list {
  position: fixed;
  z-index: 9999;
  max-height: 260px;
  overflow-y: auto;
  background: var(--surface, #fff);
  border: 1px solid var(--line, #c9cfd6);
  border-radius: var(--r-sm, 7px);
  box-shadow: 0 8px 24px rgba(16, 34, 48, 0.14);
  padding: 3px;
}

.addr-ac-row {
  padding: 7px 10px;
  border-radius: var(--r-xs, 5px);
  font-size: var(--fs-xs);
  line-height: 1.35;
  color: var(--text);
  cursor: pointer;
  /* An address is one thing. Wrapping it mid-line reads as two results. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.addr-ac-row.is-on,
.addr-ac-row:hover { background: var(--teal-50, #f0fafb); }

/* Said out loud when Google does not answer — a key whose website restriction
   does not match, or a service not restarted since the key was added,
   otherwise leaves a field that simply never suggests anything. */
.addr-ac-note { margin: 0 0 5px; font-size: var(--fs-2xs); line-height: 1.3;
                color: var(--text-subtle); }

/* ---------------------------------------------------------------------------
   Round 2026.07.22.88 — BACKLOG 140, the LA SUI strip.

   ⛔ IT REPLACES A FULL-WIDTH AMBER FLASH AT THE TOP OF THE PAGE and a column
   of one card per year, one of which said "LA SUI 2027 — Not set" every day of
   the year. Scott asked for the reminder to sit NEAR the LA SUI section and to
   be "slick and elegant"; three renders were put in front of him 21/08/2026
   and this is the one he chose.

   ⚠️ THE RATES ROW ABOVE IT IS UNTOUCHED, and that is the whole reason this
   shape won: the two alternatives both widened a card and wrapped the row onto
   a second line at 1440px. Measured, not guessed.
   --------------------------------------------------------------------------- */
.wr-sui-strip {
  display: flex; align-items: center; flex-wrap: wrap; gap: 9px;
  margin-top: 11px; padding: 9px 4px 0;
  border-top: 1px solid var(--line-subtle);
  font-size: var(--fs-2xs); line-height: var(--lh-2xs); color: var(--text-subtle);
}
.wr-sui-strip-dot { width: 7px; height: 7px; border-radius: 50%; flex: none;
                    background: var(--teal-600); }
.wr-sui-strip-t { color: var(--text); }
.wr-sui-strip-t b { font-weight: var(--fw-bold); }
.wr-sui-strip-sep { flex: 1 1 12px; }
.wr-sui-strip-n { color: var(--text-subtle); }
.wr-sui-dash { color: var(--text-subtle); }
/* ⛔ THE DATE PAIR IS ONE THING. At 390px the loose children broke between
   "to" and the end date, orphaning the word and stranding the second box on a
   line of its own. Measured, then fixed. */
.wr-sui-win { display: inline-flex; align-items: center; gap: 7px; }

/* ⛔ `input.wr-sui-d` AND NOT `.wr-sui-d`. `.wr-rates input` is (0,1,1) and
   beats a bare class (0,1,0) — the exact specificity trap this stylesheet
   already documents around line 1201. Measured: the date boxes rendered at
   full rate-card size and the second one hung off the edge of the card. */
input.wr-sui-d {
  font: inherit; font-size: var(--fs-xs); font-weight: var(--fw-medium);
  width: 84px; padding: 3px 5px; text-align: center;
  border: 1px solid var(--line); border-radius: var(--r-xs);
  background: var(--surface); color: var(--text);
}
input.wr-sui-d:hover { border-color: var(--accent); }
input.wr-sui-d:focus { outline: 2px solid var(--teal-400); outline-offset: 2px; }

/* ⛔ AMBER ONLY WHEN THE WINDOW HAS ACTUALLY ENDED. DESIGN-NOTES 14: colour is
   functional. A rate that runs out in eleven months is not a thing to check. */
.wr-sui-strip.wr-sui-ran { color: var(--gold-700); }
.wr-sui-strip.wr-sui-ran .wr-sui-strip-dot { background: var(--gold-500, #d69e2e); }
.wr-sui-strip.wr-sui-ran .wr-sui-strip-t { color: var(--gold-700); }

/* The add-next-year disclosure. A <details>, so live-edit.js keeps it open
   across a swap without a line of page script. */
.wr-sui-add-wrap { flex: 0 0 auto; }
.wr-sui-add-wrap[open] { flex: 1 1 100%; }
summary.wr-sui-add {
  list-style: none; cursor: pointer; display: inline-block;
  font-size: var(--fs-2xs); font-weight: var(--fw-semi); color: var(--teal-700);
  text-decoration: underline; text-underline-offset: 2px; padding: 2px 0;
}
summary.wr-sui-add::-webkit-details-marker { display: none; }
summary.wr-sui-add:hover { color: var(--teal-800, #12403f); }
.wr-sui-add-body {
  display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
  margin-top: 9px; padding: 10px 12px; border-radius: var(--r-md);
  background: var(--teal-50); box-shadow: inset 0 0 0 1px var(--teal-150);
}
.wr-sui-add-body .wr-fin input { width: 88px; }

@media (max-width: 760px) {
  .wr-sui-strip { gap: 7px; }
  .wr-sui-strip-sep { flex-basis: 100%; }
}

/* ==========================================================================
   ROUND 2026.07.23.05 — THE GROCERIES LAYOUT                    (08/26/2026)

   THE FIRST GROCERY RULE IN THIS FILE, AND THE END OF A FIVE-ROUND STREAK.
   Rounds .00 through .04 shipped the whole feature without a single new CSS
   rule — the bars were native <progress> elements and the tiles reused
   .stat-card — and r102, r103 and r104 each carried a standing GREEN check
   asserting there was no new grocery CSS. ALL THREE ARE RETIRED BY NAME, in
   the round document and in each suite's own header. (An earlier draft of the
   round document said four and named r101 as well; r101 has no such check and
   never did.) A restyle that silently flipped a green invariant is the "check
   whose label lies" failure round .02 recorded, and this is the deliberate
   opposite of that.

   Scott read the dashboard artifact end to end on 08/25/2026 and asked for
   the page in its shape: a two-column masthead over a full-bleed ratio band,
   then five hairline-divided tiles in ONE card, then sections introduced by a
   heading with its context hard right. Grouped columns, a ratio bar and a
   money ledger cannot be built out of <progress> and .stat-card.

   EVERY RULE BELOW IS SCOPED TO .gr-page, WHICH ONLY view_groceries() EMITS.
   style-v2.css is shared by Census, Financials, Call Bells and every other
   page in the tracker, and this round must not move a pixel on any of them.
   The scoping is belt and braces: the `gr-` prefix would be enough on its
   own, and the .gr-page ancestor means even a name collision cannot escape.

   COLOR IS FUNCTIONAL HERE (DESIGN-NOTES 14). The three house hues are the
   tracker's own --h1/--h2/--h3, which house_badge() has assigned by position
   in HOUSES since round 3 — so a house is the same color in this chart, in
   its legend, and on its badge on the Residents page. The artifact draws
   teal, orange and purple; --h1/--h2/--h3 are teal, copper and blue, which is
   the same idea in the palette this site already owns.
   ========================================================================== */

/* ---- the eyebrow and the two-column masthead --------------------------- */
.gr-page .gr-eyebrow {
  font-size: var(--fs-2xs); line-height: var(--lh-2xs);
  text-transform: uppercase; letter-spacing: var(--tracking-caps);
  font-weight: var(--fw-semi); color: var(--text-subtle);
  margin: 0 0 var(--sp-3);
}
.gr-page .gr-masthead {
  display: flex; flex-wrap: wrap; gap: var(--sp-5) var(--sp-6);
  align-items: flex-start; margin: 0 0 var(--sp-5);
}
/* The headline column is wider than the metadata box and its paragraph stops
   short of its own edge — a measure, not a full-width line. The artifact sets
   both and it is most of why its masthead reads as a masthead. */
.gr-page .gr-masthead-main { flex: 1 1 380px; min-width: 0; }
.gr-page .gr-verdict {
  font-size: var(--fs-4xl); line-height: var(--lh-4xl);
  font-weight: var(--fw-bold); letter-spacing: var(--tracking-tight);
  color: var(--text); margin: 0 0 var(--sp-3); text-wrap: balance;
}
.gr-page .gr-verdict-clause { color: var(--brand); }
.gr-page .gr-lede {
  font-size: var(--fs-base); line-height: var(--lh-base);
  color: var(--text-muted); margin: 0; max-width: 54ch;
}
/* The metadata box is floated opposite the headline and top-aligns with the
   paragraph rather than with the first line of the headline. */
.gr-page .gr-meta {
  flex: 0 1 300px; min-width: 0; align-self: flex-start;
  background: var(--surface); border-radius: var(--r-md);
  box-shadow: var(--sh-key); padding: 14px 16px;
}
.gr-page .gr-meta-row {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: var(--sp-3); padding: 5px 0;
}
.gr-page .gr-meta-row + .gr-meta-row { border-top: 1px solid var(--line); }
.gr-page .gr-meta-k {
  font-size: var(--fs-2xs); line-height: var(--lh-2xs);
  text-transform: uppercase; letter-spacing: var(--tracking-caps);
  font-weight: var(--fw-semi); color: var(--text-subtle);
}
.gr-page .gr-meta-v {
  font-size: var(--fs-sm); line-height: var(--lh-sm); color: var(--text);
  font-variant-numeric: tabular-nums; text-align: right;
}

/* ---- the ratio band ----------------------------------------------------
   The one bar the artifact opens with. It is a composition of the QUICKBOOKS
   total — green is card money an order accounts for, red is card money
   nothing accounts for — and the two are the last two lines of the ledger
   directly beneath it. */
.gr-page .gr-band { margin: 0 0 var(--sp-5); }
.gr-page .gr-ratio {
  display: flex; height: 10px; border-radius: var(--r-pill);
  overflow: hidden; background: var(--slate-100);
}
.gr-page .gr-ratio-ok  { background: var(--green-500); }
.gr-page .gr-ratio-bad { background: var(--red-500); }
.gr-page .gr-ratio-legend {
  display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-5);
  font-size: var(--fs-sm); line-height: var(--lh-sm);
  color: var(--text-muted); margin: var(--sp-2) 0 0;
}
.gr-page .gr-key { font-weight: var(--fw-semi); font-variant-numeric: tabular-nums; }
.gr-page .gr-key-ok  { color: var(--ok); }
.gr-page .gr-key-bad { color: var(--danger); }

/* ---- five tiles, hairline-divided, in ONE card -------------------------
   Scott chose five over the artifact's four rather than drop either contested
   tile. The 1px gap over a --line background draws the hairlines in BOTH
   directions, so a row that wraps on a narrow screen is divided correctly
   without a second rule — a border-left alone leaves a wrapped row welded to
   the one above it. */
.gr-page .gr-tiles {
  display: flex; flex-wrap: wrap; gap: 1px;
  background: var(--line); border-radius: var(--r-lg);
  box-shadow: var(--sh-key), var(--sh-1);
  overflow: hidden; margin: 0 0 var(--sp-6); padding: 0;
}
.gr-page .gr-tiles > .stat-card {
  flex: 1 1 168px; min-width: 0; margin: 0; padding: 16px 18px;
  background: var(--surface); border: 0; border-radius: 0; box-shadow: none;
}
.gr-page .gr-tiles > .stat-card.warn { background: var(--warn-bg); }
.gr-page .gr-tiles .stat-num { font-size: var(--fs-2xl); line-height: var(--lh-2xl); }

/* ---- the section header: title left, context right, rule under both ----
   The pill under the title is the tracker's own heading mark, the same one
   .panel > h2 carries on every other page. Scott asked for pill-shaped
   headings in the site's normal font, and this is that mark rather than a
   new one invented for this page. */
.gr-page .gr-sec {
  display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4);
  align-items: baseline; justify-content: space-between;
  border-bottom: 1px solid var(--line);
  margin: var(--sp-6) 0 var(--sp-4); padding-bottom: var(--sp-3);
}
.gr-page .gr-sec:first-of-type { margin-top: 0; }
.gr-page .gr-sec h2 {
  position: relative; margin: 0; padding-bottom: var(--sp-3);
  font-size: var(--fs-xl); line-height: var(--lh-xl);
}
.gr-page .gr-sec h2::after {
  content: ""; position: absolute; left: 0; bottom: 0;
  width: 40px; height: 3px; border-radius: var(--r-pill);
  background: linear-gradient(90deg, var(--teal-600), var(--mint-400));
}
.gr-page .gr-sec-context {
  font-size: var(--fs-sm); line-height: var(--lh-sm);
  color: var(--text-muted); text-align: right;
}
/* The panels under a section heading carry their own subheadings now, not a
   second h2 — two h2s under one section heading is two section headings. */
.gr-page .panel > h3 {
  font-size: var(--fs-lg); line-height: var(--lh-lg);
  font-weight: var(--fw-semi); margin: 0 0 var(--sp-3);
}

/* ---- the ledger -------------------------------------------------------- */
.gr-page .gr-ledger { width: 100%; border-collapse: collapse; margin: var(--sp-4) 0; }
.gr-page .gr-ledger th, .gr-page .gr-ledger td {
  padding: 7px 0; text-align: left; font-weight: var(--fw-normal);
  vertical-align: baseline;
}
.gr-page .gr-led-money {
  text-align: right; font-variant-numeric: tabular-nums;
  white-space: nowrap; padding-left: var(--sp-4);
}
.gr-page .gr-led-head > th { font-weight: var(--fw-semi); color: var(--text); }
.gr-page .gr-led-head > .gr-led-money { font-weight: var(--fw-semi); }
.gr-page .gr-led-count {
  font-size: var(--fs-xs); color: var(--text-subtle);
  font-variant-numeric: tabular-nums; font-weight: var(--fw-normal);
}
.gr-page .gr-led-sub > th, .gr-page .gr-led-sub > td {
  color: var(--text-muted); font-size: var(--fs-sm); padding-top: 3px;
  padding-bottom: 3px;
}
.gr-page .gr-led-sub > th { padding-left: var(--sp-4); }
.gr-page .gr-led-rule > th, .gr-page .gr-led-rule > td {
  border-top: 1px solid var(--line); padding-top: 9px;
}
.gr-page .gr-led-total > th, .gr-page .gr-led-total > td {
  border-top: 2px solid var(--text); padding-top: 10px;
}

/* ---- the exceptions container: one red rail down both blocks ----------- */
.gr-page .gr-alert { border-left: 3px solid var(--danger); }

/* ---- spend by house ---------------------------------------------------- */
.gr-page .gr-house-list { list-style: none; margin: var(--sp-4) 0; padding: 0; }
.gr-page .gr-house + .gr-house {
  border-top: 1px solid var(--line); margin-top: var(--sp-4); padding-top: var(--sp-4);
}
.gr-page .gr-house-top {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: var(--sp-3);
}
.gr-page .gr-house-name { font-weight: var(--fw-semi); color: var(--text); }
.gr-page .gr-house-money {
  font-size: var(--fs-lg); line-height: var(--lh-lg);
  font-weight: var(--fw-semi); font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* !! ::-webkit-progress-value DOES NOTHING UNTIL THE NATIVE APPEARANCE IS
   TURNED OFF. Without `appearance: none` Chromium keeps painting its own blue
   bar and every house reads the same color -- which is what the first cut of
   this panel did. Nothing else in the tracker styles a <progress>, so this is
   the first place it has ever mattered; the checklist's own bar above is left
   native on purpose, because it measures progress rather than a house. */
.gr-page .gr-house-bar {
  width: 100%; height: 6px; display: block; margin: 7px 0 5px;
  appearance: none; -webkit-appearance: none; border: 0;
  background: var(--slate-100); border-radius: var(--r-pill); overflow: hidden;
}
.gr-page .gr-house-bar::-webkit-progress-bar { background: var(--slate-100); }
.gr-page .gr-house-sub {
  font-size: var(--fs-xs); line-height: var(--lh-xs);
  color: var(--text-subtle); font-variant-numeric: tabular-nums;
}

/* ---- month by month, as grouped columns -------------------------------
   Reversed back to columns on Scott's own instruction, 08/25/2026, after he
   read the artifact. Round .04 recorded the bars "so a later round does not
   'fix' it back"; he fixed it back himself. ONE SCALE ACROSS EVERY MONTH is
   what grouped columns give by construction — they share a y-axis — so .04's
   rule survives the change of chart rather than being dropped by it. */
.gr-page .gr-chart {
  display: flex; gap: var(--sp-3); align-items: flex-start;
  margin: var(--sp-4) 0 var(--sp-3);
}
.gr-page .gr-axis {
  list-style: none; margin: 0; padding: 0;
  height: 200px; display: flex; flex-direction: column;
  justify-content: space-between; flex: 0 0 auto;
}
.gr-page .gr-tick { line-height: 0; }
.gr-page .gr-tick-label {
  display: block; transform: translateY(-50%);
  font-size: var(--fs-2xs); line-height: var(--lh-2xs);
  color: var(--text-subtle); font-variant-numeric: tabular-nums;
  white-space: nowrap; text-align: right;
}
.gr-page .gr-plot {
  position: relative; flex: 1 1 auto; min-width: 0;
  display: flex; gap: var(--sp-3); align-items: flex-start;
}
/* The gridlines are one element across the whole plot rather than a repeated
   background inside each group, so they do not break in the gaps between
   months. 200px over four steps is 50px. */
.gr-page .gr-plot::before {
  content: ""; position: absolute; left: 0; right: 0; top: 0; height: 200px;
  background: repeating-linear-gradient(to bottom,
    var(--line) 0 1px, transparent 1px 50px);
  pointer-events: none;
}
/* Positioned, and later in DOM order, so the columns paint over the lines. */
.gr-page .gr-group {
  position: relative; flex: 1 1 0; min-width: 0;
  display: flex; flex-direction: column; align-items: center;
}
.gr-page .gr-cols {
  height: 200px; width: 100%;
  display: flex; align-items: flex-end; justify-content: center; gap: 5px;
}
/* The baseline is ONE line across the whole plot, not a border on each group
   -- a per-group border breaks in the gaps between months and the chart then
   has three little floors instead of an axis. */
.gr-page .gr-plot::after {
  content: ""; position: absolute; left: 0; right: 0; top: 200px;
  border-top: 1px solid var(--slate-300); pointer-events: none;
}
.gr-page .gr-col {
  width: 26px; max-width: 22%; min-height: 2px;
  border-radius: var(--r-xs) var(--r-xs) 0 0;
}
.gr-page .gr-xlabel {
  margin-top: var(--sp-2); text-align: center;
  font-size: var(--fs-xs); line-height: var(--lh-xs);
  color: var(--text-muted); font-variant-numeric: tabular-nums;
}
.gr-page .gr-legend {
  display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-5);
  font-size: var(--fs-sm); line-height: var(--lh-sm);
  color: var(--text-muted); margin: 0 0 var(--sp-3);
}
.gr-page .gr-legend-item { display: inline-flex; align-items: center; gap: 7px; }
.gr-page .gr-legend-dot {
  width: 10px; height: 10px; border-radius: var(--r-xs); flex: 0 0 auto;
}
.gr-page .gr-chart-data { margin: 0 0 var(--sp-3); }
.gr-page .gr-chart-data summary { cursor: pointer; }

/* The three house hues, one slot per house, matching house_badge(). Slot 0 is
   a login with no house yet (section 8) — real orders, no hue of its own. */
.gr-page .gr-slot-1 .gr-legend-dot, .gr-page .gr-col.gr-slot-1 { background: var(--h1-dot); }
.gr-page .gr-slot-2 .gr-legend-dot, .gr-page .gr-col.gr-slot-2 { background: var(--h2-dot); }
.gr-page .gr-slot-3 .gr-legend-dot, .gr-page .gr-col.gr-slot-3 { background: var(--h3-dot); }
.gr-page .gr-slot-0 .gr-legend-dot, .gr-page .gr-col.gr-slot-0 { background: var(--slate-400); }
.gr-page .gr-house.gr-slot-1 .gr-house-bar::-webkit-progress-value { background: var(--h1-dot); }
.gr-page .gr-house.gr-slot-2 .gr-house-bar::-webkit-progress-value { background: var(--h2-dot); }
.gr-page .gr-house.gr-slot-3 .gr-house-bar::-webkit-progress-value { background: var(--h3-dot); }
.gr-page .gr-house.gr-slot-1 .gr-house-bar::-moz-progress-bar { background: var(--h1-dot); }
.gr-page .gr-house.gr-slot-2 .gr-house-bar::-moz-progress-bar { background: var(--h2-dot); }
.gr-page .gr-house.gr-slot-3 .gr-house-bar::-moz-progress-bar { background: var(--h3-dot); }

/* ---- the order table and its filter buttons ---------------------------
   The buttons reuse .wr-pbtn, which the Financials page already ships and
   which is already a pill with an aria-pressed state. A second pill button
   invented for this page would be a second answer to the same question. */
.gr-page .gr-search {
  display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center;
  margin: 0 0 var(--sp-3);
}
.gr-page .gr-filters { margin: 0 0 var(--sp-3); }
/* The artifact's own order table is wider than its content column and its
   STATUS column is clipped off the right edge. That is a defect in the
   artifact, not a thing to reproduce: this one scrolls inside its own box and
   the page body never scrolls sideways. */
.gr-page .gr-table-scroll { overflow-x: auto; margin: 0 0 var(--sp-3); }
.gr-page .gr-orders { min-width: 720px; }
.gr-page .gr-num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.gr-page .gr-state { white-space: nowrap; }
.gr-page .gr-state-matched   { color: var(--ok); }
.gr-page .gr-state-exception { color: var(--danger); font-weight: var(--fw-medium); }
.gr-page .gr-state-info      { color: var(--text-subtle); }
/* The refunded column carries a figure on 10 of the 93 orders and is blank on
   the rest, so it is quieter than the amount beside it -- a column of empty
   cells that shouts is a column that reads as missing data. */
.gr-page .gr-refund { color: var(--text-subtle); }

/* ---- the QuickBooks pull window ---------------------------------------- */
.gr-page .gr-pull {
  display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: flex-end;
  margin: 0 0 var(--sp-2);
}

/* ---- the period filter, at the top of the page -------------------------- */
.gr-page .gr-period { margin: 0 0 var(--sp-1); }
.gr-page .gr-period-note { margin: 0 0 var(--sp-4); }

/* ---- the order table collapses, and opens to the most recent 20 ---------
   No JavaScript: a <details> for the section and a link for the rest, which
   is the same call .05 made for the house and exception filters. */
.gr-page .gr-orders-wrap { margin: 0 0 var(--sp-3); }
.gr-page .gr-orders-summary {
  cursor: pointer;
  padding: var(--sp-2) 0;
  font-weight: var(--fw-medium);
  color: var(--text-subtle);
  letter-spacing: 0.02em;
}
.gr-page .gr-orders-summary:hover { color: var(--text); }
.gr-page .gr-orders-summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.gr-page .gr-more { margin: 0 0 var(--sp-3); }

/* ---- the two spend charts, side by side --------------------------------
   Scott, 08/26/2026: "you can put the total spend by house chart and the
   month by month chart right next to each other instead of stacked."
   Both still sit under the single `What each house spends` heading, which is
   .05's grouping and his choice.

   !! minmax(0, 1fr) RATHER THAN 1fr. A grid column's default min-width is
   auto, so the month chart -- which is a row of fixed-width columns -- would
   refuse to shrink below its own content and push the grid wider than the
   page. That is BACKLOG 147's defect exactly, one panel over. */
.gr-page .gr-charts {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--sp-3);
  align-items: start;
}
.gr-page .gr-charts > .panel { margin: 0; }

/* ---- narrow screens ----------------------------------------------------
   The masthead stacks, the metadata box goes full width, and the headline
   drops to the h1 size rather than the hero size. */
@media (max-width: 760px) {
  .gr-page .gr-verdict { font-size: var(--fs-2xl); line-height: var(--lh-2xl); }
  .gr-page .gr-meta { flex: 1 1 100%; }
  .gr-page .gr-sec-context { text-align: left; }
  .gr-page .gr-col { width: 18px; }
}

/* !! THE CHARTS STACK BACK WELL BEFORE THE PHONE BREAKPOINT. Two grouped
   column charts side by side stop being readable long before 760px -- the
   nine columns of the month chart get about 18px each at that width. 1024 is
   where the pair stops earning the split, and it is deliberately NOT the
   760px the rest of this block uses. */
@media (max-width: 1024px) {
  .gr-page .gr-charts { grid-template-columns: minmax(0, 1fr); }
}
