/* ══════════════════════════════════════════════════════════════════════════════
   design-c.css — Direction C, the design system.

   Loaded AFTER style.css so source order does the work and we never reach for
   !important. See DESIGN_SPEC.md for the law; this file is its implementation.

   ── HOW THIS FILE GROWS ──
   Stage 1 (this commit): TOKENS ONLY. Custom properties are inert — nothing
   consumes them yet — so this commit is provably a no-op and safe to deploy.
   Components arrive one at a time afterwards, each with a browser verification
   and a baseline compare, because .btn alone has 61 existing rules in style.css
   and taking it over restyles every button in the product at once.

   ── WHAT THIS FILE MUST NEVER TOUCH ──
   The Solar Studio. No selector in here may match #solar-studio, .ss-*, #ss-*,
   #sai-* or .sl-*. That tool has 155 controls, its panels are SVG whose
   hit-testing lives in the `fill` declaration, its chrome positions are
   hard-coded clearance against the system card, its z-index ladder and flex
   height chain are load-bearing, and the gate that reveals the entire design kit
   is a single CSS rule. There is also no build gate for any solar file, so a
   green build proves nothing there. audit/SOLAR_PROTECTION_CONTRACT.md.

   It is also 100% motion-free today: not one transition, animation or
   will-change in css/style.css:6120-6430. Do not introduce one.
   ══════════════════════════════════════════════════════════════════════════════ */

/* ── TOKENS · light (the default) ─────────────────────────────────────────── */
:root{
  /* the frame: identical in BOTH themes — this is what makes the product
     recognisable, so it deliberately does not flip */
  --c-frame:#0D0F13;
  --c-frame-ink:#F5F6F8;
  --c-frame-mut:#9AA1AB;
  --c-frame-line:rgba(245,246,248,.09);
  --c-frame-hover:rgba(245,246,248,.07);

  /* action + incoming. Named as FILLS on purpose: --c-ink-money and --c-pipe-ink
     are TEXT colours and must never be used as a background. Filling a dot or a
     badge with #8A6800 is the brown-blob mistake the July audit found in HQ. */
  --c-fill-action:#FFDD33;
  --c-fill-action-press:#FFD60A;
  --c-action-ink:#0B0C0E;
  --c-cyan:#3FE3FF;

  /* the work surface */
  --c-surface:#F6F6F3;
  --c-panel:#FFFFFF;
  --c-line:#D8D7CC;
  --c-line-strong:#B5B4A6;   /* structural rules; 2.09:1, deliberately NOT a WCAG 3:1 line */
  --c-ink:#16181D;
  --c-mut:#5F6774;
  --c-ink-money:#8A6800;     /* TEXT ONLY */

  /* status: a tinted ground + an ink, never a coloured left-border */
  --c-pipe-bg:rgba(232,163,0,.16);  --c-pipe-ink:#8A6800;   /* TEXT ONLY */
  --c-ok-bg:rgba(5,150,105,.10);    --c-ok-ink:#04785C;    --c-ok-dot:#059669;
  --c-in-bg:rgba(13,143,174,.10);   --c-in-ink:#0B7089;    --c-in-dot:#0D8FAE;
  --c-bad-bg:rgba(179,55,46,.09);   --c-bad-ink:#B3372E;   --c-bad-dot:#B3372E;
  --c-sys-bg:rgba(139,147,161,.14); --c-sys-ink:rgba(13,15,19,.60); --c-sys-dot:#8B93A1;

  /* depth is a hairline; a shadow is only for things that genuinely float */
  --c-shadow:0 12px 32px rgba(13,15,19,.14), 0 2px 6px rgba(13,15,19,.06);

  /* type */
  --c-font-display:'Clash Display', 'Satoshi', system-ui, sans-serif;
  --c-font-body:'Satoshi', Inter, system-ui, -apple-system, sans-serif;

  /* space — 4px base grid */
  --c-s1:4px; --c-s2:8px; --c-s3:12px; --c-s4:16px; --c-s5:20px; --c-s6:26px;

  /* shape */
  --c-r-pill:999px; --c-r-card:14px; --c-r-input:10px; --c-r-chip:9px;

  /* tap targets — the phone floor is 44px and is not negotiable */
  --c-tap:34px; --c-tap-touch:44px;

  /* the signature gradient. ONCE per surface, on one hero element. */
  --c-gradient:linear-gradient(100deg, #FFDD33, #3FE3FF);
}

/* ── TOKENS · dark (a real switch, not an afterthought) ───────────────────── */
/* Supports both the app's existing theme class and the OS preference. The frame
   tokens are deliberately absent here: the frame does not change. */
/* ⚠ THE APP HAS NO DARK CLASS. `applyTheme()` (js/extras.js:759) only toggles
   `theme-light` ON or OFF — dark is the ABSENCE of that class. There is no
   `theme-dark`, no `html.dark`, no `data-theme`.
   My first version of this block targeted those three and therefore NEVER matched
   in the real app: dark mode ran on the LIGHT token values, putting --c-mut
   (#5F6774) on a #15181E panel at 2.43:1 across 46 elements. QA Orchestra's
   browser-validation stage caught it. `body:not(.theme-light)` is the only
   selector that is actually true here. html.dark is kept so the design previews,
   which do use it, still work. */
html.dark, body:not(.theme-light){
  --c-surface:#0B0C0E;
  --c-panel:#15181E;
  --c-line:rgba(154,161,171,.16);
  --c-line-strong:rgba(154,161,171,.34);
  --c-ink:#F5F6F8;
  --c-mut:#9AA1AB;
  --c-ink-money:#FFDD33;

  --c-pipe-bg:rgba(255,221,51,.11);  --c-pipe-ink:#FFDD33;
  --c-ok-bg:rgba(62,207,142,.11);    --c-ok-ink:#3ECF8E;   --c-ok-dot:#3ECF8E;
  --c-in-bg:rgba(63,227,255,.09);    --c-in-ink:#3FE3FF;   --c-in-dot:#3FE3FF;
  --c-bad-bg:rgba(255,110,97,.11);   --c-bad-ink:#FF6E61;  --c-bad-dot:#FF6E61;
  --c-sys-bg:rgba(154,161,171,.12);  --c-sys-ink:#9AA1AB;  --c-sys-dot:#9AA1AB;

  --c-shadow:0 16px 40px rgba(0,0,0,.55), 0 2px 8px rgba(0,0,0,.35);
}

/* ── the accessibility floor, applied globally ────────────────────────────── */
/* Visible keyboard focus. The app had no consistent focus style; this is the
   only rule in this commit that is not purely a token, because a missing focus
   ring is an accessibility defect rather than a style preference. It is scoped
   away from the solar studio, which manages its own pointer/keyboard model. */
:focus-visible{
  outline:2px solid var(--c-in-dot);
  outline-offset:2px;
}
#solar-studio :focus-visible{ outline:revert; outline-offset:revert; }

@media (prefers-reduced-motion: reduce){
  *, *::before, *::after{
    animation-duration:.001ms !important;
    animation-iteration-count:1 !important;
    transition-duration:.001ms !important;
    scroll-behavior:auto !important;
  }
}

/* ══════════════════════════════════════════════════════════════════════════════
   COMPONENTS · pass 1 — surfaces, text and buttons

   These override classes the app ALREADY emits, so every screen improves at once
   with no JS change: .btn is emitted 663 times, .small 1073, .muted 676,
   .btn-amber 208, .panel 59. That reach is why this is the right way to work in a
   no-build app.

   ── SPECIFICITY ──
   Matched selector-for-selector against style.css rather than using !important,
   because style.css carries `body.theme-light .btn` (0,2,1) copies that an
   unqualified `.btn` would lose to. Where a rule looks over-qualified, that is why.

   ── THE SOLAR EXCLUSION, AND WHY IT IS WRITTEN THIS WAY ──
   The studio does use the generic classes: .btn (5), .btn-amber (1), .small (5),
   .muted (1), .chip-btn (3). So every rule that could reach inside it carries
   `:not(#solar-studio *)`.
   Do NOT be tempted to "reset" those elements with `all:revert` instead — revert
   rolls back past the AUTHOR origin, so it would strip style.css's own solar
   rules too and break the tool's appearance. Excluding the match is the only
   correct approach. audit/SOLAR_PROTECTION_CONTRACT.md.
   ══════════════════════════════════════════════════════════════════════════════ */

/* ── cards ──────────────────────────────────────────────────────────────────
   Depth is a hairline, never a shadow. style.css has been through several passes
   here (an inset glow, then a 6px shadow, then a "CALM SWEEP" that removed it for
   in-flow cards) so the end state differs between themes. This settles it: one
   hairline, one radius, both themes. .panel is never used inside the studio, so
   no exclusion is needed. */
body.theme-light .panel,
body.theme-dark  .panel,
.panel{
  background:var(--c-panel);
  border:1px solid var(--c-line);
  border-radius:var(--c-r-card);
  box-shadow:none;
}

/* Panel headings: style.css already quietened these to uppercase/muted and killed
   the yellow ::before bar. Direction C holds that and fixes the size and tracking. */
body.theme-light .section-h,
body.theme-light .panel > h3,
.panel > h3{
  font-size:11px;
  font-weight:700;
  letter-spacing:.09em;
  text-transform:uppercase;
  color:var(--c-mut);
}

/* ── typography ─────────────────────────────────────────────────────────────
   .small and .muted carry most of the secondary text in the product. 12px was
   under the readable floor for body copy; 12.5 is the spec minimum. */
.small:not(#solar-studio *){ font-size:12.5px; }
.muted:not(#solar-studio *){ color:var(--c-mut); }

/* Money reads as money: display face, tabular figures, so a column of prices
   lines up on the decimal point. */
.money, .tot-row b, .tot-val{
  font-family:var(--c-font-display);
  font-variant-numeric:tabular-nums;
  font-weight:700;
}

/* ── buttons ────────────────────────────────────────────────────────────────
   style.css's v423 "control system" already sets one height law (38px, 0 16px,
   13.5px) and eng-mode raises it to 44px for gloved hands. BOTH ARE KEPT — no
   height or padding is set here, so nothing reflows. What changes is shape and
   colour:
     · pill radius, per the approved direction
     · no gradient: style.css gives every dark-theme button a
       linear-gradient(180deg,#20242e,…). Direction C spends its one gradient per
       surface on a hero, not on 663 buttons.
     · hover moves the border, not the button. style.css had a translateY on
       .btn-amber:hover, which makes a row of buttons twitch as the cursor passes. */
body.theme-light .btn:not(#solar-studio *),
body.theme-dark  .btn:not(#solar-studio *),
.btn:not(#solar-studio *){
  background:var(--c-panel);
  color:var(--c-ink);
  border:1px solid var(--c-line);
  border-radius:var(--c-r-pill);
  font-weight:700;
  box-shadow:none;
}
body.theme-light .btn:not(#solar-studio *):hover,
body.theme-dark  .btn:not(#solar-studio *):hover,
.btn:not(#solar-studio *):hover{
  border-color:var(--c-mut);
  background:var(--c-surface);
  transform:none;
}

/* The primary action. In the light theme the app currently renders this CHARCOAL
   with yellow text; Direction C commits to the yellow fill, which is the single
   most visible change in this pass. #0B0C0E on #FFDD33 measures 14.58:1. */
body.theme-light .btn-amber:not(#solar-studio *),
body.theme-dark  .btn-amber:not(#solar-studio *),
.btn-amber:not(#solar-studio *){
  background:var(--c-fill-action);
  border-color:var(--c-fill-action);
  color:var(--c-action-ink);
  font-weight:700;
  box-shadow:none;
}
body.theme-light .btn-amber:not(#solar-studio *):hover,
body.theme-dark  .btn-amber:not(#solar-studio *):hover,
.btn-amber:not(#solar-studio *):hover{
  background:var(--c-fill-action-press);
  border-color:var(--c-fill-action-press);
  box-shadow:none;
  transform:none;
}

/* Icon and chip buttons take the same shape law so a toolbar reads as one set. */
.icon-btn:not(#solar-studio *),
.chip-btn:not(#solar-studio *){
  border-radius:var(--c-r-pill);
  box-shadow:none;
}

/* ══════════════════════════════════════════════════════════════════════════════
   COMPONENTS · pass 2 — the sidebar, the frame's most-seen detail

   Three things are wrong with the active nav item today:
     1. It is a charcoal fill with a yellow left bar AND a ::before bar carrying
        linear-gradient(180deg,#ffdd33,#3fe3ff). That is a second signature
        gradient on a surface whose wordmark already spends it — the direction
        allows one per surface, deliberately.
     2. `.nav-btn:hover{ padding-left:16px }` shifts the label 6px right on hover,
        so the whole list twitches as the cursor travels down it.
     3. `transition: all .13s ease` on .nav-btn animates layout properties, which
        is what makes that shift visible rather than instant.

   Direction C: the active place is a yellow pill. Nothing moves on hover.
   Specificity matched against `body.theme-light .sidebar .nav-btn.active` (0,4,1).
   ══════════════════════════════════════════════════════════════════════════════ */

body.theme-light .sidebar .nav-btn,
body.theme-dark  .sidebar .nav-btn,
.sidebar .nav-btn{
  border-left:0;
  border-radius:10px;
  color:var(--c-frame-mut);
  transition:background-color .12s ease-out, color .12s ease-out;
}

/* hover: the ground changes, the text does not move */
body.theme-light .sidebar .nav-btn:hover,
body.theme-dark  .sidebar .nav-btn:hover,
.sidebar .nav-btn:hover{
  background:var(--c-frame-hover);
  color:var(--c-frame-ink);
  padding-left:12px;
}

/* the active place: one yellow pill, no bar, no gradient */
body.theme-light .sidebar .nav-btn.active,
body.theme-dark  .sidebar .nav-btn.active,
.sidebar .nav-btn.active{
  background:var(--c-fill-action);
  color:var(--c-action-ink);
  border-left-color:transparent;
  font-weight:700;
}
body.theme-light .sidebar .nav-btn.active::before,
body.theme-dark  .sidebar .nav-btn.active::before,
.sidebar .nav-btn.active::before{ display:none; }

/* The locked-module padlock keeps working: dimmed, still legible on the yellow
   pill if a locked item is also the active one. */
.sidebar .nav-btn.nav-locked.active{ opacity:.9; }

/* ── v816 · THE LOCKED DESTINATION, AS A PAGE ────────────────────────────────
   Jamie, 6 Aug: the nav item stays, the destination explains itself. js/
   entitlements.js paints .sq-scope into the refused view's host, reusing
   .sq-teaser as its root because css/style.css:5518 hides everything in a
   .sq-locked view that is NOT .sq-teaser.

   ⚠ PLACED HERE, BESIDE .nav-locked, AND NOT AT THE END OF THE FILE. Two gates
   slice this sheet from a marker TO EOF with no closing delimiter —
   check-home-floor-g7.js (the G7 block) and check-compliance-design.js (Lane C,
   which bans raw hex) — so anything appended after them is silently read as
   part of their block. The note at the G7 header in this file records the same
   trap. Rules go next to their siblings; the sibling here is the nav padlock.

   STRUCTURE ONLY, DELIBERATELY. The visual treatment of this page is an
   Impeccable pass landing on top of this markup, and over-styling it now would
   only have to be unpicked. Closed palette, no !important, source order does
   the work, and every value is a --c-* token. */
.sq-scope .sq-scope-kicker{
  font-family:var(--c-font-body);
  font-size:12px;
  font-weight:700;
  letter-spacing:.09em;
  text-transform:uppercase;
  color:var(--c-mut);
  margin:0 0 8px;
}
.sq-scope .sq-scope-title{
  font-family:var(--c-font-display);
  color:var(--c-ink);
  margin:0 0 10px;
}
.sq-scope .sq-scope-lede{
  color:var(--c-ink);
  font-size:15px;
  line-height:1.55;
  margin:0 0 14px;
}
.sq-scope .sq-scope-how{
  color:var(--c-mut);
  font-size:13.5px;
  line-height:1.5;
  margin:0 0 16px;
}
/* A destination that is genuinely unbuilt reads differently from one the firm
   could have tomorrow — a word AND a colour, never colour alone. */
.sq-scope[data-scope-state="soon"] .sq-scope-kicker{ color:var(--c-pipe-ink); }

/* ── v816 · ADD A CUSTOMER FROM THE APP ──────────────────────────────────────
   The narrow form js/customers.js paints into #cust-list for an engineer, in
   place of the office table. Structure only, same reasoning as the block above:
   one column, thumb-sized controls, and nothing that could render a figure.

   The save button is IRREVERSIBLE in the sense DESIGN_SPEC means — it writes a
   record the office then works from — so it takes the 44px touch floor Jamie
   ruled on 30 Jul rather than the ordinary 30px. --c-tap-irrev is that token. */
.cq-wrap{ max-width:520px; }
.cq-lede{ color:var(--c-mut); margin:0 0 14px; }
.cq-form{ display:flex; flex-direction:column; gap:12px; }
.cq-field{ display:flex; flex-direction:column; gap:4px; }
.cq-field-wide{ width:100%; }
.cq-say{ min-height:1.2em; margin:10px 0 0; color:var(--c-mut); font-size:13.5px; }
.cq-say-bad{ color:var(--c-bad-ink); font-weight:600; }
.cq-save,
.cq-ask-same,
.cq-ask-diff{ min-height:var(--c-tap-irrev); margin-top:14px; }
.cq-ask-actions{ display:flex; flex-wrap:wrap; gap:10px; }
.cq-ask-head,
.cq-done-head,
.cq-off-head{ font-family:var(--c-font-display); color:var(--c-ink); margin:0 0 8px; }
.cq-ask-note,
.cq-done-note,
.cq-off-note{ color:var(--c-mut); line-height:1.5; margin:0 0 14px; }

/* Group labels in the rail take the same quiet all-caps treatment as panel heads. */
.sidebar .nav-group{
  font-size:10px;
  font-weight:700;
  letter-spacing:.09em;
  text-transform:uppercase;
  color:var(--c-frame-mut);
}

/* ══════════════════════════════════════════════════════════════════════════════
   COMPONENTS · pass 3 — kill the side-tab accent, keep every signal

   Impeccable's detector found 41 of these in style.css and calls it "the most
   recognizable tell of AI-generated UIs". DESIGN_SPEC bans it: status is a tinted
   chip or a dot plus a word, never a coloured edge.

   But most of these 3-5px stripes CARRY MEANING and cannot simply be deleted:
     .mny-row.mny-in / .mny-out   green vs red = money in vs out
     .sch-block / .sb / .eng-tl-blk  the stripe is the ENGINEER colour (var(--sc))
     .todo-row.is-late / .fa-task.over / .jd-note.jd-issue   lateness and issues
     .cert-sec / .cert-check      the certificate section colour (var(--csec))
   Deleting them would remove information from the screen, which is the one thing
   this redesign may not do. Converting them to a tinted ground or a dot needs
   markup changes in fourteen render functions, which is per-screen work.

   So this pass demotes the stripe to a 1px HAIRLINE in the same colour. The tell
   is gone (a hairline is not a side-tab), every signal survives, it is pure CSS,
   and there is no browser-support risk. Each one gets its proper Direction C
   treatment when its screen is rebuilt, and this is recorded in REDESIGN_LOG.md
   as an interim state rather than a finished one.
   ══════════════════════════════════════════════════════════════════════════════ */

.room-head:not(#solar-studio *){ border-left-width:1px; }
.cal-ev:not(#solar-studio *){ border-left-width:1px; }
.qtpl-banner:not(#solar-studio *){ border-left-width:1px; }
.jd-note.jd-issue:not(#solar-studio *){ border-left-width:1px; }
.line-sup:not(#solar-studio *){ border-left-width:1px; }
.job-cust, .job-diary:not(#solar-studio *){ border-left-width:1px; }
.opt-bar:not(#solar-studio *){ border-left-width:1px; }
.cert-sec:not(#solar-studio *){ border-left-width:1px; }
.cert-check:not(#solar-studio *){ border-left-width:1px; }
.insp-write:not(#solar-studio *){ border-left-width:1px; }
tr.qg-room td:not(#solar-studio *){ border-left-width:1px; }
.sb:not(#solar-studio *){ border-left-width:1px; }
.eng-tl-blk:not(#solar-studio *){ border-left-width:1px; }
.todo-row.is-late:not(#solar-studio *){ border-left-width:1px; }
.set-sec-blurb:not(#solar-studio *){ border-left-width:1px; }
.qg-next:not(#solar-studio *){ border-left-width:1px; }
.onmy-card:not(#solar-studio *){ border-left-width:1px; }
.sch-block:not(#solar-studio *){ border-left-width:1px; }
#desk-body .onmy-card:not(#solar-studio *){ border-left-width:1px; }
.cq-summary:not(#solar-studio *){ border-left-width:1px; }
.cmp-remind:not(#solar-studio *){ border-left-width:1px; }
.mny-row.mny-in:not(#solar-studio *){ border-left-width:1px; }
.mny-row.mny-out:not(#solar-studio *){ border-left-width:1px; }
.mny-rec:not(#solar-studio *){ border-left-width:1px; }
.fa-task.over:not(#solar-studio *){ border-left-width:1px; }
.ma-msg-body blockquote, .ma-quote blockquote:not(#solar-studio *){ border-left-width:1px; }
.sm-warn:not(#solar-studio *){ border-left-width:1px; }
.lc:not(#solar-studio *){ border-left-width:1px; }
.wk-b:not(#solar-studio *){ border-left-width:1px; }
.insp-pack:not(#solar-studio *){ border-left-width:1px; }
.djp-card:not(#solar-studio *){ border-left-width:1px; }
.dnx-list .dnx-row:not(#solar-studio *){ border-left-width:1px; }
.pjc-item:not(#solar-studio *){ border-left-width:1px; }
.sched-wrap.is-vert .sch-block:not(#solar-studio *){ border-left-width:1px; }

/* the same tell done with an inset shadow instead of a border */
.cv-row.cv-on:not(#solar-studio *){ box-shadow:none; }
.dnx-lead.dnx-lead-new:not(#solar-studio *){ box-shadow:none; }
/* `.sch-visit:not(#solar-studio *){ box-shadow:none }` stood here and was DEAD —
   (1,1,0) but not !important, against css/style.css:4737's !important inset spine,
   so the stripe this pass believed it had demoted was still 3px on the board. The
   demotion is done properly in the .sch-block.sch-visit rule further down, which
   states the whole treatment in one place. */
.hqi-item.on:not(#solar-studio *){ box-shadow:none; }

/* style.css:2149 sets `border-left:3px solid #ffd60a !important` on .room-head.
   DESIGN_SPEC says match specificity rather than reach for !important — but you
   cannot out-specify an !important without one, so this is the documented
   exception rather than a habit. */
.room-head:not(#solar-studio *){ border-left-width:1px !important; }

/* ── LIGHT-THEME CONTRAST · two real failures the old probe was hiding ──────
   The QA harness used to test "dark" by adding html.dark + body.theme-dark.
   THE APP HAS NEITHER — applyTheme() (extras.js:759) toggles theme-light and
   dark is its absence — so that sweep measured a hybrid theme that exists
   nowhere, reported 46 phantom dark failures, and never once looked at the
   LIGHT theme properly. With the probe driving the real switch: dark 0,
   light 2. Both are pre-existing in style.css (Bucket 2), both are --muted
   (#5f6774) sitting on a TINTED ground rather than on the panel. */

/* 1. The desk tab counts. Every other .tab-ct in the product already has
      `background:none` (style.css:2137); .job-tabs alone re-adds a grey
      wash (style.css:8078), which drags 5.85:1 down to 4.32:1. Putting it
      back on the panel fixes the contrast AND makes the badge consistent
      with the other 8 places .tab-ct is used. style.css:8078 sets that wash
      with !important, and you cannot out-specify an !important without one —
      the same documented exception as .room-head below, not a habit. */
body.theme-light .job-tabs .tab-ct:not(#solar-studio *){ background:none !important; }

/* 2. The hours-per-cell figure in the Calendar week grid. This is not chrome:
      PAGE_PURPOSE records that a row per engineer x 7 days with AN HOURS
      COUNT PER CELL is the whole point of that grid, so the one number the
      page exists to show was the least readable thing on it (4.14:1 at 10px).
      Promoted from --muted to the closed palette's own body ink: same size,
      no reflow. --c-ink (#16181D) and NOT style.css's --text (#1a1d23):
      DESIGN_SPEC §1 says nothing outside the closed palette may appear in app
      CSS, and reaching for the old sheet's token because it was nearer to hand
      is how a design system drifts one honest little exception at a time. */
body.theme-light .wk-h:not(#solar-studio *){ color:var(--c-ink); }

/* ── the icon scale (js/icons.js) ─────────────────────────────────────────────
   stroke-width steps DOWN as the box grows, so the rendered stroke stays
   optically constant (1.28 / 1.33 / 1.50 / 1.75 device px) and a large glyph
   never looks wiry. A CSS stroke-width beats the presentation attribute on the
   <svg>, and .fa-ico.ico-md is (0,2,0) in a later sheet, so no !important.
   The 46 existing .fa-ico rules in css/style.css are field-app rules and are
   untouched — none of them uses .ico-*.
   NEVER write a `color:` on a .fa-ico: the whole point is currentColor. */
/* THE BASE RULE. css/style.css:5473 has it for the app; the previews do not load
   style.css, so without this 7 of 12 icons computed display:inline / vertical-align:
   baseline and the phone glyph visibly sagged below its own text. Only the .ico-*
   sizes had been carried across. */
.fa-ico{ width:20px; height:20px; flex:none; display:inline-block; vertical-align:middle }
.fa-ico.ico-sm{ width:14px; height:14px; stroke-width:2.2 }
.fa-ico.ico-md{ width:16px; height:16px; stroke-width:2 }
.fa-ico.ico-lg{ width:20px; height:20px; stroke-width:1.8 }
.fa-ico.ico-xl{ width:28px; height:28px; stroke-width:1.5 }
/* the glyph never grows at 390px — it is sized to the text beside it, and the
   text does not change. The TAP TARGET grows; the icon inside it does not. */

/* ══════════════════════════════════════════════════════════════════════════════
   THE DAY BOARD — My Desk → Team, and the Calendar day view.

   Both draw with the same schedGrid (js/desk.js), so every rule here lands on
   both boards and both were measured. Everything in this section is a REPAIR of
   behaviour that pre-dates Direction C: design-c.css had touched no board rule
   at all before this commit, so none of it is redesign damage.
   ══════════════════════════════════════════════════════════════════════════════ */

/* ── C1 · the board was 30.3px tall at 1024x768, its top edge at y852 in a 768
   viewport — zero of eight lanes on screen, while the Team tab still read
   selected. Three declarations make it, and fixing any ONE of them was already
   tested and fails:
     · css/style.css:4235  .dsk-sheet{min-height:430px}
     · css/style.css:3632  .sch-grid{overflow:hidden}   overflow zeroes a flex
       item's automatic minimum size, so the grid absorbs 100% of any shortfall
       (measured 1:1). min-height:0 alone just moves it to 6px.
     · css/style.css:3733  body.desk-lock main{height:100vh;overflow:hidden}

   The ROOT is the third one. My Desk pins itself to the viewport and releases
   that lock at css/style.css:3758 — but only `@media (max-width:980px)`, on
   WIDTH. 1024x768 is 1024 wide, so the lock held on a screen 768px TALL and the
   sheet was clipped with no scrollbar anywhere in the chain: .dsk-folder
   measured 369.5px tall holding a 509px scrollHeight at overflow:visible.

   A height lock is a good idea on a screen tall enough to hold the desk, so the
   release now keys on height as well. 880px is measured, not guessed: at 900px
   the grid gets 248px with 28px of slack, so 880 clears the point where the
   floor below would start pushing the sheet out of its slot. ── */
@media (max-height:880px){
  html:has(body.desk-lock){ overflow:auto; }
  body.desk-lock{ overflow:auto; }
  body.desk-lock main{ height:auto; overflow:visible; }
  #view-desk.active{ height:auto; overflow:visible; }
}

/* …and when the lock DOES hold — a tall screen with the shelf open, where two
   shelf cards were measured taking the board down to 162px — the sheet gives up
   its 430px floor and scrolls instead of shoving the grid out of the folder.
   This is the half that was tested alone and failed: on its own it takes the
   grid to 6px. It only works with the grid's own floor, below. */
.dsk-sheet{ min-height:0; overflow:auto; }

/* ── C2 · the board scrolled in NEITHER axis, so 227.7px of lanes were
   unreachable even at 1440x900. The two rules that would have allowed it —
   css/style.css:4622 `.board-band .sch-grid{overflow-x:auto}` and :4623
   `.board-day .sch-grid{max-height:none}` — never match on My Desk, because
   there the grid's parent is .dsk-sheet and not .board-band. On the Calendar
   they do match, and still only freed the horizontal axis.

   The grid is now the scroller in both axes, with a floor that stops it being
   squeezed to nothing: 220px is the hour header plus three full lanes. Real
   scrollbars — ::-webkit-scrollbar is already styled at css/style.css:1265, so
   they are visible and draggable rather than overlay ghosts. */
.sch-grid{ overflow:auto; min-height:220px; }
.board-band .sch-grid{ overflow:auto; }        /* (0,2,0), to beat :4622's overflow-x-only */

/* the hour axis is the whole point of a time grid, so it stays put while the
   lanes scroll under it. Sticky resolves against whichever ancestor is actually
   scrolling, so this works in all three regimes: grid-scroll, sheet-scroll and
   (below 880px tall) plain page-scroll. Above .sch-now, which is z-index 5. */
/* AND IT NEEDS A GROUND. Making this sticky without one was a regression of this
   file's own making: css/style.css carried `background:transparent !important` on
   .sch-head, written when the header did not move, so the lanes scrolled straight
   THROUGH it — three blocks overlapping the header by 40.1px, its whole height,
   with the first job of the day reading "08:00 Sar8AM 9AM 10AMh Millns". z-index:6
   was never the problem; the header was already painting on top of nothing.
   The `transparent !important` is now gone from style.css (:4657) rather than
   fought with a fourth !important, and the ground is stated here, beside the
   sticky that needs it, so the two can never drift apart again. --panel2 is the
   value the header's own staff corner already paints (css/style.css:3639), so the
   bar and its corner read as one lid; the hairline is a box-shadow so it cannot
   change the 30px min-height the Direction-C block sets at css/style.css:7330. */
.sch-head{ position:sticky; top:0; z-index:6;
  background:var(--panel2); box-shadow:0 1px 0 var(--line); }
/* and the same for the engineer names once the board scrolls sideways — a lane
   you cannot put a name to is not a lane. .sch-staff is already opaque
   (background:var(--panel2), css/style.css:3639). */
.sch-staff{ position:sticky; left:0; z-index:3; }
.sch-head .sch-staff{ z-index:7; }             /* the corner belongs to both */
/* every lane wide enough to read twelve hours in. css/style.css:4625 already
   does this, but only inside .board-band, so the desk board squeezed 12 columns
   into ~200px at 390px wide. */
.sch-row{ min-width:640px; }

/* ── C4 · overlapping bookings could not be opened. Three of nine blocks failed
   their own centre hit-test at 1440x900, because siblings sharing an hour were
   drawn in the same rectangle at z-index:auto and the later one swallowed the
   earlier one's clicks. A z-index ladder does not fix that — the centre of the
   block underneath is still covered — so overlapping blocks are packed into
   horizontal bands and the lane grows to hold them (--trk / --trks, set by
   schedGrid in js/desk.js).

   56px per band = a 50px block and a 6px gap, on top of the 7px top and bottom
   insets the board already had. ONE band therefore comes to exactly 64px, which
   is the min-height .sch-row already had at css/style.css:4636 — so a lane with
   nothing overlapping is pixel-identical to before this change, and only a lane
   that is genuinely double-booked gets taller. A 50px block also clears the
   34px desktop tap floor with room to spare; splitting a 64px lane in half
   without growing it would have put it at 25px and under.

   ── THE !important HERE IS A DOCUMENTED EXCEPTION, not a habit ──
   DESIGN_SPEC's rule is that source order does the work, and it does for
   min-height below. It cannot for top/bottom: css/style.css:7307 is
   `.sch-block{ top:7px !important; bottom:7px !important }`, and you cannot
   out-specify an !important without one. This was found the hard way — the
   first cut of this rule carried no !important, `height:50px` took (nothing
   else sets height) while `top` silently lost, and every block stayed pinned
   at 7px, so the bands were computed, emitted as --trk, and drawn on top of
   each other exactly as before. The measurement is what caught it. */
.sch-row{ min-height:calc(var(--trks,1) * 56px + 8px); }
.sch-block{ top:calc(7px + var(--trk,0) * 56px) !important; bottom:auto !important; height:50px; }
/* the sticky name column has to clear the now-line, which is z-index:3 at
   css/style.css:7320 and lives in the lane beside it. */
.sch-staff{ z-index:4; }

/* ── the repeating visit's tint and dashed edge NEVER RENDERED. Two rules, both
   (0,1,0), both !important, and the later one wins: css/style.css's old
   `.sch-visit{ background … !important; box-shadow … !important }` pair lost its
   background to the Direction-C `.sch-block{ background:var(--bg2) !important;
   border:1px solid var(--line) !important }` block further down the SAME file, and
   its non-!important dashed border lost to that same border. Measured before this
   change: a repeating visit painted rgb(240,240,236) in light and rgb(13,15,19) in
   dark — byte-identical to an ordinary booking — with a SOLID border. Of the three
   signals it was written to carry (tint, dashed edge, coloured spine) only the
   spine hue survived, so the block was a booking wearing a different stripe.

   Fixed by SPECIFICITY rather than a third !important layer. `.sch-block.sch-visit`
   is (0,2,0) against the block rule's (0,1,0), so it wins on specificity and no
   longer depends on which of the two style.css board blocks comes last — which is
   what killed it in the first place and would kill it again the next time somebody
   appends a .sch-block rule. The !important is still needed, because you cannot
   out-specify an !important without one (DESIGN_SPEC's documented exception, the
   same one .sch-block's top/bottom uses at :530), but it is not what decides this.

   The tint derives from --sc, the colour the block is already handed inline, so
   the fill follows the spine instead of hard-coding a second copy of it, and
   70%-of---sc dashed is the convention .sch-block-visit already uses for an edge
   that is "not quite a booking" (css/style.css:5928).

   THE MIX PARTNER IS --panel AND THAT IS A CONTRAST DECISION, not a copy-paste.
   The first cut of this rule mixed into --bg2, the ground the block is actually
   filled with, which reads as the obvious choice and is wrong: --bg2 is DARKER
   than --panel in the light theme, so tinting into it drove the subline's --muted
   ink from 5.00:1 down to 4.25:1 — under the 4.5:1 floor, in the theme Jamie
   actually uses. Measured from real pixels, because color-mix() reports as
   `color(srgb …)` and reading those as 8-bit channels gives a wrong answer.
   Mixing into --panel instead measures 4.76:1 light and 5.89:1 dark on the same
   subline, while still shifting the block visibly violet — pixel (233,233,252)
   light and (33,35,59) dark against an ordinary booking's (240,240,236) and
   (13,15,19). 15% is about as much tint as the 4.5:1 floor carries here, so treat
   that number as load-bearing and re-measure from pixels if it is raised.
   border-left is RESTATED AFTER the shorthand deliberately: `border:` resets it,
   and that is exactly how the unconfirmed hold at :581 lost the engineer's colour
   spine. Same shorthand, same trap, not repeated here.
   The dead pair at css/style.css:4737 and the dead `.sch-visit{box-shadow:none}`
   demotion at :374 of this file are both deleted with this. */
.sch-block.sch-visit{
  background:color-mix(in srgb, var(--sc,#6d6ae8) 15%, var(--panel)) !important;
  border:1px dashed color-mix(in srgb, var(--sc,#6d6ae8) 70%, transparent) !important;
  border-left:3px solid var(--sc,#6d6ae8) !important;
  box-shadow:0 1px 2px rgba(0,0,0,.14) !important;
}

/* ── C3 · the board had ZERO focusable elements. 160 consecutive Tab presses
   never landed on it, and 15 handler-carrying bare <div>s meant every booking
   on the day was mouse-only. They are <button>s now (js/desk.js,
   js/contracts.js), which needs the browser's button chrome taking back off —
   a button is centred, inherits nothing and draws its own background. */
.sch-block{ display:block; text-align:left; font:inherit; font-size:12px;
  color:inherit; -webkit-appearance:none; appearance:none; }

/* the focus ring is DESIGN_SPEC §7's, unchanged: 2px solid var(--c-in-dot).
   design-c.css:116 already wins it — measured, not assumed: three plain
   :focus-visible rules compete at (0,1,0), css/style.css:1264 (2px --amber) and
   :4429 (3px #0d9cbc) and this file's, and this file loads last.
   The one thing that changes here is the OFFSET. At +2px the ring is drawn
   outside the block, and the board is now a scroll container, so a ring on a
   block at the edge of the visible area is clipped by the container exactly
   when you most need to see where you are. Drawn inside its own box it cannot
   be, and the block is 50px tall so 2px inside costs nothing. */
.sch-block:focus-visible{ outline-offset:-2px; z-index:2; }
/* and the scrolling region itself, which is a tab stop now so the horizontally
   scrolled hours and the Unassigned lane can be reached without a mouse. */
.sch-grid:focus-visible{ outline-offset:-2px; }

/* ── C7 · .sch-block:hover still translated 1px under prefers-reduced-motion:
   reduce (measured 884.578 -> 883.578). Both existing reduced-motion blocks —
   css/style.css:4433 and design-c.css:119 — only zero the transition DURATION,
   which makes the movement instant rather than absent. A person who has asked
   for no motion has asked for the thing not to move, not to move faster. The
   live rule is css/style.css:7312, not :4652; it carries no !important on
   transform, so source order does this one. */
@media (prefers-reduced-motion: reduce){
  .sch-block:hover, .sch-block:focus-visible, .sch-block:active{ transform:none; }
}

/* ── B1 · a board drop onto a clash is kept as a PROPOSAL and not rolled back
   (Jamie, 26 Jul: booking someone on leave deliberately is real work — a
   callout, an emergency). For a job that was already booked that meant the
   block simply moved into the new slot and read exactly like a confirmed one,
   with the only trace a line in a job log nobody opens mid-drag.

   Status is a colour AND a word, so this is both: the word "Unconfirmed", the
   reason underneath it in plain English, and a dashed edge, which is the
   convention the board already uses for a block that is not quite a booking
   (.sch-block-visit, css/style.css:5889). --c-pipe-* is the right pair — this
   is work in the pipeline waiting on a decision, not an error — and the ink is
   used as INK on the tinted ground, never as a fill. */
.sch-block-hold{
  border:1px dashed var(--c-pipe-ink) !important;   /* :7308 sets a solid 1px border !important */
  background:linear-gradient(0deg, var(--c-pipe-bg), var(--c-pipe-bg)), var(--c-panel) !important;
}
/* The word and the reason share the second line, because the first line is the
   booking's identity (time and customer) and must not be pushed about. A block
   is only as wide as the booking is long — a two-hour job on a twelve-hour
   board is about 110px — so the chip is fixed and the reason takes what is
   left and ellipsises. The full sentence is always in the title and in the
   accessible name, which is where a caption that cannot fit belongs.
   Qualified to (0,2,0) on purpose: css/style.css:3645 is
   `.sch-block span{ color:var(--muted) }` at (0,1,1), and an unqualified
   .sch-hold-tag loses to it — measured, the chip came out grey the first time. */
.sch-block-hold .sch-block-sub{ display:flex; align-items:center; gap:var(--c-s1); }
.sch-block .sch-hold-tag{
  flex:none; padding:0 6px;
  border-radius:var(--c-r-pill); background:var(--c-pipe-bg); color:var(--c-pipe-ink);
  font-size:10px; font-weight:700; letter-spacing:.02em; text-transform:none;
}
.sch-block .sch-hold-why{
  min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  color:var(--c-pipe-ink);
}

/* ── C6 · My Desk is deliberately absent on a phone (Jamie, 26 Jul: "i chose
   for my desk not to be shown on mobile as its too busy") and it is NOT coming
   to the phone. The defect was the manner of its absence: the nav offered the
   button at full strength and the tap landed on a dispatcher's console. §8 says
   a control that appears must work, so it now reads as off AND says why — the
   sentence is the point, because a greyed button on its own is colour alone.
   Toggled by deskNavSync() in js/desk.js, off sqIsHandset(), because a desktop
   window dragged narrow is not a phone. */
.nav-btn[aria-disabled="true"]{ opacity:.45; cursor:not-allowed; }
.nav-why{
  margin:0 12px 8px 14px; padding:0;
  font-size:11px; line-height:1.4; color:var(--c-frame-mut);
}
/* the two places the app already hides the My Desk button have to hide its
   reason with it: css/style.css:5194 is .nav-btn-scoped and :6472 keys on
   [data-view], so neither reaches a <p class="nav-why" data-for="desk">. */
body.eng-mode .nav-why[data-for="desk"],
body.hq-org .nav-why[data-for="desk"]{ display:none; }

/* ── v795 · A DESTINATION THIS SESSION DOES NOT HAVE. One rule, and it is the ONLY
   thing in the whole css/ tree that decides whether a nav button is offered — five
   blocks in css/style.css used to, each with its own hand-typed list of data-view
   names, and they had drifted from ENG_VIEWS in both directions at once. The list
   is gone; engNavSync() (js/engineer.js) asks the routing predicate per button and
   toggles this class, so the nav and showView can only ever say the same thing.

   A CLASS, not an inline display, on purpose: three writers already set an inline
   display on .nav-btn (sqApplyEntitlements, its QS-seat wrapper, and the HQ clear).
   Two hiders that compose beat two hiders that race.

   No :not(#solar-studio *) — the solar studio has no .nav-btn in it, and adding the
   guard would lend this rule an ID's worth of specificity for nothing (CLAUDE.md §7).
   It beats style.css:83's .nav-btn{display:flex} on specificity (0,2,0 against 0,1,0)
   as well as on source order, so it needs no !important. */
.nav-btn.nav-off{ display:none; }

/* ── H5 · a return visit now draws in MONTH view too (js/jobs.js calMonthOn).
   The day board has marked one since the js/visits.js wrappers went in —
   .sch-block-visit, css/style.css:5889, a dashed border — and the month grid
   simply did not show return visits at all, so it had nothing to mark. Same
   signal, same reasoning: dashed edge plus the ↩ in the label and "return
   visit" in the tooltip, so it is never colour alone (§7). It rides on the
   pill's own --pc so the crew colour still reads. */
.cal-ev-visit:not(#solar-studio *){
  border-left-style:dashed;
  outline:1px dashed color-mix(in srgb, var(--pc, #9aa1ab) 55%, transparent);
  outline-offset:-1px;
}

/* ══════════════════════════════════════════════════════════════════════════════
   S12 · THE BOOKING SHEET — the frontend-design floor (tool 2, Round 1 G1)
   focus · responsive · type and spacing. Everything here is scoped to
   #booking-modal so it cannot reach the eleven other surfaces that emit .bk-*,
   and nothing here touches the solar studio.

   The sheet itself is still on style.css's legacy tokens and that is scope, not
   a defect — S12 has not had its Direction C pass. What is added here is the
   accessibility and target floor, written in --c-* as §7 requires.
   ══════════════════════════════════════════════════════════════════════════════ */

/* ── the visual boundary of a control, at 3:1 ─────────────────────────────────
   WCAG 1.4.11: the border IS the control, so it is not decoration and the 3:1
   floor applies. Measured on this sheet before the fix: every input, select,
   textarea and the crew search box sat on --line at 1.40:1 light / 1.32:1 dark,
   and in light that is a white field on a white card with a hairline you cannot
   see. --c-line-strong is deliberately 2.09:1 (a structural rule, not a control
   edge) so it is the wrong token here; this is --c-mut held back so a form of
   them does not read as a cage.
   78% and not 70%: at 70% the fields cleared 3:1 on the white card (3.03) but the
   crew tickbox, which sits on the --bg2 row rather than on the panel, came out at
   2.81 and failed. One token has to clear the floor on the DARKEST ground it
   lands on, not the lightest.
   Declared in BOTH theme blocks on purpose: a custom property that references
   var(--c-mut) is substituted where it is DECLARED, so one copy on :root would
   carry the light value into the dark theme. */
:root{ --c-control-line: color-mix(in srgb, var(--c-mut) 78%, transparent); }
html.dark, body:not(.theme-light){ --c-control-line: color-mix(in srgb, var(--c-mut) 78%, transparent); }

#booking-modal .bk-f input,
#booking-modal .bk-f select,
#booking-modal .bk-msg,
#booking-modal .bk-who-find,
#booking-modal .rp-line input,
#booking-modal .rp-line select{ border-color:var(--c-control-line); }

/* the custom tickbox. Unchecked is the only state with no glyph and no fill, so
   until it is ticked the border is the whole control — and it was 1.27:1 light /
   1.29:1 dark, which is invisible. Ticked keeps the yellow fill and the ✓. */
#booking-modal .bk-who:not(.on) .bk-tick{ border-color:var(--c-control-line); }

/* ── the placeholder was never styled anywhere in the booking CSS, so it ran on
   Chrome's UA grey rgb(117,117,117): 4.61:1 in light (a pass) and 4.16:1 in dark
   (a fail). One token fixes both and puts it in the same register as every other
   piece of secondary text on the sheet. */
#booking-modal ::placeholder{ color:var(--c-mut); opacity:1; }

/* ── the travel-clash caption. #c2410c is off palette and lands differently on
   each of the four grounds this row can have (plain/ticked × light/dark): 4.53,
   4.39, 3.70 and 2.57. --c-bad-ink is the token for "this is a problem" and it
   is theme-aware, so all four grounds clear 4.5:1. */
#booking-modal .bk-who-travel{ color:var(--c-bad-ink); }
/* and the row's own clash edge, which was #f0730a at 2.20:1 in light. This is
   the same off-palette orange the day board's .sch-block-sooner ring carried
   until it was moved to --c-bad-dot (css/style.css:7317); same token, same
   reason. It stays a 1px edge — the colour changes, the weight does not. */
#booking-modal .bk-who.is-tight{ border-color:var(--c-bad-dot); }

/* ── VISIBLE KEYBOARD FOCUS ───────────────────────────────────────────────────
   Four rules in style.css set outline:none on this sheet's fields — :7357,
   :7382, :7504, :7559 — and substitute a yellow glow. Measured composited, that
   glow is #fff7cc on #ffffff = 1.08:1 in light on ALL SIX fields, and 1.44:1 in
   dark on four of them. It is not a focus indicator, it is a tint.
   The ring is DESIGN_SPEC §7's, unchanged. The id lifts these to (1,1,1) so they
   beat both .bk-f input:focus (0,2,1) and body.theme-light input:focus (0,2,2),
   without an !important and without disturbing the yellow border, which stays as
   the sheet's own "this field is live" signal. Offset 1px matches the app's
   input convention (style.css:4430) so the ring hugs the field. */
#booking-modal .bk-f input:focus-visible,
#booking-modal .bk-f select:focus-visible,
#booking-modal .bk-msg:focus-visible,
#booking-modal .bk-who-find:focus-visible,
#booking-modal .rp-line input:focus-visible,
#booking-modal .rp-line select:focus-visible{
  outline:2px solid var(--c-in-dot);
  outline-offset:1px;
}

/* The crew tickbox is a real <input type=checkbox> again — it used to carry the
   `hidden` attribute, which is display:none, which is not focusable, which is
   why thirty-one tabbable controls on this sheet included not one engineer row
   (WCAG 2.1.1). It is taken out of the layout rather than out of the document,
   so Space toggles it and a screen reader gets the checked state and the row's
   own words.
   CORRECTED 27 Jul: this used to end by saying the hidden-box treatment belonged
   to the MAIN picker alone, because the return-visit form (visCrewPick,
   js/visits.js) shared .bk-who but drew a real native box with no ✓ to stand in
   for it, so hiding its input would have lost the control. True when written,
   false now —
   visCrewPick emits .bk-tick and .bk-tickbox itself, so the two pickers are one
   pattern. The rules for it are down at the bottom of this file, keyed on
   .bk-staff rather than on #booking-modal, because that component renders on two
   surfaces and only one of them is this modal. Leaving the old sentence here is
   how the next session decides the second picker is somebody else's problem. */
#booking-modal .bk-who{ position:relative; }   /* see the note below — load-bearing */
#booking-modal .bk-who .bk-tickbox{
  position:absolute; width:1px; height:1px; margin:-1px; padding:0;
  overflow:hidden; clip-path:inset(50%); white-space:nowrap; border:0;
}
/* WHY .bk-who HAS TO BE position:relative, measured rather than assumed.
   Without it the tickbox's containing block is .bk-who-dd (css/style.css:7546),
   which sits OUTSIDE .bk-who-opts — and .bk-who-opts is a 260px scroller. So the
   box did not travel with its own row, and tabbing to the tenth engineer left
   .bk-who-opts.scrollTop at 0 with the focused row off screen: measured
   lastRowInView false, scrolled false. Anchoring it to its row fixes the scroll
   as a side effect of fixing the geometry. */
/* …and because the box itself is 1px, the ring has to be drawn on the thing the
   eye is actually looking at. :has() is already the idiom here (nine uses in
   style.css, one in this file). */
#booking-modal .bk-who:has(.bk-tickbox:focus-visible){
  outline:2px solid var(--c-in-dot);
  outline-offset:2px;
}

/* ── SCREEN-READER-ONLY, for the conflicts live region. Prefixed: `sr-only` is
   not in this tree and a bare one is exactly the kind of short name style.css
   already owns a lot of (§7). */
.c-sr-only{
  position:absolute; width:1px; height:1px; margin:-1px; padding:0;
  overflow:hidden; clip-path:inset(50%); white-space:nowrap; border:0;
}

/* ── RESPONSIVE ───────────────────────────────────────────────────────────────
   The field app's bottom tab bar is position:fixed at z-index 900
   (css/style.css:5604) and the modal layer is 100 (:385), so on a handset the
   tab bar sits ON TOP of the open sheet — measured at 390×844 it covered the
   bottom 22px of the card and elementFromPoint at its centre returned
   BUTTON.fa-tab, i.e. it was still the thing that got the tap.
   The app already knows how to do this for the sidebar — body.menu-open
   #fa-tabbar{display:none}, :5635 — but `menu-open` is the SIDEBAR's state and
   is cleared by closeMenu(), so borrowing it would hand the tab bar back the
   moment anyone opened and shut the menu behind the sheet. Keying on the modal
   actually being open cannot go out of sync, needs no JS, and fixes the same
   defect for every other modal on the handset at the same time. */
body:has(.modal.open) #fa-tabbar{ display:none; }
/* v819 — THE SAME DEFECT, ONE RUNG DOWN THE SAME LADDER, and the pass above missed
   it. .fc-footbar is the phone certificate editor's own footbar and it is z-index
   890 (css/style.css) against .modal's 100, so with a certificate preview open it
   sits ACROSS the bottom of the modal and takes the taps meant for it. Measured at
   390 with the preview open: elementFromPoint at the centre of "‹ Result" and of
   "Send to the QS" returned those two buttons — both belonging to the page
   underneath, and one of them is a send. Requested by the certificate-batons lane,
   which owns css/style.css and deliberately did not put it there: the rule belongs
   beside its #fa-tabbar sibling, and splitting the pair across two stylesheets is
   how the next person finds one and not the other.
   SPECIFICITY, because :has() takes the specificity of its most specific ARGUMENT
   and that has already cost this project a wrong measurement: body (0,0,1) +
   :has(.modal.open) (0,2,0) + .fc-footbar (0,1,0) = (0,3,1), against
   css/style.css:2940's .fc-footbar{display:flex} at (0,1,0). Same property, so they
   really do compete, and this one wins on specificity alone — source order is only
   the second reason. .fc-footbar is written into a view host by js/facerts.js, NOT
   inside a .modal, so this can never hide it permanently. */
body:has(.modal.open) .fc-footbar{ display:none; }

/* A DEAD RULE, found while measuring the phone. css/style.css:138 sets
   .modal{padding:10px} inside @media (max-width:860px) — and :385 sets
   .modal{padding:24px} with no media query, at the same specificity, LATER in
   the same file. Later wins, so the phone rule has never once taken effect:
   measured at 390×844, computed padding 24px with the 860px query matching.
   The cost is 28px of backdrop where the sheet should be — the card is
   width:min(840px,94vw) and comes out 342px instead of 366.6px.
   Fixed here for THIS sheet only. Correcting :385 itself would widen all fifteen
   modals on every phone in the product, which is a tree-wide change and not a
   page audit; it is written up as a follow-up in the tool 2 report. */
@media (max-width:860px){
  #booking-modal{ padding:10px; }
}

/* The sheet's own comment promises "fits one screen, no scrolling"
   (css/style.css:7473). It did not: at 1440×900 with the repeat card and the
   crew picker open the card measured 1084.6px tall and "Book it in" sat 140px
   below the fold, with the whole modal — title bar, Close and all — scrolling
   as one. The narrow branch already caps the card at 88vh and scrolls it
   (:7487); this does the same at every width, and puts the scroll on the BODY
   rather than the card so the sheet's title and its Close button stay put.
   .bk-actions is deliberately NOT pinned: the return-visit block is appended
   after it (js/visits.js:490), so a pinned footer would push a live control
   below itself. The comment in style.css has been corrected to say so.

   The cap is a PERCENTAGE on purpose, not a vh sum. .modal's padding is 24px
   normally and 10px under 860px (css/style.css:138), and .modal-card carries a
   4vh top margin (:391); a hand-rolled `calc(96vh - 48px)` got the desktop right
   and overshot the 768 tablet by 4px because it had the wrong padding baked in.
   100% here resolves against .modal's own content box — viewport minus whatever
   its padding happens to be — so subtracting the card's own 4vh margin lands the
   bottom edge exactly on the padding at every width, with nothing to keep in
   step. */
#booking-modal .modal-card{
  max-height:calc(100% - 4vh);
  overflow:hidden;                 /* the body scrolls, not the frame */
}
/* Making the toolbar permanent is a cost as well as a fix, and on a phone it was
   a real one. .modal-toolbar is flex-wrap:wrap (css/style.css:416), so at 390px
   "Book J-1001 — Margaret Ackroyd" pushed Close onto a row of its own: a 110px
   header that used to scroll away and now never does. Keeping them on one row
   and letting the TITLE wrap inside its own box gives the sheet that row back
   without shortening or truncating the customer's name — nothing is lost, it is
   just laid out the other way round. */
#booking-modal .modal-toolbar{ flex-wrap:nowrap; align-items:flex-start; }
#booking-modal #bk-title{ min-width:0; }
#booking-modal .modal-toolbar > .btn{ flex:none; }

#booking-modal #bk-body{
  min-height:0;
  overflow-y:auto;
  overflow-x:hidden;
  /* the crew dropdown is position:absolute inside this box; a little room under
     it means the panel is not clipped the instant it opens */
  scrollbar-gutter:stable;
}
/* css/style.css:7487 puts max-height:88vh AND overflow-y:auto on .bk-card-shell
   under 820px, which is the old "the whole card scrolls, toolbar and all"
   behaviour. Both are superseded by the two rules above; the id lifts them to
   (1,1,0) so they win without an !important, and this is written down because a
   dead-looking rule left in style.css is how the next session re-derives it. */
@media (max-width:820px){
  #booking-modal .bk-card-shell{ max-height:calc(100% - 4vh); overflow:hidden; }
}

/* ── TAP TARGETS · 34px desktop, 44px phone, no exceptions on the phone ───────
   Measured rendered rectangles, not computed styles. Desktop failures were the
   Repeat fold header at 18.6px (padding:0, no min-height, css/style.css:7524)
   and .btn.small at 30px (:158). The phone floor missed eleven more.
   These are all scoped to #booking-modal ON PURPOSE. Raising the global
   .btn phone bump from 40px to 44px (css/style.css:4348) would move every
   button in the product — .btn alone is emitted 663 times — and that is a
   tree-wide change, not a page audit. Logged as a follow-up. */
#booking-modal .bk-fold-h{ min-height:var(--c-tap); }
#booking-modal .btn.small{
  min-height:var(--c-tap);
  display:inline-flex; align-items:center; justify-content:center; gap:6px;
}
#booking-modal .bk-tpl-row{ min-height:var(--c-tap); }

@media (pointer:coarse), (max-width:820px){
  #booking-modal .btn,
  #booking-modal .btn.small,
  #booking-modal .bk-fold-h,
  #booking-modal .bk-seg-b,
  #booking-modal .rp-chip,
  #booking-modal .bk-who-find,
  #booking-modal .rp-line input,
  #booking-modal .rp-line select,
  #booking-modal .bk-tpl-row{ min-height:var(--c-tap-touch); }
  /* the M T W T F S S picker. It already wraps to two rows at 390px at 34px, so
     44px costs a column, not a row of legibility. */
  #booking-modal .rp-day{ width:var(--c-tap-touch); height:var(--c-tap-touch); }
  /* …and the primary keeps the extra weight it was given. #booking-modal .btn is
     (1,1,0) and .bk-go is (0,1,0), so the 44px floor above SHRANK the one control
     on the sheet that was already comfortably over it — measured 48 -> 44. A
     floor is a floor, not a ceiling. */
  #booking-modal .bk-go{ min-height:48px; }
}

/* ══════════════════════════════════════════════════════════════════════════════
   THE MONTH GRID'S BOOKING PILL — readable at 1440, and a real day list at 390
   27 Jul. Ordered by Jamie: the pills truncate mid-customer-name on the desktop
   and are unusable on the phone.

   The live month renderer is renderCalendar in js/jobs.js, kept alive by
   `const _renderCalMonth = renderCalendar` in js/scheduler.js — a by-value
   capture taken 432 lines before the name is reassigned, which is CLAUDE.md §1's
   own worked example. The markup these rules dress is in js/jobs.js, not in the
   scheduler's renderCalendar.

   WHAT WAS MEASURED, before (rendered rectangles, .cal-ev, LIGHT theme first,
   every ratio taken with the background composited):
     1440  cell 156x86 · pill 144x15.8 · 135px of text room. The clipped end was
           always the customer, and the customer was already only their LAST
           WORD — "Marchmont & Fenwick Ltd" drew as "Ltd" and
           "Featherstonehaugh-Wallington Property Group" as "Group". A return
           visit wanted 191px in 143 and an all-day booking 182px in 143.
      390  cell 44.8x64 · pill 36.9x15.8 · 28px of text room, holding 108-191px
           of text. Every pill on the month. One read "● …".
     tap   15.8px against a 34px desktop / 44px phone floor, at every width, and
           no pill was reachable by keyboard at all.
     ink   .cal-ev-soft ran opacity:.6 over a 22% tint and measured 4.40:1 in
           LIGHT on Sam's yellow — under the 4.5:1 floor, on every quoting pill
           and every leave pill. Dark passed; light is the one that failed,
           which is why light is measured first.

   WHY THE PHONE IS A LIST AND NOT A GRID. Seven columns at 390px leave a pill
   35px of text even after clawing back every pixel of padding and gap. Measured
   in the app's own face at 11px: the DEFAULT job number J0148 is 30.8px, the WYE
   prefix Settings itself suggests is 48.7px, and the longest Settings allows
   (8-char prefix + 4 pad) is 86.3px. A seven-column month grid physically cannot
   carry a job number on a phone, and Jamie's rule is that the number is visible
   at all times. So below 720px the SAME cells re-lay as one day per row: the
   pill gets ~233px and the whole customer name fits, unabbreviated.

   Nothing is emitted only to be hidden, and nothing moved off-screen: the crew
   names, the day-of-span and the return-visit label are in the title as before,
   and the status is now on the pill as a word instead of tooltip-only.
   ══════════════════════════════════════════════════════════════════════════════ */

/* the pill is a two-line block, and a real control */
.cal-ev:not(#solar-studio *){
  display:flex; flex-direction:column; justify-content:center; gap:1px;
  min-height:var(--c-tap);
  padding:3px 6px;
  white-space:normal;                 /* the inner spans own the ellipsis now */
  overflow:hidden;
  border-radius:var(--c-r-chip);
  color:var(--c-ink);
  /* 14%, not style.css's 22%. The tint is the ground every secondary word in the
     pill is read against, and at 22% --c-mut fell to 3.85:1 on Sam's yellow in
     the dark theme. 14% clears 4.5:1 on all three staff colours in both themes,
     composited — the crew signal lives in the dot and the hairline, which are
     full strength, so the wash does not have to carry it. */
  background:color-mix(in srgb, var(--pc, var(--c-sys-dot)) 14%, var(--c-panel));
}
.cal-ev:not(#solar-studio *):hover{
  background:color-mix(in srgb, var(--pc, var(--c-sys-dot)) 24%, var(--c-panel));
}
.cal-ev:not(#solar-studio *):focus-visible{
  outline:2px solid var(--c-in-dot); outline-offset:2px;
}
/* not won yet. This was opacity:.6, which took the TEXT down with the tint and
   measured 4.40:1 composited in light — under the floor. It is a lighter GROUND
   now, so the ink stays at full strength: 17.3:1 light, 13.7:1 dark. The dashed
   left hairline from css/style.css:770 is untouched, so the not-won signal is
   still a shape as well as a weight, and nothing is lost. */
.cal-ev-soft:not(#solar-studio *){
  opacity:1;
  background:color-mix(in srgb, var(--pc, var(--c-sys-dot)) 8%, var(--c-panel));
}
.cal-ev-soft:not(#solar-studio *):hover{
  background:color-mix(in srgb, var(--pc, var(--c-sys-dot)) 18%, var(--c-panel));
}

.cal-ev-line:not(#solar-studio *){
  display:flex; align-items:center; gap:5px; min-width:0; line-height:1.25;
}
/* the crew colour, full strength, where the 14% wash cannot say it */
.cal-ev-dot:not(#solar-studio *){
  flex:0 0 auto; width:6px; height:6px; border-radius:50%;
  background:var(--pc, var(--c-sys-dot));
  box-shadow:inset 0 0 0 1px rgba(13,15,19,.28);
}
/* JAMIE'S RULE, AS A FLEX RULE. flex:0 0 auto and no min-width means the job
   number is the one thing in the pill that cannot be shrunk or ellipsised: the
   customer gives way first, then the status. It is also first in reading order. */
.cal-ev-no:not(#solar-studio *){
  flex:0 0 auto; font-weight:700; color:var(--c-ink); white-space:nowrap;
  font-variant-numeric:tabular-nums;
}
.cal-ev-when:not(#solar-studio *){
  flex:0 0 auto; margin-left:auto; white-space:nowrap;
  font-variant-numeric:tabular-nums;
  /* --c-mut alone measures 3.85:1 on a yellow-crewed pill in dark. Mixed toward
     the ink it is 5.9:1 there and 8.7:1 in light, and still reads as secondary. */
  color:color-mix(in srgb, var(--c-mut) 55%, var(--c-ink));
}
/* THE STATUS WORD IS OFF THE DESKTOP GRID, ON PURPOSE, AND MEASURED.
   A 1440px cell gives the pill 135px. The first cut put the status on line one
   and it rendered "Accept…", "Compl…" and — on a job whose number used the
   longest prefix Settings allows — "I." for Invoiced. On line two it costs the
   customer 48px, and the customer being unreadable is the defect being fixed.
   So: it is in the title at every width, and it is shown in full in the ≤720px
   day list below, where a pill is 222-250px and the whole customer name still
   fits beside it. The pill's colour no longer encodes status at all (js/jobs.js
   sends a neutral --pc when nobody is on the booking), so nothing here is a
   status told by colour alone. */
.cal-ev-st:not(#solar-studio *){
  display:none;
  flex:0 0 auto; margin-left:8px; white-space:nowrap;
  color:color-mix(in srgb, var(--c-mut) 55%, var(--c-ink));
}
.cal-ev-cust:not(#solar-studio *){
  flex:1 1 auto; min-width:0;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  color:var(--c-ink);
}
/* the leave pill has no second line and is not a control — it keeps its own
   height rather than padding a name out to a 34px tap target it cannot use */
.cal-leave:not(#solar-studio *){ min-height:0; cursor:default; }

/* an out-of-month day. This was opacity:.38 on the whole cell, which dragged any
   booking sitting in the leading or trailing week down with it — a real pill at
   38% is unreadable in either theme. The day is recessed by GROUND now and the
   work on it is drawn at full strength. */
.cal-cell.cal-out:not(#solar-studio *){ opacity:1; background:var(--c-surface); }
.cal-out .cal-daynum:not(#solar-studio *){ color:var(--c-mut); }

/* the weekday only exists for the phone list; the .cal-dow header row says it
   once per column on the grid */
.cal-daydow:not(#solar-studio *){ display:none; }

/* the phone floor. 44px, no exceptions (DESIGN_SPEC §3). */
@media (pointer:coarse), (max-width:820px){
  .cal-ev:not(#solar-studio *){ min-height:var(--c-tap-touch); }
  .cal-leave:not(#solar-studio *){ min-height:0; }
}

/* ── ≤720px · THE SAME CELLS, ONE DAY PER ROW ────────────────────────────────
   No extra markup and no new control: the grid goes to a single column and each
   .cal-cell becomes [weekday + date] | [its pills], so a pill has ~233px and the
   longest customer name in the fixture (229px) fits whole. The out-of-month days
   that carry no work are the only thing dropped, and they carry no information a
   list needs — the in-month blanks stay, because "which days am I free" is half
   of what a month is for. */
@media (max-width:720px){
  .cal-dow:not(#solar-studio *){ display:none; }
  .cal-grid:not(#solar-studio *){ grid-template-columns:minmax(0,1fr); gap:4px; }
  .cal-cell:not(#solar-studio *){
    min-height:0; padding:6px 8px;
    display:grid; grid-template-columns:56px minmax(0,1fr);
    gap:4px 8px; align-items:start;
  }
  .cal-daynum:not(#solar-studio *){
    grid-column:1; margin:0; padding-top:5px;
    display:flex; gap:5px; align-items:baseline;
  }
  .cal-daydow:not(#solar-studio *){ display:inline; }
  .cal-ev:not(#solar-studio *){ grid-column:2; margin-bottom:0; }
  .cal-cell.cal-out:not(:has(.cal-ev)):not(#solar-studio *){ display:none; }
  /* the status fits again once a pill is 233px wide */
  .cal-ev-st:not(#solar-studio *){ display:block; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   THE RETURN-VISIT CREW ROWS — the same pattern as the main picker, 27 Jul
   (S12 fix window, item 5; tool 3 §2.10)

   `visCrewPick` (js/visits.js) renders TEN MORE .bk-who rows inside
   #booking-modal, in the return-visit form, and ten more again in the field app's
   own version of that form. Tool 2 rebuilt the main picker's row and scoped every
   rule of it to #booking-modal, which was right for the main picker and reaches
   only half of this one.

   So these are tool 2's three rules again, keyed on .bk-staff — the wrapper
   visCrewPick emits — so the component carries its own behaviour to both
   surfaces. Everything is gated on .bk-tickbox as well, because the self-booking
   crew list (js/bookcomms.js) also emits .bk-staff .bk-who and its input is a
   plain visible native box with no ✓ to stand in for it: hiding THAT would lose
   the control, which is exactly what tool 2's note warned about.

   Not scoped :not(#solar-studio *): the studio has no .bk-staff and no .bk-who,
   and cannot acquire one — it is an SVG panel editor. Same reasoning tool 2's
   #booking-modal block runs on. */
.bk-staff .bk-who:has(.bk-tickbox){ position:relative; }
.bk-staff .bk-who .bk-tickbox{
  position:absolute; width:1px; height:1px; margin:-1px; padding:0;
  overflow:hidden; clip-path:inset(50%); white-space:nowrap; border:0;
  /* min-height:0 is not boilerplate. `.fa-visform input{ min-height:40px }`
     (css/style.css) is a different PROPERTY, so `height:1px` does not beat it and
     the field app's box measured 1×53 instead of 1×1 — clipped and invisible, but
     a 53px absolutely-positioned strip all the same. Measured, not assumed. */
  min-height:0;
}
/* the box is 1px, so the ring goes on the row the eye is on — and .bk-staff is a
   186px scroller (css/style.css), which is the same geometry trap tool 2 measured
   on .bk-who-opts: without position:relative on the row the box anchors outside
   the scroller and tabbing to the tenth engineer never scrolls to them. */
.bk-staff .bk-who:has(.bk-tickbox:focus-visible){
  outline:2px solid var(--c-in-dot);
  outline-offset:2px;
}
/* and the same control edge, for the same reason: 1.27:1 is not a border. */
.bk-staff .bk-who:not(.on) .bk-tick{ border-color:var(--c-control-line); }

/* ═══════════════════════════════════════════════════════════════════════════
   THE SELF-BOOKING SHEET (#sb-modal, js/bookcomms.js sbModal) — G2 tool 2, 29 Jul

   IT WAS RENDERING BEHIND THE SHEET THAT OPENS IT, AND THAT IS NOT AN a11y
   NICETY — IT IS A DEAD CONTROL. Measured, at 1440x900, in the real app:
   `.sbm` is z-index 92 and `.ld-pop-scrim` is 90 (css/style.css, v243), while
   `.modal` has been z-index 100 since v73 (css/style.css). #booking-modal is a
   position:fixed full-viewport overlay, so with the booking sheet open,
   elementFromPoint at the centre of "Text them the link" returned an element
   inside #booking-modal, NOT inside #sb-modal — hitInSbModal false,
   hitInBookingModal true. From js/scheduler.js's "Text them a booking link",
   which is the dispatcher's route to the whole feature, the office clicks the
   button and nothing appears to happen.

   It only reproduces from that one entry point. Opened from Customers or My Desk
   with no modal up, the same probe returned hitIsSendButton true and the sheet
   works — which is why this survived since v243.

   PRE-EXISTING, NOT THE REDESIGN: both z-indexes are identical on `main`.

   120/121 rather than 101/102: it clears .modal (100) with room, and stays under
   #ops-drawer (130) and #faq-screen (1100), which are full-screen surfaces that
   should still cover this sheet.

   Scoped to #sb-modal because .ld-pop-scrim is shared with the leads pop-out
   (css/style.css), which must keep sitting UNDER a modal. */
#sb-modal .ld-pop-scrim{ z-index:120; }
#sb-modal .sbm{
  z-index:121;
  /* AND IT WAS CLIPPED. With six engineers on a 1440x520 viewport the sheet
     measured 612px tall at top:-46px, bottom 566 — the title and the ✕ off the
     top, "Text them the link" off the bottom, and position:fixed means document
     scroll reaches neither. overflow-y was `visible` on both the sheet and its
     body. The head is pinned and the BODY scrolls, so the ✕ is never the thing
     that goes off screen. */
  max-height:min(88vh,760px);
  display:flex;
  flex-direction:column;
}
#sb-modal .ld-pop-head{ flex:0 0 auto; }
/* min-height:0 is load-bearing on a flex child that scrolls — without it the
   body refuses to shrink below its content and the cap above does nothing. */
#sb-modal .sbm-body{ overflow-y:auto; min-height:0; }
/* the ✕ measured 26x26 against the 34px desktop floor (§7). `.icon-btn` is
   min-width:26px;height:26px app-wide in css/style.css, so this is scoped to
   this sheet rather than moved globally — the global is named in the report. */
#sb-modal .ld-pop-head .icon-btn{ min-width:34px; height:34px; }
@media (pointer:coarse), (max-width:820px){
  #sb-modal .ld-pop-head .icon-btn{ min-width:44px; height:44px; }
}

/* THE CREW ROW'S FOCUS RING — and NOT by renaming the class.
   sbModal's input is `.sb-crew-ck`, which matches no rule anywhere (grepped:
   two hits, both in js/bookcomms.js, zero in css/), so it draws the browser's
   default ring where the booking sheet draws a 2px --c-in-dot one. Measured on
   the real sheet: outline-style `none` on the row.

   RENAMING IT TO .bk-tickbox WOULD BREAK THE CONTROL and must never be done —
   the block directly above clips anything matching .bk-staff .bk-who .bk-tickbox
   to 1x1 with clip-path, which is safe for visCrewPick only because that
   component also emits a .bk-tick ✓ to stand in for the hidden box. sbModal
   emits no tick. The gating is deliberate and is documented at the top of that
   block; this rule is the other half of it.

   No position:relative needed here, unlike the .bk-tickbox case: this checkbox
   is a real in-flow focusable box, so .bk-staff's 186px scroller scrolls to it
   natively and the ring has a laid-out element to hang off. */
.bk-staff .bk-who:has(.sb-crew-ck:focus-visible){
  outline:2px solid var(--c-in-dot);
  outline-offset:2px;
}
/* and now that .sbm-body scrolls, the crew list's own 186px cap
   (`.bk-staff{max-height:186px;overflow-y:auto}`, css/style.css) is a SECOND
   nested scroller inside it — which is what cut the sixth engineer off
   mid-height, on a sheet whose entire safety property is "free for EVERYONE
   ticked". One scroller, and the whole crew is readable. The cap is left alone
   everywhere else: .bk-staff is shared with visCrewPick in #booking-modal and in
   the field app, and neither of those has a scrolling body to hand the job to. */
#sb-modal .bk-staff{ max-height:none; }

/* ══════════════════════════════════════════════════════════════════════════════
   §7 TAP TARGETS · .icon-btn, app-wide — 34px desktop, 44px phone
   ══════════════════════════════════════════════════════════════════════════════
   144 uses across 34 files. The block above scoped its ✕ fix to #sb-modal and
   flagged the global; this is the global.

   WHAT WAS ACTUALLY THERE, measured in the browser rather than read off the
   source, because the source alone gets this wrong:

     desktop 1440   26 x 26   against a 34px floor
     phone   390    26 x 40   against a 44px floor
     engineer 390   44 x 44   ALREADY COMPLIANT, and left byte-identical

   The phone number is the interesting one. `css/style.css` has carried
   `@media (pointer:coarse),(max-width:820px){ .icon-btn{ min-height:40px;
   min-width:40px } }` since the v4xx tap-target pass — but the v423 control
   system re-declares plain `.icon-btn{ min-width:26px; height:26px }` SIX
   HUNDRED LINES FURTHER DOWN the same file, at equal specificity. Later wins,
   so `min-width:40px` has never once applied: getComputedStyle at 390px returns
   min-width 26px with that media query matching. Only `min-height` survived,
   and only because the base declares `height`, not `min-height`. Half of that
   fix has been silently dead ever since, which is exactly the failure mode a
   stylesheet this size produces and no gate could see.

   Engineer mode escaped it because `body.eng-mode .icon-btn` is (0,2,0) and
   beats the base on specificity rather than on order. Verified: 44x44 before
   this rule and 44x44 after it.

   WHY THE BOX GROWS RATHER THAN A PSEUDO-ELEMENT HIT AREA. There is no visual
   box to protect: `.icon-btn` is `background:none;border:none` at rest, so the
   only thing a customer sees is the glyph, and the glyph is sized by font-size,
   which nothing here touches. A ::after hit area was measured and rejected on
   evidence — adjacent `.dt-actions` buttons sit 3.7px apart, so at the 44px
   phone floor two hit areas would overlap by 14.3px and each button would steal
   5.3px of its neighbour's VISIBLE face. On a row whose buttons include Delete,
   that trades a small target for a mis-tap. It would also leave the hover tint
   at 26px while the hit area was 44px, i.e. a control that responds four pixels
   outside where it lights up.

   min-* and never width/height, so anything that sets its own size still can.

   THE FIVE EXCLUSIONS ARE MEASURED, NOT ASSUMED. .cp-wrap/.jd-ph/.lm-ph/.mn-ph/
   .be-ph .icon-btn are the ✕ badges absolutely positioned ON a photo thumbnail;
   `css/style.css` already opts them out (`min-width:0;height:auto`) but that is
   (0,2,0) and this selector is (1,1,0), so it would win and they would go with
   it. Driven at 390px against the real markup those files emit: without the
   exclusions each badge jumped 14.5-26px -> 44px and its left edge moved 30px,
   covering most of a 64px thumbnail and closing on the next thumbnail's own
   badge. With them, all five are byte-identical before and after. They stay
   small and are named in the report.

   Solar is excluded as every rule here is: hit-testing there lives in `fill`
   and there are no gates. */
.icon-btn:not(#solar-studio *):not(.cp-wrap *):not(.jd-ph *):not(.lm-ph *):not(.mn-ph *):not(.be-ph *){
  min-width:34px;
  min-height:34px;
}
@media (pointer:coarse), (max-width:820px){
  .icon-btn:not(#solar-studio *):not(.cp-wrap *):not(.jd-ph *):not(.lm-ph *):not(.mn-ph *):not(.be-ph *){
    min-width:44px;
    min-height:44px;
  }
}

/* ── v722: the sidebar footer's three controls were all under the tap floor ──
   DESIGN_SPEC / CLAUDE.md §7: 34px desktop, 44px phone. Measured at 1440 with
   the real markup, all three came out 30.0px x 178.0px — "Engineer app",
   "Light / dark" and the new "Privacy notice" link. The two buttons were
   already short (Bucket 2, pre-existing); the link is new, and a new control
   must not be shipped matching a floor its neighbours are already under.
   Fixed for the row rather than for the one I added, because a 34px link
   between two 30px buttons reads as a mistake.
   Cause: `.btn` at css/style.css:151 sets padding 8px 14px and `font:inherit`,
   and `.sidebar-foot` (:92) is font-size:11px — so the whole control collapses
   to 11px of text plus 16px of padding. Height only; padding, colour and the
   11px type are left exactly as they were.
   The anchor also needs display/centring the two <button> siblings get for
   free, and that is inline on the element beside its siblings' own inline
   width rule.
   Scoped to .sidebar-foot, which cannot reach #solar-studio, so no exclusion
   is required — every rule here that COULD reach it still carries one. */
.sidebar-foot .btn{ min-height:var(--c-tap); display:flex; align-items:center; justify-content:center; }
@media (pointer:coarse), (max-width:820px){
  .sidebar-foot .btn{ min-height:var(--c-tap-touch); }
}

/* ══════════════════════════════════════════════════════════════════════════════
   §7 · G4 — THE JOB CARD'S BOOKING READOUTS (Overview strip + Bookings tab)
   ══════════════════════════════════════════════════════════════════════════════
   Three fixes, all pre-existing (Bucket 2). Every number below is a COMPOSITED
   measurement of a rendered rectangle at 1440x900, 768x1024 and 390x844, light
   measured first because light is what Jamie looks at.

   A WARNING FOR WHOEVER MEASURES THIS NEXT. The dark hairline is
   `--line:rgba(154,161,171,.17)` (css/style.css:1283) — it has ALPHA. Read the
   computed borderColor and score it as if it were opaque and you get 6.94:1 and
   a clean bill of health. Composited over its own ground it is 1.32:1. My first
   pass made exactly that mistake and had to be thrown away.

   ── 1 · THE STATUS DOT, AND WHY NO PALETTE EDIT COULD EVER FIX IT ───────────
   `.hb-dot` is 7x7px with a bare `background`, and in the light theme it sits on
   #ffffff. Composited:

       booked job   #34d399  1.92:1   <- the engineer's own colour
       survey       #e0ac00  2.09:1
       phone call   #12b7d8  2.39:1
       not booked   #e24b4a  3.93:1   (the only branch that passes)

   The floor for a non-text indicator is 3:1 (WCAG 1.4.11).

   The booked-job branch is the one that cannot be fixed by picking a better
   hex, and this is the whole reason the fix is a ring. js/customers.js:715 fills
   that dot with `staffColor(eng)`, and `staffColor` (js/staff.js:27) returns
   `s.color` — a value the USER cycles through STAFF_COLORS with a click on the
   Team page (js/staff.js:209). I drove all eight against the light strip:

       #34d399 1.92   #60a5fa 2.54   #f97316 2.80   #e879f9 2.46
       #ffdd33 1.34   #3fe3ff 1.54   #f87171 2.77   #a3e635 1.51

   EVERY ONE FAILS. Only the hard-coded `#1fa855` fallback — the colour shown
   when no engineer is on the job — reaches 3.09. So the dot's contrast is
   whatever a user picked on another page, and no edit to this file can bound it.
   A ring can: it is independent of the fill, and it costs no layout because a
   box-shadow does not reflow.

   #8a8779 is 3.61:1 on #ffffff. It is not a new colour — it is the same value
   the customer-facing survey and booking pages already draw their edges in, so
   the light theme keeps one line colour rather than gaining a second.

   Dark is deliberately left alone: every fill there measures 4.60-9.41:1
   against #14161c, so a ring would be a halo solving nothing. Verified across
   all eight palette colours in dark as well, not assumed.

   §7's OTHER half — "a colour AND a word, never colour alone" — was already
   satisfied and I checked all four branches rather than trusting it: "Not booked
   in yet", "Phone call booked", "Survey / quote visit booked", "Booked —". The
   status is in words every time, so no js change was needed for that rule.

   Scoped away from solar as every global selector in this file is. */
body.theme-light .hb-dot:not(#solar-studio *){ box-shadow:0 0 0 1px #8a8779; }

/* ── 2 · .ov-booked WAS MISSED BY THE LIGHT-THEME BORDER UPGRADE ────────────
   css/style.css:8055-8058 lifts structural card edges from #d8d7cc (1.45:1) to
   #b5b4a6 (2.09:1) and names four: .job-pane, .ov-msgs, .job-hist, #jc-tasks.
   `.ov-booked` is an equally structural card — a bordered strip whose background
   composites to #ffffff, IDENTICAL to the .job-pane it sits on, so the 1px
   border is the only thing separating them — and it is not in that list.
   Measured 1.45:1 before, on every one of the four branches.

   This joins the family rather than out-running it. #b5b4a6 is 2.09:1, which
   that block's own comment is careful to say is NOT WCAG-compliant and is a
   measured facelift compromise; raising the whole family to 3:1 is a design call
   that block already made, and making one strip the exception would be the drift
   §7 warns about. The rule lives here rather than in that list because
   css/style.css is not this pass's file; design-c.css loads after it, so the
   same (0,2,0) specificity wins on source order without an !important. */
body.theme-light .ov-booked{ border-color:#b5b4a6; }

/* ── 3 · TAP TARGETS ON THE TWO PANES · 34px desktop, 44px touch ───────────
   Rendered rectangles: every button on both panes measured 30.0px tall at 1440
   against the 34px floor, and 40.0px at 768 against the 44px floor. Seven
   controls — Deep dive, Bookings →, Book it in →, Let them book it, Move it —
   open the board, Booking options, and the three Tell-the-customer buttons.
   Cause: `.btn.small{height:30px}` (css/style.css:4991) with no desktop lift,
   and the touch query at :4347 stopping at 40px.

   WHY THIS IS SCOPED AND THE .icon-btn PASS WAS NOT. I was pointed at
   audit/page-specs/floors-survey-iconbtn.md and told to reuse its approach
   rather than invent a second one. Its approach — grow the box, never a
   pseudo-element hit area — I have kept, and for a stronger reason than it had:
   it argued there was no visual box to protect because `.icon-btn` is
   background:none, whereas `.btn.small` HAS a painted face, so a hit area
   outside that face would be a control responding where it does not light up.
   Growing the box is the only option that keeps face and target the same shape.

   Its SCOPE does not transfer, and that is not my call to make — a peer already
   made it and wrote it down 60 lines above this block: raising the global floor
   "would move every button in the product — .btn alone is emitted 663 times —
   and that is a tree-wide change, not a page audit. Logged as a follow-up."
   That reasoning holds here and the numbers make it stronger. css/style.css:4980
   documents Tier 2 as THREE classes at 30px — .btn.small (448 uses), .chip-btn
   (85) and .mini-sel (19) — whose stated purpose is "everything on a row lines
   up, everywhere". Lifting .btn.small alone would put a 34px button beside a
   30px pill and break that contract app-wide; lifting all three is 552 call
   sites and a floors pass of its own. `.icon-btn` was safe to do globally
   because it is a one-class tier with no painted box and no alignment contract.

   So: this pane, the same idiom, the same tokens. The global Tier-2 lift is
   named again in the report — this is now the FOURTH surface to hit it
   (#booking-modal, .sidebar-foot, #sb-modal, here), which is the argument for a
   dedicated pass rather than a fifth scoped patch.

   .ov-pane and .jd-book cannot contain #solar-studio, so no exclusion is needed;
   every rule here that COULD reach it still carries one.

   WHY THE TOUCH QUERY IS `(pointer:coarse)` ALONE, and not the house pairing
   `(pointer:coarse),(max-width:820px)` used everywhere else in this file.

   Two reasons, and the honest one is second.

   ON MERIT: 44px exists because a fingertip is imprecise, so the query that
   should raise it is the one that asks whether the pointer IS a fingertip. Width
   is a poor proxy — a mouse user who drags a desktop window narrow gets no more
   accurate and no less, and 34px is already their floor. `(pointer:coarse)`
   says exactly what the rule means. Measured on both sides: 44px at 768 with a
   coarse pointer, 34px at 1440, and the panes do not render at 390 at all
   (below).

   AND BECAUSE A GATE SAYS SO, WHICH THE NEXT PERSON SHOULD KNOW.
   scripts/check-jobcard-integrity.js:631 asserts
   `/max-width[^}]{0,200}(jd-po|jd-vars|inv-pane|jd-book)/` is FALSE — its
   comment reads "the belt is a PERMISSION test, not a viewport one, and not a
   CSS one", and it exists to stop anyone hiding an office-only pane behind a
   width instead of behind engLocked(). That is a good gate and I have not
   touched it. But it matches ANY max-width rule naming those four panes, not
   just one that hides them, so it also blocks legitimate responsive work on
   them — a tap floor trips it exactly as a `display:none` would. This rule is
   not a belt; it changes no pane's visibility and no control's presence.
   Rewriting that assertion to test the hiding properties rather than the
   co-occurrence of a class name and a media query is a real improvement and it
   belongs to whoever owns that gate, not to a page pass editing a safety net
   mid-flight. It is written up in the report instead. */
.ov-pane .btn.small,
.jd-book .btn.small{ min-height:var(--c-tap); }
@media (pointer:coarse){
  .ov-pane .btn.small,
  .jd-book .btn.small{ min-height:var(--c-tap-touch); }
}

/* ══════════════════════════════════════════════════════════════════════════════
   v725 · S32b — Settings → Marketing → AI concierge → Call availability

   Impeccable's pass on this surface scored it 12/40 and 7/20 and proved, per
   element in a live page, that css/design-c.css reached ZERO of the block's
   controls: not one of the seven day pills, neither time input, none of the
   five selects, not the textarea, not the toggle. The Direction C system got
   to this panel through the panel background and the .muted/.small pair and
   nothing else. Everything below is that gap closed.

   Re-measured here before anything was written, and every number Impeccable
   gave reproduced to the digit — including its own correction that --line
   resolves at runtime to #d8d7cc (1.45:1) and NOT the #e5e4dc in the source.
   Measure the rendered rectangle, never the token.
   ══════════════════════════════════════════════════════════════════════════════ */

/* ── the mobile `main` padding, which is DEAD IN DARK app-wide ───────────────
   css/style.css:125 sets main{padding:16px 14px 90px} inside @media
   (max-width:860px), but the unconditional main{flex:1;padding:24px 28px 60px}
   at :143 comes LATER at equal specificity and wins at every width. Only
   body.theme-light main (:1702) has the specificity to override it, so the
   app's mobile padding works in light and does nothing at all in dark, on
   every page. Measured at 390: 14px in light, 28px in dark.
   The Call availability block is where it becomes visible — 28px of lost width
   collapses the third grid column — but it is a general responsive defect and
   is fixed as one. This rule is later in source order than style.css:143 at
   equal specificity, so it wins in dark; in light the higher-specificity
   theme-light rule still wins and sets the identical value, so light is
   byte-identical before and after. Verified both ways.
   `main` cannot contain #solar-studio (it is a fixed full-screen overlay,
   css/style.css), so no exclusion is needed here. */
@media (max-width:860px){
  main{ padding:16px 14px 90px; }
}

/* ── ::placeholder, which NOTHING in this product styles ─────────────────────
   Neither stylesheet has a single ::placeholder rule, so every placeholder in
   the app is Chrome's UA default rgb(117,117,117): measured 4.18:1 on the dark
   panel (FAIL) and 4.61:1 on the light one — a pass by 0.11. This is exactly
   the hole CLAUDE.md §0 records ("the gate never measured ::placeholder").
   --c-mut is 5.71:1 light / 6.82:1 dark and is still lighter than --c-ink, so
   a placeholder still reads as a prompt rather than as a filled-in value.
   opacity:1 because Firefox dims placeholders by default. */
input::placeholder, textarea::placeholder{
  color:var(--c-mut);
  opacity:1;
}

/* ── the focus ring on form controls ─────────────────────────────────────────
   Three global :focus-visible declarations compete and the design system's one
   LOSES: style.css:1264 (2px var(--amber)), style.css:4429 (3px #0d9cbc on
   input/select/textarea:focus-visible, specificity 0,2,0) and this file's :117
   (0,1,0). So every form control in the product gets #0d9cbc — an off-palette
   hex one digit away from the blessed #0D8FAE. It passes at 3.23:1, so this is
   ownership rather than legibility, but a design system that loses on every
   input is not the design system. Matched selector-for-selector at 0,2,0 and
   later in source order, which is how this file is supposed to win. */
input:focus-visible, select:focus-visible, textarea:focus-visible{
  outline:2px solid var(--c-in-dot);
  outline-offset:2px;
}
#solar-studio input:focus-visible,
#solar-studio select:focus-visible,
#solar-studio textarea:focus-visible{ outline:revert; outline-offset:revert; }

/* ── the seven day pills ─────────────────────────────────────────────────────
   These render in TWO places from one class: the concierge's Call availability
   (js/marketing.js) and Opening hours (js/hours.js). Both are fixed here.

   MEASURED BEFORE, rendered and composited, identical at 1440 / 768 / 390:
     light  ON fill #efeadb 1.20:1 vs panel · ON border #8a6800 · OFF border
            #d8d7cc 1.45:1 · weight 500/500 · 33.4px tall
     dark   ON fill #363421 1.41:1 · ON border #ffdd33 · OFF border #2c2f36
            1.33:1 · weight 500/500 · 33.4px tall
   Four separate failures in that table:
     · --amber is a TEXT-ON-WHITE token (style.css says so on the line that
       defines it: "--yel is the BRAND fill; --amber is text-on-white only"),
       so the selected state was Electric Yellow in dark and OLIVE BROWN in
       light — the theme Jamie actually looks at. The sibling control two rules
       away already knew: body.theme-light .chip-btn.on{background:#ffdd33}.
     · the ON fill at 1.20:1 was decoration reading as a smudge.
     · both OFF borders fail WCAG 1.4.11's 3:1 for a component boundary.
     · 33.4px is 0.6px under the desktop floor and 10.6px under the phone one,
       with no responsive adaptation between the two viewports at all.
   And on/off was carried by three colour signals and nothing else — no weight
   change, no tick, no word. DESIGN_SPEC §7: a colour AND a word. */
.mk-days, .hrs-days{
  /* Seven pills summed to 365.4px against a 339px container at 1440 and 300px
     at 390, so SUNDAY WRAPPED TO A ROW OF ITS OWN at every width — measured as
     two distinct `top` values both times. The weekend split across a fold, on
     the control whose whole job is showing the shape of a week. Fixing the tap
     height makes a wrapping flex row worse, not better, so the two are solved
     together with a real seven-column grid. */
  display:grid;
  grid-template-columns:repeat(7, minmax(0, 1fr));
  gap:var(--c-s1);
  flex-wrap:nowrap;
}
.mk-day{
  display:flex;
  flex-direction:column;
  align-items:center;
  justify-content:center;
  gap:1px;
  min-height:var(--c-tap);
  padding:var(--c-s1) 2px;
  border:1px solid var(--c-mut);      /* 5.71:1 light · 6.82:1 dark, both clear 3:1 */
  border-radius:var(--c-r-pill);
  color:var(--c-mut);
  background:var(--c-panel);
  font-weight:500;
  text-align:center;
}
.mk-day.on{
  /* the BRAND fill, in both themes, which is what selected looks like
     everywhere else in this product */
  background:var(--c-fill-action);
  border-color:var(--c-mut);
  color:var(--c-action-ink);
  font-weight:700;
}
/* the non-colour half of the state, per §7 */
.mk-day.on .mk-day-lab::before{
  content:"\2713\00a0";
}
/* A day ticked HERE and shut in Settings → Business. mkFreeSlots tests both;
   the pill only ever read c.days, so it rendered as a normal ticked pill and
   produced nothing. Three cues, none of them colour alone: the fill drops back,
   the label is struck through, and it says the word. */
.mk-day.on.mk-day-shut{
  background:var(--c-sys-bg);
  color:var(--c-mut);
  font-weight:500;
}
.mk-day.on.mk-day-shut .mk-day-lab{ text-decoration:line-through; }
.mk-day.on.mk-day-shut .mk-day-lab::before{ content:none; }
.mk-day-note{
  font-size:10px;
  font-weight:700;
  letter-spacing:.4px;
  text-transform:uppercase;
  line-height:1;
  color:var(--c-mut);
}
/* the input is c-sr-only rather than `hidden`, so it can take focus again —
   the ring belongs on the pill the user can actually see */
.mk-day:has(:focus-visible){
  outline:2px solid var(--c-in-dot);
  outline-offset:2px;
}
@media (pointer:coarse), (max-width:820px){
  .mk-day{ min-height:var(--c-tap-touch); }
}

/* ── the Call availability group ─────────────────────────────────────────────
   A <fieldset>/<legend> so the seven pills, the window and the readout announce
   as one named group instead of eleven loose controls. Every UA default a
   fieldset brings with it is reset, so the panel looks the same as it did. */
.mk-avail{
  border:0;
  margin:var(--c-s3) 0 0;
  padding:0;
  min-width:0;   /* fieldsets default to min-width:min-content and will not shrink */
}
.mk-avail > legend{
  padding:0;
  margin-bottom:var(--c-s2);
}
/* Mode = Off used to leave this block rendering unchanged and still asserting
   "Next it would offer: Thu 30 Jul at 09:00" — the word Off meant neither "it
   has stopped" nor "these settings are inert". The settings stay live and
   editable (nothing is lost); the group is quietened and the sentence beneath
   the legend says so in words. */
.mk-avail-off{ opacity:.72; }

/* From / Until / Call length. The inline style="grid-template-columns:1fr 1fr
   1fr" on this row OUTRANKED every rule in this file without !important, and
   it did not produce thirds: both native <input type=time> fields have a
   min-content floor and COLUMN THREE absorbed the whole deficit. Measured at
   390 in dark, "Call length" got 56.4px against a label needing 58.2px, at
   text-overflow:clip. The inline style is gone from js/marketing.js — a
   stylesheet fix alone would have looked done while doing nothing. */
.set-grid.mk-window{
  grid-template-columns:repeat(auto-fit, minmax(150px, 1fr));
  gap:var(--c-s3);
}

/* the honest warning line: what is set here against what is set elsewhere */
.mk-warn{
  margin:calc(-1 * var(--c-s2)) 0 var(--c-s3);
  padding:var(--c-s2) var(--c-s3);
  border-radius:var(--c-r-chip);
  background:var(--c-pipe-bg);
  color:var(--c-ink);
  font-size:12.5px;
  line-height:1.45;
}
.mk-warn a{ color:var(--c-ink); text-decoration:underline; }

/* ── the tap-target floor across this panel ──────────────────────────────────
   Impeccable's sweep, reproduced here: FIFTEEN of the sixteen interactive
   elements missed the 44px phone floor and ELEVEN missed 34px on the desktop.
   selects 33.0/38.0 · .btn.small 30.0/40.0 · the add-on checkbox 15.6 × 15.6 /
   24 × 24 · the in-panel link 34.9 · both time inputs 42.8 at 390.
   Scoped to #marketing-body, following the precedent this file already set for
   .icon-btn: these are app-wide floors and changing `select` or `.btn.small`
   globally restyles hundreds of controls this pass cannot verify. The app-wide
   finding is named in the report rather than quietly half-fixed. */
#marketing-body select,
#marketing-body input[type=time],
#marketing-body input[type=text],
#marketing-body input[type=tel],
#marketing-body .btn{
  min-height:var(--c-tap);
}
#marketing-body .vat-toggle input[type=checkbox]{
  width:20px; height:20px; flex:none;
}
@media (pointer:coarse), (max-width:820px){
  #marketing-body select,
  #marketing-body input[type=time],
  #marketing-body input[type=text],
  #marketing-body input[type=tel],
  #marketing-body .btn{
    min-height:var(--c-tap-touch);
  }
  /* the checkbox itself cannot be 44px without dwarfing its own label, so the
     LABEL carries the target — the whole row is the hit area, which is what a
     .vat-toggle already is */
  #marketing-body .vat-toggle{ min-height:var(--c-tap-touch); }
  #marketing-body .vat-toggle input[type=checkbox]{ width:24px; height:24px; }
}

/* ── content that was clipped with nothing to say it had been ────────────────
   "Calls go in whose diary" with a 96-character engineer name needed 550.3px
   and had 297px at text-overflow:clip — 47 characters cut mid-word with no
   ellipsis at all. A <select> honours text-overflow in Chromium/WebKit. */
#marketing-body select{
  text-overflow:ellipsis;
}
/* the availability notes box: a 431-character note measured clientHeight 51
   against scrollHeight 163 — 112px of the note hidden in a two-line scroller
   that does not grow. This is the field whose contents block dates in the real
   diary and the owner could not see what he had told it. rows went 2 → 4 in
   js/marketing.js; this stops it collapsing under a narrow column and lets him
   drag it further. */
#marketing-body textarea{
  min-height:5.5em;
  resize:vertical;
}

/* ── a section heading that was smaller than its own sub-label ───────────────
   .test-title is pinned to font-size:12px !important (style.css:2134) while
   .small in this file is 12.5px, so every explanatory sub-label rendered
   LARGER than the heading it explains, both in --c-mut, both at weight 800.
   That is the whole reason these headings do not read as headings. Later in
   source order at higher specificity, so no !important is needed on this side. */
.test-title .small{ font-size:11.5px; }

/* ── the three controls the first pass of the floor above still missed ───────
   Re-measured after the fix rather than assumed, which is how these turned up.
   1. `input[type=text]` matched NOTHING here: the Gateway URL and token fields
      carry no type attribute at all, so they defaulted to text and the
      attribute selector never fired. 40.8px at 390.
   2. `.vat-toggle` got the phone floor and not the desktop one — 26px at 1440,
      and one of the three is the concierge add-on switch itself, the highest
      consequence control on the page. The LABEL is the hit area here (the whole
      row toggles the box), which is why the floor belongs on the row.
   3. sqHelpBtn's ⓘ is 17 × 17 app-wide (css/style.css:3104) and misses both
      floors everywhere it appears, Opening hours included. This pass ADDED one,
      and a control introduced today must not ship under the floor. The hit area
      is extended with a pseudo-element rather than by growing the button: a
      34px ⓘ beside a 12px heading would be wrong, and would be inconsistent
      with the ~40 others in the product. Zero layout change — verified by
      re-measuring the legend and the pill row either side of it.
      The app-wide 17px is named in the report; it is not this pass's to change.
   The in-panel "Settings → AI & data" link is deliberately NOT here. It sits
   inside a sentence, which is WCAG 2.5.8's own inline exception, and padding it
   to 44px would break the paragraph it lives in. */
#marketing-body input:not([type=checkbox]):not([type=radio]):not([type=file]):not(.c-sr-only){
  min-height:var(--c-tap);
}
#marketing-body .vat-toggle{ min-height:var(--c-tap); }
#marketing-body .sq-i{ position:relative; }
#marketing-body .sq-i::after{
  content:"";
  position:absolute;
  inset:calc((var(--c-tap) - 17px) / -2);
}
@media (pointer:coarse), (max-width:820px){
  #marketing-body input:not([type=checkbox]):not([type=radio]):not([type=file]):not(.c-sr-only){
    min-height:var(--c-tap-touch);
  }
  #marketing-body .sq-i::after{ inset:calc((var(--c-tap-touch) - 17px) / -2); }
  /* re-stated here because this block is LATER in the file than the first
     touch override, and at equal specificity the later rule wins even across
     media queries - measured: the toggle row dropped 44 -> 34 without it. */
  #marketing-body .vat-toggle{ min-height:var(--c-tap-touch); }
}

/* ══════════════════════════════════════════════════════════════════════════════
   v728 · MY DAY (S14/S15) — the field day's own repairs.
   Every rule here exists because something was measured on a real 390x844
   handset in engineer mode and found wrong, in the theme it was found wrong in.
   The field app is NOT locked to light (CLAUDE.md §3, corrected 29 Jul: it ships
   its own .fa-themebtn switch), so every one of these uses the closed --c-*
   tokens, which flip with body:not(.theme-light), rather than a literal.
   ══════════════════════════════════════════════════════════════════════════════ */

/* ── the leave banner: the phone finally says he is off ───────────────────────
   Amber, not red. Being booked off is not an error and must not read as one — it
   is the single most important fact on the screen that day, and the jobs are
   still listed underneath it on purpose (js/engineer.js has the reasoning). */
.fa-leave{
  display:flex; flex-direction:column; gap:3px;
  background:var(--c-pipe-bg); border:1px solid var(--c-line-strong);
  border-radius:var(--c-r-card); padding:11px 14px; margin:0 0 10px;
}
.fa-leave strong{ font-family:var(--c-font-display); font-size:14.5px; color:var(--c-ink); }
.fa-leave span{ font-size:12px; line-height:1.45; color:var(--c-mut); }

/* ── the week strip: a leave day is a MARK and a word, never a bare colour ──── */
.eng-wd.off{ background:var(--c-pipe-bg); }
.eng-wd-off{
  position:absolute; top:2px; right:3px; font-style:normal; font-size:9px; line-height:1;
}

/* ── the job row's status WORD ────────────────────────────────────────────────
   §7: status is a colour AND a word. The rail carried the colour; nothing carried
   the word; and JOB_STATUSES gives accepted, completed and paid the SAME #059669,
   so a finished job and one not started were the same row and the screen looked
   identical at 4pm and 8am. The chip takes the status colour as its GROUND (the
   inline --c the rail already uses) and keeps the word on --c-ink, so the colour
   can never be the thing carrying the contrast. */
.fa-jobrow-no{ display:flex; flex-direction:column; align-items:flex-end; gap:4px; }
.fa-jobrow-st{
  font-style:normal; font-size:10px; font-weight:800; letter-spacing:.01em; white-space:nowrap;
  padding:2px 7px; border-radius:var(--c-r-chip); color:var(--c-ink);
  background:color-mix(in srgb, var(--c, #3FE3FF) 18%, transparent);
  border:1px solid color-mix(in srgb, var(--c, #3FE3FF) 45%, transparent);
}

/* ── the overdue task: the red edge was DEAD, and now there is a word ─────────
   css/style.css:5228 sets `.fa-task.over{border-left:3px solid #f87171}` and
   `.fa-shell .fa-task{…border-color:var(--line)}` 370 lines LATER resets all four
   border colours at EQUAL specificity, so the red never rendered inside the field
   shell — which is the only place .fa-task renders. This pass's own line above
   (.fa-task.over → border-left-width:1px) then thinned a hairline that was already
   invisible: Bucket 1, the redesign compounding a pre-existing defect. Colour
   restored on a token that flips with the theme, and the WORD comes from
   js/fieldapp.js so the meaning does not depend on a 1px edge at all. */
.fa-shell .fa-task.over, .fa-task.over{ border-left-color:var(--c-bad-dot); }
.fa-task-late{
  font-size:10px; font-weight:800; letter-spacing:.02em; text-transform:none;
  background:var(--c-bad-bg); color:var(--c-bad-ink);
  border-radius:var(--c-r-chip); padding:1px 6px; white-space:nowrap;
}

/* ── the bottom tab bar: the SELECTED tab was the palest text on it ───────────
   Measured from real screenshot PIXELS, not composited computed styles — the
   repo's own contrast helper reads #fa-tabbar's `color(srgb 0.968627 …/0.88)`
   ground as an 8-bit channel and invents a near-black background, which reversed
   the result both ways (it passed the failing tab and failed three passing ones).
   Pixel-sampled: the active label was rgb(176,137,0) on rgb(248,248,244) = 3.07:1
   at 10.5px/700, against a 4.5 requirement. --c-pipe-ink is the amber TEXT token
   and is #8A6800 in light — the same value style.css already uses for the time
   column, pixel-measured at 5.17:1 — and #FFDD33 in dark.
   And the selected state was conveyed by COLOUR ALONE (§7). It now carries a bar,
   a heavier weight and aria-current="page" from js/fielddesign.js. */
.fa-tab{ position:relative; }
.fa-tab.on{ color:var(--c-pipe-ink); font-weight:800; }
.fa-tab.on::before{
  content:""; position:absolute; top:0; left:50%; transform:translateX(-50%);
  width:24px; height:3px; border-radius:0 0 3px 3px; background:currentColor;
}

/* ── the CLOSED bottom sheet must leave the tab order ─────────────────────────
   .fa-sheet-wrap is only opacity:0 + pointer-events:none when shut, so every
   button inside the More sheet stayed focusable: 14 presses of Tab from the top of
   My Day never reached one of the day's own controls. visibility is the property
   that removes a subtree from the tab order, and the 180ms delay keeps the
   existing fade-out — snapping it hidden would trade one defect for a flicker. */
.fa-sheet-wrap:not(.open){
  visibility:hidden;
  transition:opacity .18s ease, visibility 0s linear .18s;
}

/* ── S32a · My Desk → Map: the honesty line, the status key, a bar that wraps ──
   Three things this group never had, because css/design-c.css carried ZERO rules
   for this surface — no Direction C pass at all.

   1. `.dsk-map-bar` (css/style.css) is `display:flex; flex:0 0 auto` with no
      flex-wrap, so it could neither wrap nor shrink: at ~1000-1100px the count
      label and the ✕ "every date" button were pushed outside their own bar and a
      strict hit-test landed on the jobs panel instead — a date filter you could
      set and could not clear. The label has moved to its own row (js/desk.js) and
      the bar is allowed to wrap, so nothing is pushed off it.
   2. The count line is now a SENTENCE that names why each job is missing, so it
      needs room to wrap rather than a single-line slot in a control bar.
   3. Status was colour alone across eight pin fills with no key anywhere, and the
      word only after a click — CLAUDE.md §7. .dsk-map-key is the key; its swatch
      wears the same fill the marker does (DESK_PIN, js/desk.js) so the two cannot
      drift apart.
   Every colour here is a token. The swatch's white ring matches the marker's own
   2px stroke, and the hairline under it is what keeps a pale fill from
   disappearing into a white panel — the map's pins have the tiles for that. */
.dsk-map-bar{ flex-wrap:wrap; }
.dsk-map-say{
  display:flex; flex-wrap:wrap; align-items:center;
  gap:var(--c-s1) var(--c-s3); flex:0 0 auto; min-width:0;
}
.dsk-map-ask{ font-size:12px; font-weight:700; color:var(--c-ink); }
.dsk-map-legend{ display:flex; flex-wrap:wrap; align-items:center; gap:var(--c-s1) var(--c-s3); }
.dsk-map-key{
  display:inline-flex; align-items:center; gap:5px;
  font-size:12px; color:var(--c-mut); white-space:nowrap;
}
.dsk-map-key i{
  width:11px; height:11px; border-radius:50%;
  border:2px solid var(--c-panel); box-shadow:0 0 0 1px var(--c-line-strong);
  flex:0 0 auto;
}

/* ── S32a, second pass (G10 tool 3) · THE SIX FINDINGS THAT WERE HELD BACK
   BECAUSE THIS FILE WAS BUSY. One of them was live and nobody had measured how
   far it went. ──────────────────────────────────────────────────────────────

   M1 · THE WORKBENCH WAS STARVED TO NOTHING BY THE LIST ABOVE IT, and the map's
   only honest sentence went with it.

   Measured, strict hit-test at each element's centre, seven Leeds jobs, the day
   filter set to a quiet day, light AND dark, viewport 900 tall:

     outstanding items    widths 620-1440 where the count label and the ✕ are
     in "Next up"         UNREACHABLE even after scrollIntoView
     ------------------   ---------------------------------------------------
              0            0 of 14
              2            5 of 14      ← TWO. Not an edge case
              4            5 of 14   (+5 more not visible without scrolling)
              8            5 of 14

   The brief said 860-980px. It is 780-980px unreachable, plus 620 and
   1000-1180 invisible where they sit, and **the width is not the variable —
   the amount of outstanding work is.** At 900x900 with four overdue items
   .dsk-sheet computes 26px: padding + border and a content box of ZERO.

   The chain, resolved in the browser rather than grepped:
     · .dnx-wrap ("Next up") is flex:0 1 auto with min-height:auto, so it cannot
       shrink below its own content and it has no ceiling in the compact desk —
       css/style.css:4481 caps it at 190px, but ONLY under .dsk-left-big.
     · .dsk-folder is flex:1 1 0% with min-height:0 (css/style.css:4233), so it
       has a flex-basis of nothing and absorbs 100% of the shortfall. Measured
       1:1 — dnx 106/249/389/530/596 against folder 667/550/410/270/204.
     · this file's own C1 fix (:477) took .dsk-sheet's 430px floor away on
       purpose and gave .sch-grid its own floor to compensate. THE MAP TAB NEVER
       GOT THE MATCHING FLOOR, so the sheet collapses onto it.

   So it is C1's shape one layer up, and the fix is C1's own idiom: the thing
   that must not vanish gets a floor, and the thing above it gives ground and
   scrolls — exactly what css/style.css:4481 already decided for this same
   element in "More room" mode.

   min(300px, 50%) and not a flat 300px, because a flat floor is measurably
   cruel at narrow widths: it crushed Next up to 46px, while the half-share
   never took it below 161px and still cleared every hit-test. The percentage
   resolves against .dsk-left; where that height is indefinite (the page-scroll
   regime below 881px tall) it computes to 0 and this rule stands down, which is
   right — nothing is starved there. Verified byte-for-byte identical geometry
   before and after at 768px tall, in "More room" mode, and at load 0.

   ONE HONEST NOTE ABOUT THE TWO DECLARATIONS ON .dnx-wrap, because a mutant
   found it out: `min-height:0` is REDUNDANT for the shrinking, since
   `overflow-y:auto` already zeroes a flex item's automatic minimum size — the
   same mechanism C1 above documents for .sch-grid. A browser mutant that
   removed only the min-height stayed green, and it was withdrawn rather than
   re-aimed, because a gate must not claim to catch a no-op. Both declarations
   stay: min-height:0 keeps the shrink independent of whatever the overflow
   value becomes, and overflow-y:auto is what stops a shrunk list losing rows —
   scripts/qa_desk_map_reach.py asserts the last Next-up row is still reachable
   for exactly that reason, and a mutant that takes the scroll away trips it. */
.dsk-left .dnx-wrap{ min-height:0; overflow-y:auto; }
.dsk-left .dsk-folder{ min-height:min(300px, 50%); }

/* M2 · the map bar's own controls sat under the 34px desktop floor: the stage
   select 122x31, the job-type select 151x31, the date input 127x32.6. They come
   from ONE bespoke rule, css/style.css:3857 `.dsk-map-bar select{font-size:12px;
   padding:6px 10px}`, so this is a scoped lift and not a tier change.
   The "Today's jobs" chip is left at 30px ON PURPOSE: :1404 records that
   css/style.css:4980 defines Tier 2 as .btn.small / .chip-btn / .mini-sel at
   30px so everything on a row lines up, and lifting one chip on one bar breaks
   the contract the tier exists to keep. The ✕ and the ⛶ already measure 34x34.
   --c-tap, never a hard-coded 34, so the coarse-pointer half comes with it. */
.dsk-map-bar select,
.dsk-map-bar input[type=date]{ min-height:var(--c-tap); }
@media (pointer:coarse){
  .dsk-map-bar select,
  .dsk-map-bar input[type=date]{ min-height:var(--c-tap-touch); }
}

/* M3 · the ⛶ was #16181d with a #000 hover — neither is in the palette, and it
   had no light variant at all on the default theme. --c-frame is the chrome
   token and is deliberately the same in both themes, which is what this button
   wants: a dark chip reads over pale map tiles either way. Its contrast was
   never the problem (13.2:1 measured) so this is palette and theme coverage.
   The hover now goes to the action fill rather than to black — "gold only where
   your hand goes next", which is the desk's own rule (css/style.css:3775). */
.dsk-map-expand{ background:var(--c-frame); color:var(--c-fill-action); }
.dsk-map-expand:hover,
.dsk-map-expand:focus-visible{ background:var(--c-fill-action); color:var(--c-action-ink); }

/* M4 · THE LIVE-ENGINEER BADGE. CORRECTED 1 Aug, and the first version of this
   rule was the defect. It put the FILL token on the TEXT and left the fill
   alone, so the van's initials went yellow on yellow: 1.00:1 in both themes,
   measured on the rendered rectangle with a real pin on the map, and ZERO of
   3,952 interior pixels differing from the fill. The chip is the only thing that
   tells one van from another on a dispatch map and it had gone blank.

   It is CLAUDE.md §7's trap inverted. §7 warns that --c-ink-money and
   --c-pipe-ink are TEXT colours which render as a brown blob when used as a
   fill. This was the same mistake with the sign flipped: --c-fill-action is a
   FILL and it was used as ink.

   And the untokenised declaration was the BACKGROUND all along. `.deng-i` in
   style.css reads background:var(--accent,#ffdd33); --accent has zero
   definitions in either sheet — measured, it resolves to the empty string on
   body — so the badge was permanently its own hard-coded fallback and could
   never be themed, while the ink beside it was a literal #08090b. Naming a token
   on `color` fixed neither of those and created a third problem.

   Both properties now carry the token that belongs on them, the fill on the fill
   and the ink on the ink: 14.58:1 composited, identical light and dark, which is
   right for a chip that sits over the same map tiles in either theme. (14.58 and
   not the 14.84 this chip read before v747 — the old ink was a literal #08090b
   and the palette's --c-action-ink #0B0C0E is a shade lighter. Both numbers are
   measured; do not "restore" the darker one, it is off-palette.)

   `background-color` rather than the `background` shorthand, so this replaces
   exactly the one longhand that was wrong and cannot silently reset anything
   else about how the badge paints.

   No :not(#solar-studio *). That guard is for rules which could reach inside the
   studio; `deng-i` is emitted from one place in the whole tree — deskEngIcon in
   js/desk.js — into a Leaflet divIcon, and the studio is Google Maps. The rule
   this competes with is style.css's own `.deng-i`, also (0,1,0), setting the SAME
   two properties, so per §7 they compete normally and source order decides — this
   file is later. The guard's 100 points would be bought for nothing here and
   would only make the rule harder to override later. */
.deng-i{ background-color:var(--c-fill-action); color:var(--c-action-ink); }

/* M5 · THE PIN POPUP WAS LEAFLET'S FACTORY DEFAULT IN BOTH THEMES — 20
   .leaflet-popup rules in js/vendor/leaflet.css and zero of ours. It is the one
   place in the product where a customer's name and address are presented in a
   third party's visual language, and in dark it was a white card in a dark app.
   js/vendor/leaflet.css loads at index.html:25, BEFORE this file, so equal
   specificity is enough for everything except the close button, whose vendor
   selector is `.leaflet-container a.leaflet-popup-close-button` (0,2,1) — that
   one is matched exactly rather than out-specified.
   .dsk-map-open is the same trap and the reason this comment exists: as a bare
   class it is (0,1,0) and LOSES to the vendor's `.leaflet-container a` (0,1,1)
   at :85, so the map-to-job link would have kept Leaflet's blue while looking
   like it had been styled. Two correct rules, and the wrong one wins. */
.leaflet-popup-content-wrapper,
.leaflet-popup-tip{
  background:var(--c-panel); color:var(--c-ink);
  box-shadow:var(--c-shadow);
}
.leaflet-popup-content-wrapper{ border-radius:var(--c-r-card); }
.leaflet-popup-content{ color:var(--c-ink); }
.leaflet-container a.leaflet-popup-close-button{
  width:var(--c-tap); height:var(--c-tap); font-size:16px; line-height:var(--c-tap);
  color:var(--c-mut); border-radius:var(--c-r-chip);
}
.leaflet-container a.leaflet-popup-close-button:hover,
.leaflet-container a.leaflet-popup-close-button:focus{
  color:var(--c-ink); background:var(--c-sys-bg);
}
/* the primary map-to-job path, measured by tool 1 at 92.9x15px */
.leaflet-container a.dsk-map-open{
  display:inline-flex; align-items:center; min-height:var(--c-tap);
  color:var(--c-in-ink); font-weight:700; text-decoration:none;
}
.leaflet-container a.dsk-map-open:hover{ text-decoration:underline; }
/* Leaflet's own zoom pair renders 30x30. `:not(#solar-studio *)` is carried
   HERE for the specificity it lends (the vendor rule is `.leaflet-touch
   .leaflet-bar a`, (0,2,1)); the solar studio has no Leaflet, so it guards
   nothing — that is the honest reason, and removing it would silently drop 100
   and put the buttons back to 30px. */
.leaflet-bar a:not(#solar-studio *){
  width:var(--c-tap); height:var(--c-tap); line-height:var(--c-tap);
}

/* ═══════════════════════════════════════════════════════════════════════════
   G11 · LEAVE AND SHUT DAYS ON THE BOARDS — 30 Jul.
   The firm's opening hours and its bank-holiday list were enforced against
   CUSTOMERS (sbSlots refuses Boxing Day) and drawn on no office board at all,
   and the desk board had zero leave marks of any kind: a lane for a man on
   approved leave was pixel-identical to a lane for a man who is in. Every mark
   below is a WORD as well as a treatment (§7) — none of them carries meaning in
   colour alone — and all of them use --c-* tokens so they flip with the theme.
   Nothing here BLOCKS anything: the diary belongs to the firm and a bank-holiday
   callout is real work. These say what the day is; the dispatcher decides.
   ═════════════════════════════════════════════════════════════════════════ */

/* the day board (shared by My Desk and the Calendar day view — schedGrid) */
.sch-shut:not(#solar-studio *){
  display:flex; align-items:center; gap:var(--c-s2);
  padding:6px 10px; margin:0 0 2px;
  font-size:12px; font-weight:600; color:var(--c-mut);
  background:var(--c-sys-bg); border-radius:var(--c-r-chip);
}
.sch-lane-leave:not(#solar-studio *){
  display:inline-block; margin-left:6px; padding:1px 6px;
  font-size:10px; font-weight:700; letter-spacing:.02em;
  color:var(--c-mut); background:var(--c-sys-bg);
  border-radius:var(--c-r-pill); white-space:nowrap;
}

/* the week grid */
.wk-cell.wk-shut:not(#solar-studio *){ background:var(--c-sys-bg); }
.wk-shut-tag:not(#solar-studio *){
  display:block; margin-top:1px;
  font-size:9px; font-weight:700; text-transform:uppercase; letter-spacing:.05em;
  color:var(--c-mut); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}

/* the day band on the Calendar day view */
.board-shut:not(#solar-studio *){
  font-size:11.5px; font-weight:600; color:var(--c-mut);
  background:var(--c-sys-bg); border-radius:var(--c-r-pill);
  padding:2px 9px; white-space:nowrap;
}

/* the month grid */
.cal-cell.cal-shut:not(#solar-studio *){ background:var(--c-sys-bg); }
.cal-shut-tag:not(#solar-studio *){
  font-size:9.5px; font-weight:700; text-transform:uppercase; letter-spacing:.05em;
  color:var(--c-mut); margin:0 0 2px; white-space:nowrap;
  overflow:hidden; text-overflow:ellipsis;
}

/* the engineer's "which half" choice — reuses .fa-lv-type's pill, and the row
   is hidden until the tickbox is on. [hidden] is spelled out because the reset
   this app ships does not carry it and the buttons showed through. */
.fa-lv-halfpart:not(#solar-studio *){ display:flex; flex-wrap:wrap; gap:6px; margin:0 0 8px; }
.fa-lv-halfpart[hidden]:not(#solar-studio *){ display:none; }

/* ══════════════════════════════════════════════════════════════════════════════
   G5 / G6 · TOOL 2 (frontend-design) — the field day and the van's booking sheet
   Measured at 390x844 with real touch emulation, sqIsHandset() and engModeOn()
   read OUT OF THE PAGE, light first then dark, and re-measured at 320/375/390/430.
   Backgrounds composited before every ratio; color-mix() grounds sampled from
   real pixels. The repo's own contrast helper reads `color(srgb 0.96 …)` as an
   8-bit channel and invents a near-black ground, so it was not used.
   ══════════════════════════════════════════════════════════════════════════════ */

/* ── BUCKET 1 · the shared crew row collapsed to a centred column ─────────────
   v689 consolidated the two engineer pickers into one component, and moved
   `<label class="bk-who">` into a container that has its own label rule:

     .fa-visform label { flex-direction:column; font-size:11px }   (0,1,1)
     .bk-who           { display:flex; align-items:center }        (0,1,0)

   0-1-1 beats 0-1-0 whatever the source order, so in the van the row ran as a
   COLUMN with .bk-who's align-items:center now centring horizontally. Measured
   at 390 before this rule: the tick sat at x=186 of a 356px row ABOVE the name,
   the name overflowed its own 46px row by 11.9px and was clipped mid-word, and
   .bk-who-bar — the engineer's identity colour everywhere else in the product —
   rendered 3x3px instead of 3x22px, because `flex:0 0 3px` puts flex-basis on
   the MAIN axis and in a column the main axis is height.

   Two rules, and the second is the one that matters longer term: the row is
   scoped against its host so it beats the container, and the identity bar is
   given an explicit non-flex axis so no future container can flip it again.
   The desktop twin's container is .set-grid, which has no label flex rule, so
   it was never affected — and `flex:0 0 auto` with an explicit 3x22 renders
   byte-identical there (verified by measuring #booking-modal after this rule). */
.fa-visform label.bk-who:not(#solar-studio *){
  flex-direction:row; align-items:center; gap:10px;
  font-size:13.5px; font-weight:600; color:var(--c-ink);
}
.bk-who-bar:not(#solar-studio *){ flex:0 0 auto; width:3px; height:22px; }

/* the form's own captions ran at 11px, under this file's 12.5px floor (.small,
   :184). They are the only words telling a gloved hand which box is which. */
.fa-visform > label:not(#solar-studio *){ font-size:12.5px; }

/* ── the five white-on-white fields whose border was the only boundary ────────
   1.40:1 light / 1.29:1 dark against WCAG 1.4.11's 3:1. The fix for this exact
   defect already existed one file over, scoped `#booking-modal .bk-f input` —
   the field twin was simply left out of the selector list. Same token, so the
   two sheets cannot drift apart again: --c-control-line is declared in BOTH
   theme blocks precisely so it carries the right value into dark. */
.fa-visform input:not(#solar-studio *),
.fa-visform select:not(#solar-studio *){ border-color:var(--c-control-line); }

/* ── three visually identical cards, one of which is a button ─────────────────
   .fa-bknow (read-only), .fa-visrow (read-only) and .fa-morerow (a BUTTON) all
   rendered fill 1.10:1 on the sheet and an identical 1.31:1 hairline, separated
   only by 1-3px of radius and a grey chevron. On a surface whose whole
   accessibility case is outdoor sunlight, structure carried by a 1.31:1 hairline
   is not carried at all. The interactive one takes the control line (3.37:1 over
   the card fill, computed then pixel-checked); the two read-only cards keep the
   quiet hairline, so the difference now IS the affordance rather than a radius. */
.fa-morerow:not(#solar-studio *){ border-color:var(--c-control-line); }

/* ── the crew list was a 186px window inside a sheet that scrolls itself ──────
   `.bk-staff{max-height:186px;overflow-y:auto}` is a second scroller nested in
   one that already works. design-c lifted the same cap for #sb-modal and wrote
   down why it did not lift it here — "neither of those has a scrolling body to
   hand the job to". That premise is false for the field sheet: .fa-sheet is
   max-height:calc(100dvh - 24px); overflow-y:auto; overscroll-behavior:contain,
   measured overflowY:auto at 390. At five engineers the cap already clipped
   (186px window, 262px of rows); at ten it hides six of them. */
.fa-visform .bk-staff:not(#solar-studio *){ max-height:none; }

/* ── the offline banner must not eat the control underneath it ────────────────
   .fa-offline is position:fixed z-index:1300 — above .fa-sheet-wrap's 1200 —
   and it is a pure status readout with no controls of its own (enumerated: zero
   button/a/input descendants). Measured offline at 390 with the visit form open:
   elementFromPoint at the centre of #fav-label "What for" returned
   div#fa-offline, so the one field describing what the return visit is FOR was
   untappable, and offline is the NORMAL field state, not an edge case.

   Two rules, because they fix two different halves:
     1. pointer-events:none — a readout can never be the thing that swallows a
        tap, on any surface, whether or not a sheet is open.
     2. while a bottom sheet or a modal is up, the banner docks to the TOP so it
        stops sitting on the sheet's own controls. It stays on screen — G6 called
        the offline handling the best part of this surface and the reassurance is
        the point — it just stops standing where the fingers go. The sheet is
        bottom-anchored (align-items:flex-end) so the top is the free edge, and
        .fa-sheet scrolls, so nothing it overlaps can be permanently hidden. */
.fa-offline:not(#solar-studio *){ pointer-events:none; }
/* v819 — THE BANNER IS NOW A READOUT WITH ONE CONTROL IN IT, so the child opts back
   in while the slab itself still swallows nothing. The comment above enumerates
   "zero button/a/input descendants"; that was true when it was written and stopped
   being true at v819, when the photo-queue lane gave the engineer a way to see,
   retry or discard a file that could not be sent — without it that banner was a
   dead end (§8). Requested by that lane, which cannot write this file. The button
   ships with pointer-events:auto as an INLINE style (js/fieldapp.js, #fa-offline-open)
   so the door already works; this is the rule living where it belongs, and it lets
   the inline declaration be removed later. pointer-events is an inherited property,
   so the child was picking up `none` by INHERITANCE, not by a competing rule — any
   declaration on the child beats that regardless of specificity, and nothing here
   fights anything. */
.fa-offline:not(#solar-studio *) > button{ pointer-events:auto; }
body:has(.fa-sheet-wrap.open) .fa-offline:not(#solar-studio *),
body:has(.modal.open) .fa-offline:not(#solar-studio *){
  top:calc(8px + env(safe-area-inset-top, 0px)); bottom:auto;
}

/* ── the day's empty state offered no action ─────────────────────────────────
   It named two destinations in prose — "Search a job or check the calendar" —
   and neither was a control (measured: 0 buttons, 0 links). §8: a screen that
   tells you where to go should take you there. js/fielddesign.js emits the two
   buttons; this is their box, and they carry the 44px floor like everything
   else an engineer taps. */
.fa-empty2-go:not(#solar-studio *){
  display:flex; flex-wrap:wrap; justify-content:center;
  gap:var(--c-s2); margin-top:var(--c-s3);
}
.fa-empty2-go .btn:not(#solar-studio *),
.fa-empty2-go button:not(#solar-studio *){ min-height:44px; }

/* ── the route optimiser existed with no control anywhere ────────────────────
   faRouteToggle (js/fieldapp.js) flips window._faRouteOpt, which faOrderJobs
   reads to reorder the day nearest-first. Measured: 0 elements in the DOM
   reference it. A built feature nobody can reach. The control is emitted only
   when it can actually do something — three or more jobs that have a position —
   because a toggle that silently changes nothing is the stub §8 forbids. */
.fa-routeopt:not(#solar-studio *){
  display:inline-flex; align-items:center; gap:6px; min-height:44px;
  padding:0 12px; border-radius:var(--c-r-pill); cursor:pointer;
  font-size:12.5px; font-weight:700; white-space:nowrap;
  background:var(--c-sys-bg); color:var(--c-mut);
  border:1px solid var(--c-control-line);
}
.fa-routeopt.on:not(#solar-studio *){
  background:color-mix(in srgb, var(--c-pipe-ink) 14%, transparent);
  color:var(--c-pipe-ink); border-color:var(--c-pipe-ink);
}

/* ── WHERE THIS BLOCK SITS, AND WHY IT IS NOT AT THE END OF THE FILE ────────
   scripts/check-home-floor-g7.js:248 takes its region as
   `css.slice(css.lastIndexOf('/*', <the G7 header>))` — from the G7 comment
   opener TO THE END OF THE FILE, with no closing delimiter. G7 happened to be
   the last block, so that read the right thing right up until something was
   appended after it. This block was, and G7's assertion
   "no SELECTOR here needs the solar guard" duly went red on MY `:not(#solar-studio *)`,
   which is not in G7's block at all and is required in this one (§2).
   The gate is right about G7 and wrong about where G7 ends. Rewriting an
   assertion in somebody else's safety net mid-flight is not this pass's call —
   the same judgement the .jd-book block records a thousand lines up — so the
   fix here is placement: this block goes BEFORE G7, G7 stays last, and the gate
   keeps testing exactly what it was written to test. It is written up in the
   report as an owed repair, because the next pass to append to this file will
   trip the same wire for the same wrong reason. */

/* ══════════════════════════════════════════════════════════════════════════════
   v751 · THE TAP FLOOR ON IRREVERSIBLE CONTROLS — Jamie's ruling, 30 Jul 2026

   Six separate passes flagged the same 30px controls and every one of them
   correctly declined to overrule the recorded Tier-2 decision 1,000 lines up
   this file rather than patching round it from JS. That was right each time,
   and it is why the question survived long enough to be put to him. He decided
   it, and CLAUDE.md §7 now records it:

       IRREVERSIBLE CONTROLS ARE LIFTED TO 44px ON TOUCH.
       EVERYTHING ELSE KEEPS 30px.

   "Irreversible" is an action a person cannot take back from the same screen —
   Approve, Decline, Delete, Send, Mark paid, and anything else that reaches a
   customer or moves money. His reasoning is the part that decides the edge
   cases: A MIS-TAP ON A CHIP COSTS A SCROLL; A MIS-TAP ON DECLINE COSTS
   SOMEBODY THEIR HOLIDAY.

   So a 30px control that is NOT irreversible is now RECORDED AS ACCEPTED. It is
   not a finding. The Tier-2 block at css/style.css:5014 stands, all 552 call
   sites of it, and the four surfaces that scoped it locally stay scoped.

   ── WHY THIS TARGETS THE HANDLER AND NOT THE CONTAINER ──────────────────────
   The ruling is about what a control DOES, not where it sits. The same Approve
   is emitted three times in three different containers (the approvals queue,
   the flat Leave list, the staff record's Time off tab) and a container-scoped
   rule reaches one copy and silently misses two — which is exactly how the
   duplicate was found. Worse, a container scope over-reaches in the other
   direction: `#team-body .ap-row .btn` also catches Timesheets' "Open", which
   is navigation, and `.cap-table + h3 .btn` — proposed by an earlier pass —
   MATCHES NOTHING AT ALL, because `.cap-table` sits inside `.cert-tbl-wrap` and
   the <h3> holding the week arrows PRECEDES it. Verified against the source.

   Every one of these controls carries its action in an inline `onclick`, so the
   attribute selector names the action itself. That is fragile in one specific
   way — rename a handler and the rule stops matching — and the gate is built
   for exactly that: scripts/qa_tap_floor_irreversible.py MEASURES RENDERED
   RECTANGLES, so a selector that stops matching shows up as a control back
   under the floor, not as a green grep.

   ── THE THREE CASCADE FACTS THIS RULE DEPENDS ON ────────────────────────────
   1. `min-height` beats `height` in the used-value calculation regardless of
      specificity, so this clamps `.btn.small{height:30px}` (style.css:5023) and
      `#qb-money .qb-mbtn{height:32px}` (style.css:5749, inside
      @media(max-width:700px)) without needing to out-specify either. That
      second one is the worst shape in the set: the invoice/quote money bar's
      "Mark paid" and "Mark accepted" render SMALLER ON A PHONE (32px) than on
      a desk (34px), because a phone rule slimmed the bar and the commit button
      went with it. Two correct rules fighting, which CLAUDE.md §7 names as the
      commonest defect here.
   2. `:not(#solar-studio *)` IS HERE FOR THE SOLAR EXCLUSION (§2), NOT FOR ITS
      SPECIFICITY — and this sentence started out claiming the opposite.
      I wrote that the guard's ID-worth of specificity, (1,1,0), is what makes
      these rules beat `@media(...){ .btn{min-height:40px} }` (style.css:4348,
      (0,1,0)). The gate's mutation pass stripped the guard from one selector to
      prove it, AND THE MUTANT SURVIVED: not one rectangle moved. Fact 1 above
      is why — min-height never needed to out-specify anything — and where a
      min-height IS competing, a bare attribute selector is (0,1,0) too and
      design-c.css loads after style.css, so source order had already decided
      it. The guard stays, for the solar exclusion and so the NEXT competing
      rule is not a coin-toss on load order, but do not reason about this block
      as if the 100 points were doing the work. That assertion was withdrawn
      rather than weakened; the reasoning is written up in the gate.
   3. ONE list, not two. The floor is switched on :root by the media query, so
      adding a control later is a one-line change and the gate has exactly one
      list to check. `--c-tap`/`--c-tap-touch` are declared on :root at :75, so
      the var() resolves.

   ── WHAT IT DOES TO LAYOUT ──────────────────────────────────────────────────
   Every container holding one of these is a wrapping flex row or a block with
   no fixed height — `.tm-req-a`, `.ap-row span:last-child`, `.job-chip`,
   `.po-actions`, `.faq-payrow`, `.cv-draft`, `.cert-topbar`, `.hb-need-act`,
   `.qb-money-top` — so a taller button grows its own row and pushes nothing out
   of reach. Checked, not assumed: growing a control is how `.dsk-sheet`
   collapsed to a 26px content box last night, and the gate re-measures the
   surfaces after the lift rather than trusting this paragraph.

   ── WHAT IS DELIBERATELY NOT HERE ───────────────────────────────────────────
   · HQ (`.hql-win`, `.hql-del`, `.hq-acts button`, `.hql-act`).
     ⚠ THE ORIGINAL REASON HERE HAS EXPIRED AND WAS REPLACED, v812. It read
     'CLAUDE.md §5: "HQ is not in scope yet; do not start down that route until
     Jamie says so."' Jamie put HQ in scope this week and is about to sell
     through it, so that sentence would now send the next pass to the wrong
     conclusion. The exclusion still stands, on a BETTER reason:
       Jamie's 30 Jul tap-floor ruling defines irreversible as "an action a
       person cannot take back FROM THE SAME SCREEN". Suspend is not one.
       Driven at v812 on the accounts desk: suspending an account moves it into
       a "Suspended (n)" list ON THAT SAME SCREEN, each row carrying its own
       "↩ Restore". So Suspend fails the definition by the width of one list,
       and lifting it would be applying the ruling to something the ruling
       explicitly does not cover.
     Two supporting facts, measured rather than assumed: `.hq-acts button` is
     min-height 36px (css/style.css), already clear of the 34px desktop floor,
     and HQ is a desktop console — PRODUCT.md says it is not operated on a
     touch tablet. A sibling pass reported these controls as 44px; they are 36.
     `--c-tap-irrev` already flips under (pointer:coarse),(max-width:820px), so
     if that answer ever changes the machinery is one selector away.
   · The Solar Studio's own delete (js/solarstudio.js:222) — §2, and it carries
     no class at all.
   · `.pq-accept` on the published customer quote page — genuinely irreversible,
     but its CSS is a template string inside js/docs.js, which is not css/ and is
     held by another agent this wave.
   · Anything whose size is decided by an inline style in JS. Named in the
     report rather than reached for.
   ══════════════════════════════════════════════════════════════════════════ */
:root{ --c-tap-irrev:var(--c-tap); }
@media (pointer:coarse), (max-width:820px){
  :root{ --c-tap-irrev:var(--c-tap-touch); }
}

/* A · the leave decisions — Jamie's own worked example. Three copies of
   Approve/Decline (js/teamportfolio.js:514-515, :771-772, :910-911) plus the
   bin that has no sqRecycle behind it, and the engineer's own request send.
   Note lvDecide DOES raise a confirm(), but only when the approval clashes with
   booked work — Decline never confirms, and Jamie named Decline. A downstream
   confirm therefore does NOT exempt a control here. */
[onclick*="lvDecide("]:not(#solar-studio *),
[onclick*="leaveRm("]:not(#solar-studio *),
[onclick*="faLeaveSend"]:not(#solar-studio *),

/* B · money moved, recorded or demanded */
[onclick*="markDepositPaid("]:not(#solar-studio *),
[onclick*="markBillPaid("]:not(#solar-studio *),
[onclick*="payRequestDeposit("]:not(#solar-studio *),
[onclick*="payAskAmount("]:not(#solar-studio *),
[onclick*="payRestIsCis("]:not(#solar-studio *),
[onclick*="payRecord("]:not(#solar-studio *),
[onclick*="payRm("]:not(#solar-studio *),
[onclick*="paySettleNow("]:not(#solar-studio *),
[onclick*="faqPayment("]:not(#solar-studio *),
[onclick*="qbMarkAccepted"]:not(#solar-studio *),
[onclick*="quoteMarkAccepted"]:not(#solar-studio *),
[onclick*="optAccept("]:not(#solar-studio *),
[onclick*="resetAllPrices"]:not(#solar-studio *),
[onclick*="resetAllHours"]:not(#solar-studio *),
.qb-mbtn-ok:not(#solar-studio *),
.faq-accept:not(#solar-studio *),

/* C · reaches a customer. ovSend('tel') is excluded on purpose: it hands off to
   the phone's dialler and the person still has to press call. */
[onclick*="docSendGo('mail')"]:not(#solar-studio *),
[onclick*="publishQuoteLink"]:not(#solar-studio *),
[onclick*="ovSend("]:not([onclick*="'tel'"]):not(#solar-studio *),
[onclick*="ovShareEmail"]:not(#solar-studio *),
[onclick*="sbSend("]:not(#solar-studio *),
[onclick*="convSend("]:not(#solar-studio *),
[onclick*="mkDraftSend"]:not(#solar-studio *),
[onclick*="maSend("]:not(#solar-studio *),
[onclick*="svAutoSend"]:not(#solar-studio *),
[onclick*="msgTestSend"]:not(#solar-studio *),
[onclick*="mkTwilioTest"]:not(#solar-studio *),
[onclick*="mkEmailTest"]:not(#solar-studio *),
[onclick*="chaseEmail("]:not(#solar-studio *),
[onclick*="poEmail("]:not(#solar-studio *),
[onclick*="poMark"][onclick*="'sent'"]:not(#solar-studio *),
[onclick*="certEmail("]:not(#solar-studio *),
[onclick*="faProblemSend"]:not(#solar-studio *),
[onclick*="faOnMyWaySend"]:not(#solar-studio *),
[onclick*="faTextSend"]:not(#solar-studio *),
[onclick*="faSignSave"]:not(#solar-studio *),
[onclick*="cloudInvite"]:not(#solar-studio *),
[data-tl="resend"]:not(#solar-studio *),
[onclick*="calPushNow"]:not(#solar-studio *),
[onclick*="formComplete("]:not(#solar-studio *),
[onclick*="schedMoveSend"]:not(#solar-studio *),
[onclick*="bkConfirm"]:not(#solar-studio *),
[onclick*="bkCancel"]:not(#solar-studio *),
[onclick*="fcSend("]:not(#solar-studio *),
[onclick*="certSendQS("]:not(#solar-studio *),
[onclick*="certQSApprove("]:not(#solar-studio *),
[onclick*="certQSReturn("]:not(#solar-studio *),
#so-go:not(#solar-studio *),

/* D · destroys a record with no undo on that screen */
[onclick*="cdbDeleteGo"]:not(#solar-studio *),
[onclick*="jobsBulkDelete"]:not(#solar-studio *),
[onclick*="certRmBoard("]:not(#solar-studio *),
[onclick*="cktRm("]:not(#solar-studio *),
[onclick*="obsRm("]:not(#solar-studio *),
[onclick*="patRm("]:not(#solar-studio *),
[onclick*="emlRm("]:not(#solar-studio *),
[onclick*="bkMsgDel("]:not(#solar-studio *),
[onclick*="etDel("]:not(#solar-studio *),
[onclick*="pjDel("]:not(#solar-studio *),
[onclick*="matNewDelete"]:not(#solar-studio *),
[onclick*="msgTemplates.splice"]:not(#solar-studio *),
[onclick*="poRm("]:not(#solar-studio *),
[onclick*="tsRm("]:not(#solar-studio *),
[onclick*="deleteKit("]:not(#solar-studio *),
[onclick*="optRemove("]:not(#solar-studio *){
  min-height:var(--c-tap-irrev);
}

/* ══════════════════════════════════════════════════════════════════════════════
   G7 · THE CLASSIC HOME AND THE COCKPIT — tool 2, frontend-design, 30 Jul
   ══════════════════════════════════════════════════════════════════════════════
   These two surfaces had never had a Direction C pass: not one of the fourteen
   .hb-* / .fo-* classes they use appeared in this file before today. So every
   number below is "already broken", never "the redesign broke it".

   MEASURED FIRST, at 1440 and 768, LIGHT THEN DARK, on rendered rectangles with
   every background composited to the first opaque layer:

     keyboard   focusable 33 · clickable 61 · nonSemantic 22
                nonSemanticFocusable 0 · WithRole 0 · WithKeydown 0
                .focus() on a .hb-row landed on BODY. Nobody without a mouse
                could open a job, expand a stat or clear a missed item.
     headings   h1 1, h2 0, h3 0 — the only heading on the page was "Evening."
     taps       .fo-back a 16.0 · .hb-tlink 17.0 · .hb-links span 19.4
                .hb-tick 22.0 · .hb-fold summary 22.6 · .fo-basis .chip-btn 24.0
                .chip-btn / .btn.small 30.0        (floor: 34 desktop, 44 touch)
     boundary   .hb-row border-top 1.35:1 light, 1.26:1 dark
     tasks tab  .due-late 3.93:1 light / 4.60 dark · .due-today 4.28 / 4.23,
                both at 11px/700 where 4.5:1 is required, and the two states are
                otherwise IDENTICAL — same size, same weight, just a date in a
                different colour. Colour alone, on the one list whose entire job
                is telling overdue from due-today.
     found here .hb-tlink 3.72:1 in DARK (#0e7490 on the #14161c panel). Neither
                of tool 1's two assessments caught this one: it only renders when
                an open task carries a jobId, and their fixtures had none.

   WHY THESE RULES DO NOT CARRY :not(#solar-studio *) AND THE REST OF THE FILE
   DOES. That guard lends a rule an ID's worth of specificity — (1,x,y), not
   (0,x,y) — and here that is not free protection, it is a bug. `.hb-links button`
   is (0,1,1) and loses to style.css's `.hb-links .hb-alert{color:var(--red)}` at
   (0,2,0), which is exactly right: the unsatisfactory-EICR link must stay red.
   Guarded, my reset would be (1,1,1), would win, and would silently repaint the
   one link on the page that is a warning. Checked rather than assumed: `grep -c
   "hb-\|fo-" js/solar*.js` is 0 across all eleven solar files, so none of these
   selectors can reach inside the studio and none of them needs the guard.
   ══════════════════════════════════════════════════════════════════════════════ */

/* the eight categorical slice colours, so the cockpit's donut stops being a
   ninth place colour is decided. Values are byte-identical to the FO_WHEEL array
   js/finhome.js still carries as its fallback, so this is provably a no-op
   today — what it buys is that a token edit can now reach the charts at all. */
:root{
  --c-wheel-1:#ffdd33; --c-wheel-2:#3fe3ff; --c-wheel-3:#34d399; --c-wheel-4:#a78bfa;
  --c-wheel-5:#fb923c; --c-wheel-6:#f472b6; --c-wheel-7:#60a5fa; --c-wheel-8:#facc15;
}

/* ── 1 · the rows, stats and links are <button> now, and must not look it ─────
   js/jobs.js emits them as real buttons so they get the role, the tab stop,
   Enter AND Space and this file's focus ring. The UA then supplies a grey face,
   a 2px outset border, 13.33px Arial and centred text, none of which the old
   divs had. Everything below is undoing exactly that and nothing else.

   THE BORDER IS DONE PER SIDE ON PURPOSE. `border:0` here would be a shorthand
   expanding to all four sides, and it would kill style.css's
   `.hb-row{border-top:1px solid var(--line)}` from a file that loads later —
   the shorthand-beats-longhand trap CLAUDE.md §7 names, which has already got
   through a gate written to catch it. Only the three sides the UA added are
   zeroed, so the row divider keeps coming from style.css and
   `.hb-expand .hb-row:first-of-type{border-top:none}` still wins. */
button.hb-row, button.hb-stat, .hb-need-go,
.hb-links button, .hb-expand-foot button, button.hb-tlink, .fo-inline-link{
  font:inherit; color:inherit; text-align:left;
  background:none; -webkit-appearance:none; appearance:none;
  border-top:0 none; border-right:0 none; border-bottom:0 none; border-left:0 none;
  margin:0; cursor:pointer;
}
button.hb-row{
  width:100%; border-right:0 none; border-bottom:0 none; border-left:0 none;
}
/* the sub-line was a <div> inside the row and is a <span> now — a <div> is not
   phrasing content and is invalid inside a button. Put its block back. */
button.hb-row .dt-sub, button.hb-stat .hb-fig, button.hb-stat .hb-lbl{ display:block; }
button.hb-stat{ padding:0; }

/* ── 2 · the "what got missed" row: one primary control, one tick ────────────
   The row used to be a <div onclick> with a <button> inside it. A <button>
   wrapper is impossible (nested buttons are invalid and break), and
   role="button" is wrong for the same reason — a button's children are
   presentational, so the tick inside it would disappear from the a11y tree.
   So the row keeps its markup, the text becomes the primary <button>, and a
   stretched ::after gives the mouse back the whole-row target it had. Nothing
   is lost: clicking the dot, the text or the empty space still opens the job,
   and the tick is now a genuinely separate second stop rather than a control
   buried inside another control. */
.hb-need.hb-missed{ position:relative; }
.hb-need-go::after{ content:''; position:absolute; inset:0; }
.hb-need.hb-missed .hb-need-act{ position:relative; z-index:1; }
/* the focus ring must follow the stretched area, not the text box, or a keyboard
   user sees the outline in a different place from where the click lands */
.hb-need-go:focus-visible{ outline:none; }
.hb-need-go:focus-visible::after{ outline:2px solid var(--c-in-dot); outline-offset:-2px; }

/* ── 3 · hover and section styling the old spans had, kept on the buttons ────
   style.css hangs these off `.hb-links span` / `.hb-expand-foot span`, which no
   longer match. Re-stated for the element that does, at the same specificity so
   `.hb-links .hb-alert` still wins the colour. */
.hb-links button:hover, .hb-expand-foot button:hover{ color:var(--text); text-decoration:underline; }
.hb-tlink:hover{ text-decoration:underline; }
.fo-inline-link{ text-decoration:underline; padding:0; }
/* THE RESET ABOVE TOOK THE LINK COLOUR OFF, AND THAT IS A REAL REGRESSION I CAUSED.
   `.hb-tlink{color:#0e7490}` is (0,1,0); grouping it into a `button.hb-tlink` reset
   at (0,1,1) meant color:inherit won and the job number inside a task stopped
   looking like a link at all — measured, in the same pass that made it focusable.
   Two CORRECT rules fighting, which is the commonest defect shape in this CSS and
   the reason the cascade gets resolved rather than assumed. It is put back as a
   TOKEN, not as the hex: #0e7490 measured 3.72:1 on the dark panel (a failure tool
   1's two assessments both missed, because .hb-tlink only renders when an open task
   carries a jobId and neither fixture had one). --c-in-ink is 5.72:1 light and
   13.1:1 dark. Same specificity as the reset, later in the file, so it wins on
   source order — which is how this file is supposed to work. */
button.hb-tlink{ color:var(--c-in-ink); }
/* .fo-card-h is an <h2> now; style.css sets only margin-bottom, so the UA's
   0.83em top margin would push every card header down. */
h2.fo-card-h{ margin-top:0; }

/* ── 4 · the row divider: 1.35:1 -> 2.09:1, and why not 3:1 ─────────────────
   tool 1 read .hb-row's boundary as a CONTROL boundary and called for WCAG
   1.4.11's 3:1. I am not taking it to 3:1 and the reason is written here rather
   than left out of the report: DESIGN_SPEC's own token block declares
   --c-line-strong "2.09:1, deliberately NOT a WCAG 3:1 line" for structural
   rules, and a 3:1 hairline between twelve stacked rows turns a list into a
   table. What identifies these as controls is now the button role, the pointer,
   the hover tint and a 2px focus ring — none of which existed when that
   measurement was taken. The divider's job is separation, and it is raised to
   the strongest line the design system has. Stated as a decision, not omitted. */
.hb-row, .hb-need, .hb-empty, .hb-links, .hb-stats{ border-top-color:var(--c-line-strong); }
.hb-row:hover{ background:var(--c-in-bg); }

/* the cockpit's own two: the doorway button in the strip and the "open in jobs"
   link I turned from a styled <span onclick> into a real button. */
.fo-strip .btn, .fo-strip .btn.small{ min-height:var(--c-tap); display:inline-flex; align-items:center; justify-content:center; }
.fo-inline-link{ position:relative; }
.fo-inline-link::after{ content:''; position:absolute; inset:-8px -4px; }

/* ── 5 · tap targets, scoped to this group ───────────────────────────────────
   34px desktop, 44px touch. Scoped to the G7 containers rather than lifting
   .chip-btn and .btn.small globally: those two are emitted right across the
   product and the global Tier-2 lift is its own pass, already deferred further
   up this file. This closes the six failures on these two screens without
   moving anything else. */
.hb-links button, .hb-expand-foot button, .hb-fold summary,
.hb-stats .chip-btn, .hb-chart-chips .chip-btn, .hb-taskadd .btn,
.hb-need-act .btn, .fo-periods .chip-btn, .fo-basis .chip-btn{
  min-height:var(--c-tap);
  display:inline-flex; align-items:center; gap:6px;
}
.hb-fold summary{ display:flex; }
/* the tick and the two inline links keep their drawn size and grow only their
   HIT area. Growing the boxes would put a 34px circle where a 22px one was and
   would break the inline flow of a link sitting mid-sentence — the same
   evidence-led choice the .icon-btn block above made in the opposite direction,
   for the opposite reason (there was no visual box to protect there; here there
   is). Centred on the control, so the target grows evenly. */
.hb-need .hb-tick, .hb-missed .hb-tick, .hb-tlink, .fo-back a, .fo-foot a{ position:relative; }
.hb-need .hb-tick::after, .hb-missed .hb-tick::after{
  content:''; position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
  width:var(--c-tap); height:var(--c-tap);
}
.hb-tlink::after, .fo-back a::after, .fo-foot a::after{ content:''; position:absolute; inset:-9px -4px; }
@media (pointer:coarse), (max-width:820px){
  .hb-links button, .hb-expand-foot button, .hb-fold summary,
  .hb-stats .chip-btn, .hb-chart-chips .chip-btn, .hb-taskadd .btn,
  .hb-need-act .btn, .fo-periods .chip-btn, .fo-basis .chip-btn{ min-height:var(--c-tap-touch); }
  .hb-need .hb-tick::after, .hb-missed .hb-tick::after{
    width:var(--c-tap-touch); height:var(--c-tap-touch);
  }
  .hb-tlink::after, .fo-back a::after, .fo-foot a::after{ inset:-14px -6px; }
  .fo-strip .btn, .fo-strip .btn.small{ min-height:var(--c-tap-touch); }
  /* the missed row IS the target — its stretched ::after is inset:0 of the row —
     so the row carries the floor rather than the button inside it. */
  .hb-need.hb-missed{ min-height:var(--c-tap-touch); }
}

/* ── 6 · the desk's task dates: contrast, and a word ─────────────────────────
   #e24b4a on the white .dsk-task panel is 3.93:1 and #b06a00 is 4.28:1, both at
   11px/700 against a 4.5:1 floor — and the least legible text on the row is the
   most urgent one. --c-bad-ink and --c-pipe-ink are the tokens for exactly this
   and pass in BOTH themes: 6.02:1 / 6.65:1 and 5.17:1 / 13.60:1.

   The other half of the house rule — a colour AND a word, never colour alone —
   is a one-word change in js/desk.js:1207, which belongs to another agent this
   wave. Generated content is the second-best fix and it is a real one: ::before
   text participates in the accessible name, so "overdue 27 Jul" and "due 30 Jul"
   are what a screen reader says and what a red-green-confusing user reads.
   Scoped to .dsk-task-meta because the OTHER sink, .todo-meta at js/desk.js:93,
   already writes "overdue " into the text itself and would say it twice. */
.due-late{ color:var(--c-bad-ink); }
.due-today{ color:var(--c-pipe-ink); }
.dsk-task-meta .due-late::before{ content:'overdue '; }
.due-today::before{ content:'due '; }

/* ── 7 · two money figures that only fail when there IS money ────────────────
   Neither of tool 1's assessments could see these: both fixtures ran at £0, so
   the two colours that only appear when the firm is owed something never
   rendered. Driven with a real overdue invoice, both are the most urgent number
   on their screen and the least legible text on it.

     .fo-warn      "£2,520.00 overdue", 11px/700, #d97706 on the white KPI card
                   -> 3.19:1 LIGHT against 4.5:1.  Dark already passed.
     .hb-fig.hb-warm  the classic home's Unpaid tile, 22px/600, #b45309
                   -> 3.97:1 DARK against 4.5:1.  22px at 600 is NOT "large
                   text" (that needs >=24px, or >=18.66px at >=700), so 3:1
                   does not apply and it is a straight fail.  Light passed at
                   5.02:1, which is why measuring one theme would have missed it.

   --c-bad-ink for overdue and --c-ink-money for money owed: both are TEXT
   tokens, which is what these are, and both pass in both themes —
   6.02:1 / 6.65:1 and 5.17:1 / 13.60:1. */
.fo-warn{ color:var(--c-bad-ink); }
.hb-fig.hb-warm{ color:var(--c-ink-money); }

/* ── the launch-scoped Home's counters, 6 Aug 2026 ───────────────────────────
   The one rule that surface needs, and it lives HERE rather than at the end of
   the file on purpose: the note at the very bottom says two gates slice from a
   marker to EOF and will silently own anything appended after them, and the v814
   certificate block already turned check-hq-reads.js red exactly that way. This
   sits with its sibling instead, which is also where a reader looking for
   .hb-fig would go.

   .hb-fig.hb-bad is "this one has already lapsed" on the launch Home's counter
   row — a retest gone past its date, an MOT that has run out. It is NOT
   .hb-fig.hb-warm: --c-ink-money is the MONEY colour, and a lapsed MOT is not
   money. --c-bad-ink is the token for "this is a problem", it is a TEXT token
   used as text, and it is the same one .fo-warn takes one line up (6.02:1 light,
   6.65:1 dark — both clear 4.5:1, which 22px/600 needs since it is not large
   text). No solar guard, matching its two neighbours: `grep "hb-" js/solar*.js`
   is 0 across all eleven solar files, so nothing here can reach inside. */
.hb-fig.hb-bad{ color:var(--c-bad-ink); }

/* ══════════════════════════════════════════════════════════════════════════════
   G5 TOOL 2 (frontend-design) · THE FIELD DAY AND THE WEEK STRIP — S14 + S15
   The four things tool 2 owns: responsive, focus, motion, type and spacing.
   Everything here was measured as a RENDERED RECTANGLE at 320/375/390/430/768/
   1440, engineer mode, light first then dark, and re-measured on main v674 in
   its own worktree on its own port to decide the bucket.

   ── WHERE THIS BLOCK SITS ──────────────────────────────────────────────────
   AFTER G7, deliberately, and opening with a top-level `/* ═` banner. The
   v751/v752 pass had to squeeze in ABOVE G7 because check-home-floor-g7.js read
   its region as "from the G7 comment to END OF FILE" and so silently owned
   anything appended. That was repaired at v754: the region now closes at the
   next `/* ═` banner. This block is the first append since the repair and it
   proves the repair — verified by running the gate before and after, and by
   deleting a token from G7 itself and watching it go red rather than being
   propped up by a later block.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── 1 · [BUCKET 1] THE FOCUS RING ON ALL SEVEN WEEK DAYS WAS CLIPPED TO TWO
        ARCS, AND THE 44px TAP FLOOR IS WHAT DID IT ─────────────────────────
   v728 lifted `.eng-wd` off 42.6px onto the floor, and the only way to do that
   in a fixed seven-cell row is to let the row scroll instead of shrinking a
   target. Its own comment says so and the reasoning is right. What went with it
   was `overflow-x:auto` — and per CSS overflow, a non-visible value on ONE axis
   computes the other from `visible` to `auto`. Measured: `overflowY: auto`.

   `.eng-week` is exactly as tall as `.eng-wd` (both 57.8px at 390; topRoom 0,
   bottomRoom 0), and the ring is drawn 4px outside the border box (2px offset +
   2px width). So the top and bottom strokes were clipped on ALL SEVEN days, and
   the outer vertical stroke on Monday and on Sunday as well. What survived was
   two curved slivers that read as decoration. Measured at 390 AND 1440, light
   and dark, all four cuts = 4.00px, and photographed.

   The whole navigation of My Day had no visible keyboard focus. On main v674
   `.eng-week` is `overflow: visible` and the ring is a complete rectangle —
   driven in a main worktree — so this is the redesign's own regression.

   THE FIX IS THE PATTERN THIS FILE ALREADY USES, not a new one: `.sch-block`
   hit the same wall and design-c answered it with a negative offset, writing
   down why — "Drawn inside its own box it cannot be [clipped]". A ring at
   -3px sits wholly within the border box, so no ancestor overflow can reach
   it, nothing needs a negative margin, and NOT ONE RECTANGLE MOVES. */
body.eng-mode .eng-wd:focus-visible:not(#solar-studio *){ outline-offset:-3px; }

/* ── 2 · AT 861px AND UP THE DOCKED SIDEBAR BURIED THE FIRST FIELD TAB ──────
   An engineer is in engineer mode on ANY screen (engModeDefault reads the role,
   not the width), so #fa-tabbar draws on a desktop. It is
   `position:fixed; left:0; right:0; z-index:900` and the docked sidebar is
   `z-index:1000`, so the bar runs underneath it.

   Driven with elementFromPoint at each tab's centre, both themes:
     768, 860  sidebar off-canvas at x -264      all four tabs reachable
     861+      sidebar sticky, x 0, w 210        "My day" hit-tests to
               main.x also 210                   DIV.sidebar-foot — UNREACHABLE
   A real mouse click at its centre changed nothing. The tab kept
   `aria-current="page"` and the selected styling the whole time: highlighted,
   and untappable. CLAUDE.md section 8 — a control that appears must work.

   861px is not a number invented here; it is one past the sidebar's own
   `@media (max-width:860px)` off-canvas rule, and 210px is the sidebar's own
   declared width (css/style.css). The gate asserts both against that source so
   the two cannot drift apart.

   Same property (`left`) as the rule it beats, equal specificity (1,0,0), later
   file — so source order does it and no `!important` is involved. No solar
   guard: an id that cannot occur inside the studio does not need one, and
   adding it would raise the specificity by 100 for nothing. */
@media (min-width:861px){
  #fa-tabbar{ left:210px; }
}

/* ── 3 · THE JOB NUMBER WAS 11px/400, UNDER THE PHONE FLOOR ────────────────
   DESIGN_SPEC section 2: "Body text never drops below 13px on desktop or 12px
   on the phone", and secondary text is Satoshi 400 at 12.5-13px. css/style.css
   already carries a sweep for exactly this — "fix 5 · minimum data-text size:
   9-10px data → 12px" — which lifted fourteen selectors including `.eng-wd span`
   and deliberately left `.cal-ev` at 11px with its reason written down (a month
   cell clips at 12). `.fa-jobrow-no` is on neither list. It was missed.

   J-5000 is the number a man reads back down the phone to the office, at 11px
   and weight 400, sitting next to `.fa-jobrow-addr` at 12.5px and the same
   weight. 12.5px puts it on the approved secondary step and level with its own
   neighbour. This is NOT the open 12/13/14 question in DESIGN_SPEC — that one
   is about which of three approved steps to keep, and nothing here moves any of
   them.

   WHAT IT COSTS, A/B'd in ONE frame on ONE page by toggling only this
   declaration — because "measured after" against a number from a different run
   is how a stale claim gets written down. I first wrote "the row stays 71px"
   here. It does not:

     390   row 71 -> 72px; the day 991.77 -> 996.77 (five of six rows +1px, the
           wrapped-name row was already 93 and did not move)
     1440  row 71 -> 72px, shell height unchanged
     320   NO CHANGE AT ALL — rows there are 93px, set by the wrapped name

   Nothing moves sideways: `.fa-jobrow-name` holds its width to the hundredth of
   a pixel at all three widths, `.fa-jobrow-no` holds its width, the address
   clips exactly as many times as before (6 at 390, 0 at 1440 — the pre-existing
   nowrap ellipsis), and horizontal overflow stays 0. One pixel a row is the
   price, and tool 3 must re-measure row heights against 72, not 71. */
.fa-jobrow-no:not(#solar-studio *){ font-size:12.5px; }

/* ── 4 · MEASURED AND ACCEPTED, so the next pass does not re-open them ──────
   `.fa-jobrow-st` "Accepted" 10px/800 and `.fa-tab` 10.5px/800 are also under
   12.5px and are NOT changed. Both are label-role, not body copy: DESIGN_SPEC's
   own scale carries a 10px label step, both are extra-bold, and both were
   measured by tool 3 at 14.27:1 and 4.82:1. Lifting a chip and a bottom-nav
   label inside a 334px row would cost the customer's name its width, which is
   the one thing on that row a driver has to read. Named here rather than
   dropped.

   AND ONE RESIDUAL ON RULE 1, measured with a real Tab walk rather than
   .focus(), so it is the keyboard a person actually uses. Where the strip
   scrolls (320 and 375 only) Chromium does not always scroll a part-visible day
   fully in on focus, so ONE day's outer vertical stroke is cut:
       320  Sunday-1 focused, scrollLeft stays 0, right stroke cut 14px
       375  Monday focused after the strip centred today, left stroke cut 6px
   The other three strokes are fully drawn in both cases and T/B are -1 on all
   seven days at every width, so the ring reads as a ring. Buying the last 6-14px
   back needs a delegated focusin listener on #engday-body, and a new global
   listener on a surface G6 is about to work on is not worth 6px of outline on
   one day at one width. Named, not started. */

/* ══════════════════════════════════════════════════════════════════════════════
   v775 · LANE B — CERTIFICATES GET A FRONT DOOR ON THE PHONE
   Tool 2 (frontend-design). js/facerts.js wraps renderCertList the way it already
   wraps renderCertEditor, so this is the phone skin for a list that previously
   had no phone route at all: measured at v774, the field tab bar was four tabs
   and the More sheet was ten rows, and neither carried the word "certificate".

   Tokens: --c-* throughout, per DESIGN_SPEC §1 — not style.css's --text/--muted,
   even though the .fc-* rules next door use them. In light the two systems are
   the same values to the byte for panel, line and muted, so there is no seam;
   in dark --c-panel is #15181E against --panel #14161c, which is under a
   perceptual JND. The flow moving wholesale onto --c-* is B16 and is handed on.

   No :not(#solar-studio *) guard on any rule here: none of these class names
   exists inside the studio, so none can reach it, and adding the guard would
   lend each rule an ID's worth of specificity for nothing.
   ══════════════════════════════════════════════════════════════════════════════ */

/* The desktop page furniture, off the phone. #view-certs ships an <h1> and a
   marketing <p class="lead"> above #certs-body. On a 390 handset they measured
   ~200px — a quarter of the first screen spent on a title the phone's own top
   bar already carries, above a paragraph written for a sales page. Nothing is
   lost: this is chrome the field skin replaces, not a control, and the desktop
   keeps both. Scoped to the field skin's own body class. */
body.eng-mode #view-certs > .view-head,
body.eng-mode #view-certs > .lead{ display:none; }

.fc-listshell .fa-topbar2{ justify-content:flex-start; }
.fc-lcount{
  margin-left:auto; font-family:var(--c-font-display);
  font-size:13px; font-weight:700; color:var(--c-mut);
  font-variant-numeric:tabular-nums;
}

/* ── a row: the whole row opens it, a 44px ... carries the three list actions ── */
.fc-lrow{ display:flex; align-items:stretch; gap:6px; margin:0 0 8px; }
.fc-lrow-go{
  flex:1; min-width:0; display:flex; align-items:center; gap:9px;
  min-height:var(--c-tap-touch); padding:9px 11px; text-align:left; cursor:pointer;
  background:var(--c-panel); border:1px solid var(--c-line);
  border-radius:var(--c-r-card); color:var(--c-ink);
  font-family:var(--c-font-body); font-size:13px;
}
/* v821: 18px -> 14px. It was the BIGGEST glyph on the row while the customer
   name — the thing the eye is actually hunting for — was 14px, so a list of rows
   led with a decorative emoji and had no entry point. The type it stands for is
   spelled out in words two millimetres below it, so shrinking costs no
   information; it is kept rather than dropped because at a glance the shape
   still sorts a PAT from an EICR faster than reading does. */
.fc-lrow-ico{ font-size:14px; line-height:1; flex:0 0 auto; }
.fc-lrow-main{ flex:1; min-width:0; display:flex; flex-direction:column; gap:2px; }
/* The name WRAPS; the sub-line clips. Measured at 390 with the status chip in
   the row: an ellipsised name gave "Mrs Enid Braith..." and "Halifax Join...",
   which throws away the surname and the company - the two words that say which
   customer this is. .fa-jobrow-addr next door keeps the BEGINNING of an address
   for the same reason, and for a name the identifying part is at the end, so the
   answer there is the opposite of the answer here. Two lines of name costs one
   row 17px and only on the long ones. */
/* v821: 14/600 -> 16/650. The name is what the eye hunts for on a list of
   customers and it was the third-largest thing on its own row, behind an
   18px decorative emoji and level with nothing. */
.fc-lrow-main b{
  font-family:var(--c-font-display); font-size:16px; font-weight:650;
  overflow-wrap:anywhere; line-height:1.22;
}
.fc-lrow-sub{
  font-size:12.5px; color:var(--c-mut);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}

/* ══ v821 — THE FIXED RIGHT COLUMN, AND THE FIXING IS THE POINT ════════════════
   Before this, the status pill sat directly in the row's flex line, so its own
   text length decided how much width was left for the address: a "With the QS"
   pill left the sub-line 106px and a "Draft" pill left it 141px, on the same
   screen, in the same list. A column whose width is a function of a word in
   another column cannot be reasoned about and cannot be tested — the next status
   label anybody adds silently re-measures every address on the page.
   So: ONE fixed column, 82px, holding the pill and the date. 82 is the widest
   pill ("With the QS", 77px) plus its own breathing room; the date lines are
   narrower than that by construction. flex:0 0 82px, never a percentage — the
   thing being defended is an absolute number of characters of date, not a share
   of a screen. */
.fc-lrow-meta{
  flex:0 0 82px; min-width:0; display:flex; flex-direction:column;
  align-items:flex-end; justify-content:center; gap:3px; text-align:right;
}
/* ⚠ THE DATE MAY NOT ELLIPSIS, WRAP, SHRINK OR CLIP. That is the whole defect
   this block exists to close, so there is deliberately no overflow rule here to
   soften it: if a date ever did not fit, the gate measures scrollWidth against
   clientWidth on this element AND on every descendant, and goes red. A silent
   ellipsis is what got us here. */
.fc-lrow-when{
  display:flex; flex-direction:column; align-items:flex-end; line-height:1.15;
  font-family:var(--c-font-body); white-space:nowrap;
}
/* 12px is the phone floor and the label sits ON it rather than under it. A first
   draft had the label at 10px, which is where a caption naturally wants to go and
   is under the floor for a surface read in a van in daylight. "Due back" at
   12px/700 measures 58px and the widest date 68px, both inside the 82px column,
   so the floor costs nothing here. */
.fc-lrow-when i{
  font-style:normal; font-size:12px; font-weight:700; letter-spacing:.01em;
  color:var(--c-mut); white-space:nowrap;
}
.fc-lrow-when b{
  font-size:13px; font-weight:700; color:var(--c-mut);
  font-variant-numeric:tabular-nums; white-space:nowrap;
}
/* A recall date is a colour AND a word (DESIGN_SPEC §7): the label above it says
   "Due back" or "Overdue" in text, so the tint is reinforcement and never the
   carrier. --c-bad-ink and --c-pipe-ink are TEXT tokens (CLAUDE.md §7) and are
   used as text here, which is the only thing they are for. */
.fc-lrow-when-due i, .fc-lrow-when-due b{ color:var(--c-pipe-ink); }
.fc-lrow-when-late i, .fc-lrow-when-late b{ color:var(--c-bad-ink); }

/* ⚠ 36px OF PAINT, 44px OF TAP. It was 44x74-109 with a border, a radius and a
   background BYTE-IDENTICAL to .fc-lrow-go, 6px away — so every row read as two
   equal cards and the second one was a menu button. It also ate 44px of exactly
   the width the address is starving for.
   The paint shrinks and goes quiet; the TARGET does not move. ::after is an
   invisible 44px-wide overlay, so the tap floor is met on the axis a thumb
   misses on while the ⋯ stops competing with the row. position:relative on the
   button is what the overlay is measured against; the row is align-items:stretch
   so the height is the row's, always well over 44. */
.fc-lrow-more{
  position:relative;
  flex:0 0 auto; width:36px; min-height:var(--c-tap-touch);
  display:flex; align-items:center; justify-content:center; cursor:pointer;
  background:none; border:0; border-radius:var(--c-r-card); color:var(--c-mut);
  font-size:19px; line-height:1; font-family:var(--c-font-body);
}
.fc-lrow-more::after{ content:''; position:absolute; top:0; bottom:0; left:-4px; right:-4px; }
.fc-lrow-more:active{ color:var(--c-ink); }
.fc-lrow-del .fa-row-l{ color:var(--c-bad-ink); }

/* ══ v821 — THE COLLAPSED PICKER ══════════════════════════════════════════════
   Full width so it cannot be mistaken for a row, directly under the filter bar,
   and it opens the same seven tiles in the field skin's own sheet. See
   fcStartSheet in js/facerts.js for what the 385px card cost on landing. */
.fc-lstart{
  display:flex; align-items:center; justify-content:center;
  width:100%; min-height:var(--c-tap-touch); margin:0 0 12px;
  font-family:var(--c-font-display); font-size:15px; font-weight:700;
}

/* Status: a colour AND a word (DESIGN_SPEC §7), never the colour on its own.
   Tinted chip grounds, never a coloured left-border. */
.fc-lst{
  flex:0 0 auto; align-self:center;
  font-size:10.5px; font-weight:700; letter-spacing:.02em;
  padding:3px 8px; border-radius:var(--c-r-chip); white-space:nowrap;
}
/* .fc-lst sets align-self:center, which was right when the pill sat on the row's
   own horizontal flex line and is the CROSS axis now it is in a column — it
   would centre the pill against a right-aligned date under it. Same property,
   same file, later in source order, so it wins without an !important. */
.fc-lrow-meta > .fc-lst{ align-self:flex-end; max-width:100%; }
.fc-lst-ok  { background:var(--c-ok-bg);   color:var(--c-ok-ink); }
.fc-lst-pipe{ background:var(--c-pipe-bg); color:var(--c-pipe-ink); }
.fc-lst-bad { background:var(--c-bad-bg);  color:var(--c-bad-ink); }
.fc-lst-sys { background:var(--c-sys-bg);  color:var(--c-sys-ink); }

/* The sent-back card gets a HAIRLINE, not a tint. PAGE-CHECKLIST: never tint a
   container with the same token as a chip inside it — a .fc-lst-bad chip on a
   --c-bad-bg card composites two layers of one token to a ground neither was
   measured against, and that is how a 5.17 reads 4.12 in the wild. */
.fc-lsent{ border-color:var(--c-bad-dot); }
.fc-lsent h4{ color:var(--c-bad-ink); }
.fc-lerr h4{ color:var(--c-bad-ink); }

/* ── start a certificate: six tiles, three up, every one over the phone floor ── */
/* grid-auto-rows:1fr because the two rows measured 76px and 85.3px on the first
   drive - "Emergency lights" and "Appliances" wrap, "New work" does not - and a
   grid of tiles that are not the same size reads as a mistake. */
.fc-typegrid{ display:grid; grid-template-columns:repeat(3, minmax(0,1fr)); grid-auto-rows:1fr; gap:8px; }
.fc-ltile{
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:3px; min-height:76px; padding:10px 6px; cursor:pointer; text-align:center;
  background:var(--c-panel); border:1px solid var(--c-line);
  border-radius:var(--c-r-card); color:var(--c-ink); font-family:var(--c-font-body);
}
.fc-ltile-ico{ font-size:21px; line-height:1; }
.fc-ltile b{ font-family:var(--c-font-display); font-size:13px; font-weight:700; }
.fc-ltile span{ font-size:11px; color:var(--c-mut); line-height:1.25; }
.fc-ltile:active{ background:var(--c-fill-action); color:var(--c-action-ink); }
.fc-ltile:active span{ color:var(--c-action-ink); }

/* ── the empty state: says what this is for and gives the action (DESIGN_SPEC §6) */
.fc-lempty{
  display:flex; flex-direction:column; gap:4px; padding:14px 12px;
  border:1px dashed var(--c-line-strong); border-radius:var(--c-r-card);
  background:var(--c-surface);
}
.fc-lempty b{ font-family:var(--c-font-display); font-size:14px; font-weight:600; }
.fc-lempty span{ font-size:12.5px; color:var(--c-mut); line-height:1.5; }

/* ── the offline state, and the thing that is true and was invisible ──────────
   Every complaint in the certificate market research is about losing work, not
   about forms. This app genuinely saves every keystroke through persist() and
   certSendQS makes no network call at all. Saying so is worth more here than
   any amount of polish. */
.fc-ltrust{
  margin:10px 0 0; font-size:12px; line-height:1.5; color:var(--c-mut);
  padding-top:10px; border-top:1px solid var(--c-line);
}

/* ── find one: search + type chips, the desktop's two filters, kept ─────────── */
/* the two filters the desktop table carries, in a card of their own - a bare
   <details> sitting straight on the work surface read as an orphaned line */
.fc-lfind{
  background:var(--c-panel); border:1px solid var(--c-line);
  border-radius:var(--c-r-card); padding:12px 14px; margin:0 0 12px;
}
.fc-lfind > summary{
  font-family:var(--c-font-display); font-size:14px; font-weight:600;
  cursor:pointer; min-height:var(--c-tap); display:flex; align-items:center;
}
.fc-lchips{ display:flex; flex-wrap:wrap; gap:6px; margin-top:8px; }
.fc-lchip{
  min-height:34px; padding:6px 11px; cursor:pointer;
  background:var(--c-panel); border:1px solid var(--c-line);
  border-radius:var(--c-r-pill); color:var(--c-ink);
  font-family:var(--c-font-body); font-size:12.5px; font-weight:700;
}
.fc-lchip i{ font-style:normal; color:var(--c-mut); font-variant-numeric:tabular-nums; }
.fc-lchip.on{
  background:var(--c-fill-action); border-color:var(--c-fill-action);
  color:var(--c-action-ink);
}
.fc-lchip.on i{ color:var(--c-action-ink); }

/* ── the fifth tab ────────────────────────────────────────────────────────────
   .fa-tab is flex:1, so the bar re-divides itself: 94.5px -> 78px at 390 and
   64px at 320. Both are far over the 44px floor on the axis a thumb misses on,
   and the bar's own 49px height is untouched. The only thing five tabs cost is
   label width, so the labels are pinned to one line — a wrapped "My day" would
   push the bar taller and shift every tab's centre. */
#fa-tabbar .fa-tab > span:last-child{ white-space:nowrap; }

/* A refused action, shown WITH THE REASON rather than hidden or left live. It is
   a <div>, not a <button>, so it is not a tab stop and cannot be pressed — a
   disabled-looking button that is still focusable is the same lie in a quieter
   voice. See fcCanDelete in js/facerts.js. */
.fa-row.fc-lrow-cant{ opacity:.72; cursor:default; }
.fa-row.fc-lrow-cant .fa-row-l{ color:var(--c-mut); }

/* ══════════════════════════════════════════════════════════════════════════════
   v777 · LANE B — THE 86-ITEM INSPECTION SCHEDULE, GIVEN A MIDDLE
   Measured before: 7,121px at 390 and 8,840px at 320, ten section headers and
   the step rail all position:static, and the 0/86 counter the smallest element
   on the step. Nine screenfuls with no fixed reference point.
   ══════════════════════════════════════════════════════════════════════════════ */

/* ── v825 · PHASE 2 — THE SCHEDULE BECOMES A LONG-LIST CARD ───────────────────
   The step's own card wrapper was measured as `cards:1, h4s:0`: ONE 1400px
   rectangle round all 86 rows, a border nobody can see at that length, and no
   heading at all. Its 15px side padding was also what squeezed the row text
   column to 168px, and a 168px column is what turned 86 items into SIX row
   heights (65x15, 76x30, 94x29, 112x9, 130x2, 185x1, measured at 390).

   So this card becomes DESIGN_SPEC's long-list variant: padding 0, rows running
   full-bleed to the card edge, and the row rules doing the separating instead of
   the border — because a card taller than the window by nature cannot separate
   anything with its own outline. The header sticks instead.

   ⚠ overflow is `clip`, NOT `hidden`. `hidden` makes the card its own scroll
   container and every sticky child then sticks to a box that never scrolls;
   `clip` does not. It is here so the full-bleed rows do not square off the
   card's radius. The sticky levels are re-measured after this change, not
   assumed — see the offsets block further down this file. */
.fc-shell .fc-card.fc-schedcard:not(#solar-studio *){
  padding:0; overflow:clip;
}
/* the prose and the bulk control keep the inset the rows gave up */
.fc-shell .fc-schedcard > .fc-hint:not(#solar-studio *){ padding:10px 14px 0; margin:0; }

/* The bar that stays. The card no longer pads, so this no longer has to escape
   it — the negative margins that used to pull it to the card edges are gone. */
.fc-schedbar{
  position:sticky; top:0; z-index:3;
  display:flex; align-items:center; gap:10px; flex-wrap:wrap;
  margin:0; padding:11px 14px;
  background:var(--c-panel); border-bottom:1px solid var(--c-line-strong);
  border-radius:var(--c-r-card) var(--c-r-card) 0 0;
}
.fc-schedbar-n{ font-size:13px; color:var(--c-mut); font-variant-numeric:tabular-nums; }
.fc-schedbar-n b{
  font-family:var(--c-font-display); font-size:16px; font-weight:700; color:var(--c-ink);
}
.fc-schedbar-bad{ font-style:normal; color:var(--c-bad-ink); font-weight:700; }
.fc-schedbar-done{ margin-left:auto; font-size:12.5px; font-weight:700; color:var(--c-ok-ink); }
.fc-schednext{ margin-left:auto; }

/* A section: native <details>, so the keyboard and the screen reader come free
   and no disclosure widget has to be invented. The rule between sections is the
   STRONG hairline, because with the card's border no longer separating anything
   these rules are the only structure the reader gets. */
.fc-insec{ border-top:1px solid var(--c-line-strong); }
.fc-insec-h{
  display:flex; align-items:center; gap:10px; cursor:pointer;
  min-height:48px; padding:8px 14px; list-style:none;
  background:var(--c-surface); border-bottom:1px solid var(--c-line-strong);
}
.fc-insec-h::-webkit-details-marker{ display:none; }
.fc-insec-h::before{
  content:"›"; flex:0 0 auto; width:16px; text-align:center;
  font-size:17px; line-height:1; color:var(--c-mut);
  transform:rotate(0deg); transition:transform .16s ease;
}
.fc-insec[open] > .fc-insec-h::before{ transform:rotate(90deg); }
/* ⚠ 13.5px WAS OFF-SPEC. DESIGN_SPEC §2 records 13.5px as the one step that was
   removed from the scale, and at 13.5px this heading was also SMALLER than the
   14px body text underneath it — a heading that loses to its own body is not a
   heading. It is now the spec's "card heading" role exactly: Clash 600, 14px. */
.fc-insec-t{
  flex:1 1 auto; min-width:0;
  font-family:var(--c-font-display); font-size:14px; font-weight:600;
  line-height:1.3; color:var(--c-ink);
}
/* ⚠ THIS RULE'S OWN COMMENT USED TO ARGUE THE OPPOSITE, and it is reversed on
   purpose rather than quietly. It said tinting the counts "would be colour
   alone". That objection was correct about a chip carrying ONLY a number, and it
   is answered by the chip now carrying A WORD as well (none / part / done) —
   DESIGN_SPEC §7 is satisfied by the word, and the tint is the second channel,
   not the only one.

   What the old wording could not see is that its own scheme rendered THREE
   states as TWO: `sec.blanks.length ? 'part' : 'done'` gave "0/23" (nobody has
   touched this section) and "4/9" (somebody is part way through it) the SAME
   class and byte-identical styling. Measured with 6 items ticked: ten chips, ONE
   distinct rendered state. On a document somebody signs, "not started" and "part
   done" are the two facts the reader most needs kept apart.

   The old note also warned that --c-pipe-bg is "the ground the rows below sit
   on". It is not: the rows sit on --c-panel, and a DONE row now sits on
   --c-ok-bg, so a done chip agreeing with the done rows is coherence, not a
   collision. The chip sits in the header band (--c-surface) regardless. */
.fc-insec-n{
  flex:0 0 auto; display:inline-flex; align-items:baseline; gap:5px;
  font-size:11px; font-weight:700; letter-spacing:.02em;
  font-variant-numeric:tabular-nums; padding:4px 8px; border-radius:var(--c-r-chip);
  background:var(--c-sys-bg); color:var(--c-mut);
}
.fc-insec-n i{ font-style:normal; font-weight:700; opacity:.85; }
/* ⚠ --c-mut, NOT --c-sys-ink. The system ink is rgba(13,15,19,.60) and composited
   over --c-sys-bg over the header band it measured 4.49:1 — a real miss by 0.01,
   found by this block's own gate rather than by eye. --c-mut is a solid value and
   clears it comfortably in both themes. */
.fc-insec-n-none{ background:var(--c-sys-bg); color:var(--c-mut); }
.fc-insec-n-part{ background:var(--c-pipe-bg); color:var(--c-pipe-ink); }
.fc-insec-n-done{ background:var(--c-ok-bg);  color:var(--c-ok-ink); }
.fc-insec-b{ padding:0 0 10px; }

/* ── THE ITEM ROW ─────────────────────────────────────────────────────────────
   ONE REGULAR HEIGHT THAT GROWS TO FIT, which is the only answer compatible with
   Jamie's standing rule that a field shows everything in it, grows to fit and
   scrolls last. A statutory item an engineer attests to is never clipped: there
   is no line-clamp, no ellipsis and no max-height anywhere in this block, and a
   gate asserts that no row's text overflows its own box.

   ⚠ THE ID IS INLINE, AND THAT IS A MEASURED DECISION THAT WENT THE OTHER WAY
   FIRST. Giving the id its own tabular column reads better on paper and the
   mockup drew it that way — but driven at 390 it cost 44px (a 40px track plus a
   gap) out of a 304px content row, and the wording column fell from 168px to
   126px. That made the SIX heights EIGHT and pushed the worst row from 185px to
   213px: the ripple this work exists to remove, made worse by the thing meant to
   remove it. The schedule's wording runs to 202 characters on item 4.14 and a
   median of 73, so on this screen width for the wording outranks a tidy column.
   The id keeps a distinct tabular treatment inline so it still anchors the row.

   Reclaimed alongside it: the gap is 8 rather than 10, and the outcome button's
   min-width is 52 rather than 64 (it holds N/A, LIM, C1 and ⋯ comfortably, and
   its 44px height is untouched). Content row 304px: 192 wording + 44 + 52 + 16. */
.fc-shell .fc-schedcard .fc-insp:not(#solar-studio *){
  display:flex; align-items:center; gap:8px;
  min-height:56px; padding:6px 14px;
  border-top:1px solid var(--c-line); background:var(--c-panel);
}
.fc-shell .fc-schedcard .fc-insp-t:not(#solar-studio *){
  flex:1 1 auto; min-width:0; font-size:13px; line-height:1.4; color:var(--c-ink);
  /* a regulation cite like (522.6.202;522.6.203) is one unbreakable token and it
     was measured spilling 12px past its own box. This wraps it instead — the row
     grows, which is the rule, and nothing is ever cut off. */
  overflow-wrap:anywhere;
}
.fc-shell .fc-schedcard .fc-insp-id:not(#solar-studio *){
  font-weight:700; font-size:11px; color:var(--c-mut);
  font-variant-numeric:tabular-nums; white-space:nowrap; margin-right:5px;
}
.fc-shell .fc-schedcard .fc-outcome:not(#solar-studio *){ min-width:52px; }

/* ── DONE DIFFERS ON THREE PROPERTIES, NOT ONE GLYPH ──────────────────────────
   Measured before this change: a ticked row and an untouched row were IDENTICAL
   on background, colour, weight, size, border and opacity. The only difference
   in the whole row was a ✓ inside its own button, so finding your place in 86
   rows meant reading 86 glyphs.

   Three channels now, and none of them is colour alone (§7): the row ground is
   tinted, the tick button is genuinely FILLED rather than tinted, and the
   wording drops to --c-mut. The id keeps its own ink so the column still reads
   as a column all the way down. */
.fc-shell .fc-schedcard .fc-insp.fc-insp-done:not(#solar-studio *){ background:var(--c-ok-bg); }
.fc-shell .fc-schedcard .fc-insp.fc-insp-done .fc-insp-t:not(#solar-studio *){ color:var(--c-mut); }
/* an item carrying a code is neither done nor untouched, and it is the one the
   reader is hunting for. The outcome button already says WHICH code in words. */
.fc-shell .fc-schedcard .fc-insp.fc-insp-coded:not(#solar-studio *){ background:var(--c-bad-bg); }

/* the one-tap ✓ */
.fc-insp-ok{
  flex:0 0 var(--c-tap-touch); width:var(--c-tap-touch);
  min-width:var(--c-tap-touch); height:var(--c-tap-touch); min-height:var(--c-tap-touch);
  display:flex; align-items:center; justify-content:center; cursor:pointer;
  background:var(--c-panel); border:1px solid var(--c-line);
  border-radius:11px; color:var(--c-mut); font-size:17px; line-height:1;
}
/* a FILL, not a tint — this is one of the three channels above */
.fc-insp-ok.on{
  background:var(--c-ok-dot); border-color:var(--c-ok-dot); color:#0B0C0E;
  font-weight:700;
}
.fc-secrest{ width:calc(100% - 28px); margin:10px 14px 0; min-height:var(--c-tap-touch); }

/* ══════════════════════════════════════════════════════════════════════════════
   v778 · LANE B — THE BOARD-SCAN CONFIRM SWEEP
   Everyone in the category has the camera; nobody has the check. Phase 0 stopped
   the scan fabricating a curve or an IΔn it could not read, so a blank is now
   genuinely blank — and this is the screen that makes that VISIBLE instead of
   silent. A missing value that looks like a gap is how a scanned board gets
   rubber-stamped.
   ══════════════════════════════════════════════════════════════════════════════ */

/* the photo, pinned. It is the thing being checked against, so it does not
   scroll away underneath the ways it produced. */
.fc-scanphoto{
  position:sticky; top:0; z-index:4; display:block; width:calc(100% + 32px);
  margin:-15px -16px 0; padding:0; border:0; cursor:zoom-in; background:var(--c-frame);
  border-radius:var(--c-r-card) var(--c-r-card) 0 0; overflow:hidden;
}
.fc-scanphoto img{ display:block; width:100%; height:112px; object-fit:cover; object-position:center; }
.fc-scanbig img{ display:block; width:100%; height:auto; border-radius:var(--c-r-input); }

.fc-scanbar{
  position:sticky; top:112px; z-index:3;
  display:flex; align-items:center; gap:10px; flex-wrap:wrap;
  margin:0 -16px 10px; padding:10px 16px;
  background:var(--c-panel); border-bottom:1px solid var(--c-line);
}
.fc-scanbar-n{ font-size:13px; color:var(--c-mut); font-variant-numeric:tabular-nums; }
.fc-scanbar-n b{ font-family:var(--c-font-display); font-size:16px; font-weight:700; color:var(--c-ink); }
.fc-scanbar-bad{ font-style:normal; color:var(--c-bad-ink); font-weight:700; }
.fc-scanjump{ margin-left:auto; }

/* one card per way. The way NUMBER is the anchor, because that is what the eye
   reads off the board — biggest thing on the card, hard left, tabular. */
.fc-scanrow{
  display:flex; align-items:stretch; gap:8px;
  border:1px solid var(--c-line); border-radius:var(--c-r-card);
  margin:0 0 8px; background:var(--c-panel);
}
.fc-scanway{
  flex:0 0 auto; width:36px; text-align:center;
  font-family:var(--c-font-display); font-size:20px; font-weight:700;
  font-variant-numeric:tabular-nums; color:var(--c-ink); line-height:1.15;
}
.fc-scanrow-go{
  flex:1; min-width:0; display:flex; align-items:flex-start; gap:10px;
  padding:11px 4px 11px 8px; background:none; border:0; cursor:pointer;
  text-align:left; color:var(--c-ink); font-family:var(--c-font-body);
  min-height:var(--c-tap-touch);
}
.fc-scanrow-m{ flex:1; min-width:0; display:flex; flex-direction:column; gap:3px; }
.fc-scanrow-m > b{ font-family:var(--c-font-display); font-size:14px; font-weight:600; overflow-wrap:anywhere; }
.fc-scanrow-id{ font-size:12.5px; color:var(--c-mut); }
.fc-scanrow-cab{ font-size:12px; color:var(--c-mut); }

/* A BLANK IS A BLANK, LOUDLY. Never whitespace: a named dashed slot, so the eye
   lands on the thing nobody read instead of sliding over it. */
.fc-scanslot{
  display:inline-block; padding:0 6px; border:1px dashed var(--c-bad-dot);
  border-radius:var(--c-r-chip); color:var(--c-bad-ink);
  font-size:11.5px; font-weight:700; line-height:1.5;
}
/* the reason row, and it is a WORD as well as a colour (DESIGN_SPEC §7) —
   never colour alone, and the amber-in-dark trap that put .fc-o-C1 at 2.77:1 is
   why this uses the --c-bad ink/fill PAIR rather than one value doing both. */
.fc-scanrow-say{ font-size:12px; font-weight:600; color:var(--c-bad-ink); line-height:1.4; }
.fc-scanrow.is-blank{ border-style:dashed; border-color:var(--c-bad-dot); }

.fc-scanrow-act{ flex:0 0 auto; display:flex; flex-direction:column; gap:6px; padding:8px 8px 8px 0; }
/* the tick is the ONLY green thing on the card */
.fc-scanok{
  width:var(--c-tap-touch); min-height:var(--c-tap-touch);
  display:flex; align-items:center; justify-content:center; cursor:pointer;
  background:var(--c-ok-bg); border:1px solid var(--c-ok-dot); border-radius:11px;
  color:var(--c-ok-ink); font-size:18px; font-weight:700; line-height:1;
}
/* 44px, not 34. "Not used" CLEARS the curve, rating and IΔn the photo read, and
   there is no way back to those values from this screen — that is irreversible
   under Jamie's 30 Jul ruling, so it goes to the touch floor. It deliberately
   does NOT ask first: it is the commonest correction on a scanned board (the scan
   reads blanking plates as circuits) and a confirm on the commonest correction is
   how a sweep gets abandoned. One tap on the card puts it back by hand. */
.fc-scanspare{
  min-height:var(--c-tap-touch); padding:4px 8px; cursor:pointer; white-space:nowrap;
  background:var(--c-panel); border:1px solid var(--c-line); border-radius:var(--c-r-chip);
  color:var(--c-mut); font-family:var(--c-font-body); font-size:11px; font-weight:700;
}
/* CONFIRMED collapses to a single line. That is what makes progress physical and
   keeps the work that is left on screen. */
.fc-scanrow.is-done{
  align-items:center; gap:8px; padding:0 12px 0 4px;
  min-height:var(--c-tap-touch); background:var(--c-surface);
}
.fc-scanrow.is-done .fc-scanway{ font-size:15px; color:var(--c-mut); }
.fc-scanrow-t{ flex:1; min-width:0; font-size:13px; color:var(--c-mut);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.fc-scanrow-ok{ flex:0 0 auto; font-size:11.5px; font-weight:700; color:var(--c-ok-ink); }

.fc-scanclean{ width:100%; min-height:var(--c-tap-touch); margin-top:6px; }
.fc-scanclean-h{ margin:6px 0 0; }

/* ══════════════════════════════════════════════════════════════════════════════
   v779 · LANE B — THE REST OF TOOL 1'S LIST, IN ITS PRIORITY ORDER
   ══════════════════════════════════════════════════════════════════════════════ */

/* ── B6 · "DANGER — act now" WAS 2.77:1 IN DARK ───────────────────────────────
   Found twice independently by two methods and re-measured here: white on
   var(--red), and dark's --red is #f87171 while light's is #dc2626, so the one
   theme that failed was the one an electrician switches to in a dim cupboard, on
   the single most safety-critical code on the schedule.

   ROOT CAUSE, and it is DESIGN_SPEC §1's own rule recurring in the older token
   system: style.css:18 sets dark --red-ink to the IDENTICAL value as --red, even
   though the comment on that line calls --red-ink "red as TEXT" and --red "the
   fill/border colour". Light splits them correctly. C2 is fine because it keeps
   text and fill apart; C1's solid fill collapsed the distinction.

   THE FIX IS THE FILL, NOT THE INK, because the ink is white and has to stay
   white on a danger code. The fill is pinned to #dc2626 in BOTH themes - a value
   already in this tree as light --red, not a new hue - which measures 4.83:1
   with white in either theme. Fixing the token itself would restyle every one of
   the 40+ rules reading --red across the whole product and belongs to whoever
   takes the old token system on (that is B16, and it is handed on).

   !important is required and is a DOCUMENTED EXCEPTION: css/style.css:2882 sets
   these three with !important, and you cannot out-specify an !important without
   one. Same specificity (0,1,0), later source order, so this wins. */
.fc-o-C1{
  background:#dc2626 !important;
  border-color:#dc2626 !important;
  color:#fff !important;
}

/* ── B6b · THE SAME TRAP RUNNING THE OTHER WAY ────────────────────────────────
   "✓ Zs fine — max 1.1Ω for this breaker" measured 3.77:1 in LIGHT and 9.41 in
   dark: a good-news banner failing in the theme that usually passes. --green
   (#059669) is the DOT/fill value; --c-ok-ink (#04785C) is the token mixed to be
   read as TEXT on a pale ok tint, which is exactly this. Its own tinted ground is
   left alone so the chip still reads as green. */
.fc-flag-ok{ color:var(--c-ok-ink); }

/* ── B6c · THE ONLY CONTROL IN THE FLOW UNDER JAMIE'S FLOOR ───────────────────
   "The installation is at the same address" — the wrapping <label> is the real
   tap target and measured 300x28 at every viewport in both themes. The pattern
   was already three rules away in the same sheet (.fc-seg button carries
   min-height:44px) and this control never got it. Scoped to the field skin: the
   .vat-toggle class is shared with the desktop's VAT row, which is a desktop
   control on a desktop floor. */
.fc-shell .vat-toggle{
  display:flex; align-items:center; gap:10px;
  min-height:var(--c-tap-touch); cursor:pointer;
}

/* ── THE 320px HORIZONTAL OVERFLOW, SETTLED ───────────────────────────────────
   Three earlier measurements blamed three different elements and the audit's
   #ops-drawer attribution was adopted as best-evidenced. Measured directly at
   320, element by element, on both surfaces: #ops-drawer DOES overhang to 720px
   on My Day and the document's scrollWidth there is exactly 320, because it is
   position:fixed and clipped — SO IT IS NOT THE CAUSE. In the certificate editor
   the document really is 339, and the first element past the edge in document
   order is the top bar's own row: back chevron + title + Ask + status chip come
   to 339.2px of nowrap flex that cannot shrink. The .fc-railstep buttons overhang
   much further but sit inside an overflow-x:auto parent and cannot inflate the
   document, which is what the earlier reasoning got right.
   The title is the part that can give, so it is the part that gives. */
.fc-shell .fa-topbar2{ flex-wrap:wrap; row-gap:6px; }
.fc-shell .fa-topjob{ min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }

/* ── B12 + B13 · TWO SCROLLING STRIPS WITH NO AFFORDANCE ──────────────────────
   The step rail is 744px of content in 334 at 390 and 264 at 320, so 4 of 7 steps
   are invisible on the commonest handset — and it is simultaneously the progress
   indicator, the wayfinding and the only way to jump. The circuit sheet's tab
   strip is 452 in 356 and crops "Live tests" mid-glyph with "Sign-off" entirely
   off screen. Both scroll (-webkit-overflow-scrolling:touch is set) so they are
   functionally reachable and visually undiscoverable.
   A mask fade is the affordance. It costs nothing when the strip is not
   over-wide, because what it fades there is empty background. */
.fc-rail, .fc-ckt-jump{
  -webkit-mask-image:linear-gradient(90deg, #000 calc(100% - 26px), transparent);
  mask-image:linear-gradient(90deg, #000 calc(100% - 26px), transparent);
}
@media (prefers-reduced-motion: reduce){
  .fc-rail, .fc-ckt-jump{ scroll-behavior:auto; }
}

/* ══ v825 · THE STEP BAR — B12's real answer ══════════════════════════════════
   Added HERE, beside the .fc-rail rule it supersedes, and NOT at the end of this
   sheet: two gates slice css/design-c.css from a marker to EOF with no closing
   delimiter, so anything appended is silently swallowed into their block. The
   mask-fade above is the affordance that was the best available answer while the
   rail still scrolled horizontally; it is left in place because .fc-ckt-jump (the
   circuit sheet's tab strip) still scrolls and still needs it.

   B12 measured: 721px of chips in a 334px window at 390 — 3 of 7 steps visible,
   the same 3 at 360 and 430 — and position:static, so 600px into the Checks step
   the whole thing sat at top:-438 and never returned. This replaces it with a bar
   that is sticky, full-width, and width-independent at any step count.
   Ported from the approved mockup (scratchpad/mock/proposal.css .pp-pos), with
   one deliberate change: the mockup's device frame had a sticky 48px chrome bar
   so it stuck at top:48px. THE REAL .fa-topbar2 IS NOT STICKY — it is a plain
   flex row with margin-bottom — so here the bar sticks at top:0 and the top bar
   scrolls away above it. Nothing is lost by that: the top bar carries the back
   chevron, the certificate number and the status chip, none of which is the
   "where am I in the flow" question this bar exists to answer.                 */
.fc-shell .fc-pos:not(#solar-studio *){
  position:sticky; top:0; z-index:29;
  margin:0 0 12px; padding:8px 12px 9px;
  background:var(--c-panel); border-bottom:1px solid var(--c-line);
}
.fc-shell .fc-pos-eyebrow:not(#solar-studio *){
  display:flex; align-items:baseline; gap:6px; flex-wrap:wrap;
  margin:0 0 1px; color:var(--c-mut);
  font-weight:700; font-size:10px; letter-spacing:.09em; text-transform:uppercase;
}
.fc-shell .fc-pos-eyebrow i:not(#solar-studio *){ font-style:normal; color:var(--c-line-strong); }
.fc-shell .fc-pos-row:not(#solar-studio *){ display:flex; align-items:center; gap:8px; }
/* min-width:0 is what lets a long step name ellipsis instead of pushing Maisie
   out of the row — DESIGN_SPEC §5, a flex item defaults to min-width:auto. */
.fc-shell .fc-pos-name:not(#solar-studio *){
  min-width:0; flex:1 1 auto; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  font-family:var(--c-font-display); font-weight:600; font-size:19px;
  letter-spacing:-.2px; color:var(--c-ink);
}
/* The negative margins pull a 44x44 hit area back into a 30px-tall visual row
   without adding vertical space to the bar. flex:0 0 44px is the declaration that
   actually HOLDS the width in a flex row — a bare width: on a flex item is a
   suggestion, and that is how a declared 44 was measured at 37. */
.fc-shell .fc-pos-all:not(#solar-studio *){
  flex:0 0 44px; width:44px; min-width:44px; height:44px; min-height:44px;
  margin:-10px -8px -10px 0; padding:0;
  border:0; background:transparent; color:var(--c-mut);
  font-size:15px; line-height:1; cursor:pointer;
}
/* one segment per step: SHAPE carries "you are here", FILL carries done-ness, and
   the eyebrow row above says both in words so §7's never-colour-alone holds. */
.fc-shell .fc-pos-seg:not(#solar-studio *){ display:flex; gap:3px; align-items:flex-end; height:7px; margin-top:7px; }
.fc-shell .fc-pos-seg b:not(#solar-studio *){ flex:1 1 0; min-width:0; height:4px; border-radius:2px; background:var(--c-line-strong); }
.fc-shell .fc-pos-seg b.done:not(#solar-studio *){ background:var(--c-ok-dot); }
.fc-shell .fc-pos-seg b.part:not(#solar-studio *){ background:var(--c-fill-action); }
.fc-shell .fc-pos-seg b.here:not(#solar-studio *){ height:7px; box-shadow:inset 0 0 0 1px var(--c-ink); }

/* ── MAISIE, IN THE BAR THAT IS ALWAYS ON ─────────────────────────────────────
   She was measured at 115.3 x 40 in the old top bar — under the §3 phone floor on
   BOTH axes — with her artwork nowhere near it (the old control was the glyph "⚡"
   and the words). Target 119 x 44 with her real render from js/icons.js at 34px,
   the face AND the words "Ask Maisie", because §7 forbids a glyph alone.

   THREE RULES IN style.css SET min-height ON THIS SELECTOR and the last of them
   wins on source order, so this has to out-specify rather than out-order:
     :5436  body.eng-mode .fc-ask-top{ min-height:40px }        (0,2,1)
     :8832  @media(pointer:coarse) .fc-ask-top{ min-height:44px }
     :8839  body.eng-mode .fc-ask-top{ min-height:44px }        (0,2,1)
   Same property competing with the same property, which per CLAUDE.md §7 is the
   one case where specificity genuinely decides it. body.eng-mode .fc-shell
   .fc-ask-top is (0,3,1) and beats all three; the :not(#solar-studio *) lends it
   another 100 on top, which is belt and braces rather than the mechanism.        */
body.eng-mode .fc-shell .fc-ask-top:not(#solar-studio *){
  flex:0 0 auto; margin:0 0 0 auto;
  display:inline-flex; align-items:center; gap:7px;
  min-height:44px; height:44px; padding:4px 12px 4px 5px;
  border:1px solid var(--c-line); border-radius:var(--c-r-pill);
  background:var(--c-panel); color:var(--c-ink);
  font-family:var(--c-font-body); font-weight:700; font-size:12px;
  white-space:nowrap;
}
body.eng-mode .fc-shell .fc-ask-top .sq-ai-face:not(#solar-studio *){ width:34px; height:34px; }
body.eng-mode .fc-shell .fc-ask-top b:not(#solar-studio *){ font-weight:700; }

/* ── SECTION HEADERS AS A SECOND STICKY LEVEL ─────────────────────────────────
   So 600px into Checks the reader can still see: step 3 of 7 · Checks · 31 of 86
   · AND which section they are in. --fc-barh is published by fcPosBarHeight()
   from the bar's own measured rectangle, because the bar's height is not a
   constant — a long step name wraps and the tally grows. The fallback is the
   measured 390px height, so a first paint before the measurement lands is still
   very nearly right rather than badly wrong.

   .fc-card carries NO overflow, which is the only reason this works: an
   overflow:hidden ancestor makes the card its own scroll container and a sticky
   child then sticks to a box that never scrolls. overflow:clip would not.       */
.fc-shell .fc-schedbar:not(#solar-studio *){
  position:sticky; top:var(--fc-barh, 93px); z-index:20;
  background:var(--c-panel);
}
/* THE THIRD LEVEL, and it needs the SECOND one's height as well as the first's.
   Found by looking at a screenshot rather than by reading the CSS: with both of
   these stuck at the same top, the section header sat exactly behind the count
   bar and the reader lost "which section am I in" — which is half of what the
   scrolled state is supposed to answer. Both heights are published from their own
   measured rectangles by fcPosBarHeight(), because neither is a constant: the bar
   grows when a step name wraps and the count bar grows when the Next-unanswered
   button wraps under the count. */
/* ⚠ v825: the background here is --c-surface, matching the base .fc-insec-h rule
   above. This selector is LATER in the file and more specific, so leaving it at
   --c-panel would have silently reverted the header band to the rows' own ground
   the moment a header stuck — the two rules setting the SAME property is exactly
   the case where specificity decides it (CLAUDE.md §7), so both say the same
   thing on purpose rather than relying on which one runs. */
.fc-shell .fc-insec-h:not(#solar-studio *){
  position:sticky; top:calc(var(--fc-barh, 93px) + var(--fc-schedh, 0px)); z-index:19;
  background:var(--c-surface);
}

/* ── THE HAMBURGER CANNOT BURY THE ONE THING THAT MUST ALWAYS BE VISIBLE ──────
   MEASURED, not reasoned: #menu-btn is position:fixed, z-index 120, 44x44 at
   (10,12). While the rail was static it scrolled away and nothing was ever under
   the button. The step bar is sticky at top:0 by design, so from the first pixel
   of scroll the button sat on top of it and covered "STEP 3 OF 7" and the left
   third of the step name — the exact two facts PAGE_PURPOSE names as the #1
   immediately-visible item on this screen.

   ⚠ MY FIRST ANSWER WAS TO HIDE IT, FOLLOWING css/style.css:8340'S OWN PRECEDENT
   (`body:has(.modal.open):not(.menu-open) #menu-btn{ display:none }`), on the
   reasoning that this screen carries its own complete navigation — a back chevron
   and the five-tab field bar — so nothing would become unreachable.
   THAT WAS WRONG AND A GATE CAUGHT IT. check-cert-mobile-batons.mjs drives the
   burger WITH A CERTIFICATE OPEN and asserts the drawer opens from it; hiding it
   turned two of its checks red. The button is load-bearing on exactly this screen
   and §8 means it stays. The gate was right and the reasoning above was a
   plausible argument for removing a feature, which is the thing §8 exists to stop.

   SO IT MOVES INSTEAD OF GOING AWAY. It drops to just under the bar, where it
   floats over the card exactly as it floats over scrolling content on every other
   screen in the app. It stays visible, stays 44x44, stays tappable, and the drawer
   still opens — the gate is green again — while the two facts underneath it are
   never covered. The offset is the bar's own measured height, not a constant, for
   the same reason the sticky levels are. */
body.eng-mode:has(#certs-body .fc-shell) #menu-btn{
  top: calc(var(--fc-barh, 93px) + 8px + env(safe-area-inset-top, 0px));
}

/* ── THE EDITOR IS TALLER THAN THE SCREEN IT HAS TO FIT ───────────────────────
   MEASURED at 390×844: back row + status pill (~44px) + .fc-pos (93px, its own
   measured height, per fcPosBarHeight above) + .fc-footbar (48px button +
   64px reserved clearance for #fa-tabbar underneath it) + #fa-tabbar itself
   (49px + safe-area) all stack before a single checklist row is on screen.
   The bottom two are the fixable ones: #fa-tabbar is My day / Jobs / Certs /
   Alerts / More, and every one of those destinations is a full navigation away
   from the certificate you are mid-way through filling in — the editor is not
   a tab, it is a task entered FROM the Certs tab, and it already carries its
   own way out (the back chevron in .fa-topbar2) and its own way through (the
   Prev/Next pair in .fc-footbar). Keeping the five-tab bar mounted underneath
   a task screen buys back nothing: nobody taps "Jobs" mid-checklist without
   backing out of the certificate first regardless of whether the tab is
   visible, because engModeOn's own showView() call is what tears this editor
   down. So it is not reachable navigation, it is 49px+ of screen doing nothing
   but sit there.
   SCOPE: `.fc-shell:not(.fc-listshell)` is the editor and the list's own error
   card, NOT the list itself — .fc-listshell is the front door reached FROM the
   tab bar (v775, above) and stays exactly as it is; hiding the bar there would
   strand a user who landed on an empty list with no other way to the other
   four tabs. Same body.eng-mode gate as #menu-btn above, so this can never fire
   on the desktop skin, which never mounts #fa-tabbar in the first place.
   .fc-footbar drops its 64px clearance to 0 in the same breath: that clearance
   exists solely so its buttons do not sit under the tab bar's own hit area, and
   with the tab bar gone the reservation is dead space with nothing under it. */
body.eng-mode:has(#certs-body .fc-shell:not(.fc-listshell)) #fa-tabbar{ display:none; }
/* v833: the floating alert bell rides on the tab bar's own clearance — it hides
   in lockstep so it never floats alone once the bar underneath it is gone. */
body.eng-mode:has(#certs-body .fc-shell:not(.fc-listshell)) #fa-alertbell{ display:none; }
body.eng-mode:has(#certs-body .fc-shell:not(.fc-listshell)) .fc-footbar{
  bottom: env(safe-area-inset-bottom, 0px);
}

/* ── THE ALL-STEPS SHEET ──────────────────────────────────────────────────────
   Every row: a glyph, the step's name, and a WORD — Done / 31 of 86 / Not
   started. Never a step number (that is what the old chips carried, and it said
   nothing about what was left) and never colour alone.                         */
.fc-stepsheet .fc-sr:not(#solar-studio *){
  display:flex; align-items:center; gap:10px; width:100%;
  min-height:52px; padding:6px 14px; border:0; border-top:1px solid var(--c-line);
  background:transparent; color:var(--c-ink); text-align:left; font:inherit; cursor:pointer;
}
.fc-stepsheet .fc-sr.here:not(#solar-studio *){ background:var(--c-surface); }
.fc-stepsheet .fc-sr-p:not(#solar-studio *){
  flex:0 0 22px; width:22px; height:22px; border-radius:50%;
  display:grid; place-items:center; font-size:12px; font-weight:700;
  background:var(--c-sys-bg); color:var(--c-mut);
}
.fc-stepsheet .fc-sr.done .fc-sr-p:not(#solar-studio *){ background:var(--c-ok-bg); color:var(--c-ok-ink); }
.fc-stepsheet .fc-sr.part .fc-sr-p:not(#solar-studio *){ background:var(--c-pipe-bg); color:var(--c-pipe-ink); }
.fc-stepsheet .fc-sr-n:not(#solar-studio *){ font-weight:700; font-size:14px; min-width:0; }
.fc-stepsheet .fc-sr-here:not(#solar-studio *){
  display:block; font-style:normal; font-weight:700; font-size:10px;
  letter-spacing:.09em; text-transform:uppercase; color:var(--c-mut);
}
.fc-stepsheet .fc-sr-s:not(#solar-studio *){
  margin-left:auto; flex:0 0 auto; font-weight:700; font-size:12px;
  font-variant-numeric:tabular-nums; color:var(--c-mut);
}

/* ── B8 · THE PREVIEW BREAKS ITS OWN MOBILE LAYOUT, AND IT IS ONE DECLARATION ──
   .cpv-head gets flex-wrap:wrap inside the sheet's @media (max-width:640px), so
   the header is MEANT to stack on a phone. The sibling .cpv-actions keeps its
   base flex:1 - flex-grow 1, flex-shrink 1, flex-basis 0% - which is never
   overridden in that block, and a flex item with basis 0 and grow 1 does not
   wrap onto its own line: it just shares the row. So the actions sit beside a
   nowrap h2 and produce a 252px two-column header, a third of a 390 viewport, at
   the one moment where an electrician is meant to actually LOOK at the finished
   document before it goes to the QS.

   Same property, shorthand against shorthand, equal specificity (0,1,0), and
   design-c.css loads after style.css - so source order decides and no
   !important is needed. Inside the same breakpoint as the rule it completes. */
@media (max-width:640px){
  .cpv-actions{ flex:0 0 100%; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   LANE F · THE FRONT DOOR
   Every rule here is scoped inside #cloud-gate. The gate is a hard-coded dark
   card in BOTH themes (css/style.css sets #0d0f13 / #14161c), which is a
   legitimate hero-moment choice under DESIGN_SPEC - but it means anything
   inside it must be measured against CHARCOAL, not against the theme. That is
   the whole reason the secondary button was a white slab in the default light
   theme: it inherited the app's themed .btn while sitting on a charcoal card.
   The frame is charcoal in both themes, so these values are fixed, exactly as
   DESIGN_SPEC says frame variants must be.
   ═══════════════════════════════════════════════════════════════════════════ */

/* v780 · the store build has no signup route, so it needs a sentence instead of
   a button. Same width and rhythm as the control it replaces. */
#cloud-gate .cg-note{
  margin:2px 0 0; padding:11px 13px; border-radius:10px;
  background:rgba(245,246,248,.05); border:1px solid rgba(245,246,248,.11);
  color:#c3c9d2; font-size:12.5px; line-height:1.5; text-align:center;
}

/* ── v783 · THE FRONT DOOR, TO THE QUALITY FLOOR ──────────────────────────────
   Everything here is measured against the CARD (#14161c), never against the
   theme, because #cloud-gate hard-codes charcoal in both light and dark. That
   is exactly the case DESIGN_SPEC §1 describes when it says a chip on the
   charcoal frame cannot use the light inks: these are fixed values, and they
   are fixed on purpose.
   The one contrast failure on the gate was .cg-foot at 3.08:1 (11px, both
   themes) — the line carrying the product's only piece of reassurance on the
   screen where a stranger decides whether to trust us. */

/* the mark. The brand's one non-negotiable asset, and the most brand-critical
   screen in the product was the only screen without it. */
#cloud-gate .cg-mark{ display:block; margin:0 auto 2px; }

/* Real labels. Both fields were placeholder-as-label, so the label vanished
   the moment anybody typed — DESIGN_SPEC §2's all-caps label step, which is
   what this scale is for. */
#cloud-gate .cg-lab{
  font-size:10px; font-weight:700; letter-spacing:.09em; text-transform:uppercase;
  color:#9aa1ab; margin:4px 0 -4px; display:block;
}
#cloud-gate .cg-form{ display:flex; flex-direction:column; gap:10px; margin:0; }

/* 16px, and this one is not a preference. PRODUCT.md names "16px+ inputs so iOS
   never zooms" as a HARD constraint on the engineer surface, and the gate broke
   it on the first screen of the store build: at 14px iOS zooms the page on
   focus, at the front door, before anybody has signed in.
   style.css sets `font:inherit` on the same selector, which expands to a
   font-size longhand at equal specificity (1,0,1) — so source order decides and
   design-c.css, loaded after, wins without !important. */
#cloud-gate input{ font-size:16px; }
#cloud-gate .cg-code{ text-align:center; font-size:26px; letter-spacing:10px; }

/* Show/hide, because a phone in gloves and rain is where passwords get typed
   wrong. 44px on the axis a thumb misses on. */
#cloud-gate .cg-pwrap{ position:relative; display:block; }
#cloud-gate .cg-pwrap input{ width:100%; padding-right:74px; }
#cloud-gate .cg-reveal{
  position:absolute; right:5px; top:50%; transform:translateY(-50%);
  min-height:44px; min-width:58px; padding:0 12px;
  background:none; border:0; border-radius:8px; cursor:pointer;
  color:#9aa1ab; font:inherit; font-size:12.5px; font-weight:700;
}
#cloud-gate .cg-reveal:hover{ color:#f5f6f8; background:rgba(245,246,248,.07); }
#cloud-gate .cg-caps{ margin:-4px 0 0; font-size:12px; color:#ffdd33; }

/* Forgot your password was 23.4px tall — under BOTH floors, and it is the
   lock-out path, tapped by somebody who is already locked out and already
   fed up. 44px everywhere; a taller link on a login card costs nothing. */
#cloud-gate .cg-link{
  min-height:44px; display:flex; align-items:center; justify-content:center;
  padding:4px 8px; border-radius:10px;
}
#cloud-gate .cg-link:hover{ background:rgba(245,246,248,.06); }

/* THE SECONDARY BUTTON WAS A WHITE SLAB IN THE DEFAULT THEME. The card is
   hard-coded charcoal but the buttons inside it are the app's themed .btn, so
   in light — which IS the default — "New here? Create an account" composited to
   pure rgb(255,255,255) and was the loudest thing on the card, louder than the
   primary action. Fixed values, because the surface behind them is fixed.
   Not .btn-amber: the primary keeps the yellow, and there is only one.

   MEASURED, NOT REASONED, AND THE FIRST ATTEMPT LOST. `#cloud-gate .cg-btn:not(.btn-amber)`
   is (1,2,0), and in LIGHT it was beaten by design-c's own
   `body.theme-light .btn:not(#solar-studio *)` at (1,2,1) — the `#solar-studio`
   inside `:not()` lends that rule an ID, which is exactly the trap CLAUDE.md §7
   records, and both rules set the SAME property (background), so specificity
   genuinely decides it. Dark passed on the first attempt only because
   `body.theme-dark` matches nothing (there is no dark class, §3), leaving just the
   (1,1,0) copy for mine to beat. Light failing while dark passed is the signature
   of that pair, and it is why this was caught by measuring rather than by reading.
   Matched selector for selector, per DESIGN_SPEC §4.4, instead of !important. */
body.theme-light #cloud-gate .cg-btn:not(.btn-amber),
#cloud-gate .cg-btn:not(.btn-amber){
  background:rgba(245,246,248,.06);
  border:1px solid rgba(245,246,248,.16);
  color:#f5f6f8;
  box-shadow:none;
}
body.theme-light #cloud-gate .cg-btn:not(.btn-amber):hover,
#cloud-gate .cg-btn:not(.btn-amber):hover{ background:rgba(245,246,248,.11); border-color:rgba(245,246,248,.3); }
#cloud-gate .cg-quiet{ opacity:.72; }
#cloud-gate .btn[disabled]{ opacity:.55; cursor:default; }

/* The message line was #3fe3ff for "Code sent" and for "That combination isn't
   right" alike — success and failure the same colour, no icon, nothing to tell
   them apart at a glance. Colour AND a word, per DESIGN_SPEC §7. */
#cloud-gate .cg-msg{ min-height:18px; font-size:13px; line-height:1.45; }
#cloud-gate .cg-msg.cg-bad{ color:#ff6e61; }
#cloud-gate .cg-msg.cg-ok{ color:#3ecf8e; }
#cloud-gate .cg-msg.cg-busy{ color:#3fe3ff; }

/* 3.08:1 -> 5.40:1 at 11px on the card, and still quieter than .cg-sub (6.94). */
#cloud-gate .cg-foot{ color:#868d97; }

/* ── THE GATE WAS UNDER THE THING IT GATES ────────────────────────────────────
   Found by looking at a screenshot: the full 21-destination sidebar was legible
   BESIDE the sign-in card, on a screen nobody had signed in to. Not a paint
   artefact — measured, then driven:

     #cloud-gate  z-index:300   (css/style.css)
     #sidebar     z-index:1000  (css/style.css:5677)
     elementFromPoint over the Home button -> the nav button, not the gate
     a real Playwright click on the nav -> view-home became view-desk,
     WITH THE GATE STILL PRESENT. A second click reached view-jobs.

   So the app behind the sign-in screen was not merely visible, it was operable
   by mouse. js/router.js guards hash routing on `!!document.getElementById
   ('cloud-gate')`, but the sidebar buttons are wired straight to showView() in
   app.js's DOMContentLoaded and never consult it. This is tool 1's §3.6 — tab
   escaping the dialog — one input device over, and fixing only the keyboard
   half would have been a half-fix.

   There is exactly one correct answer to "should a modal sign-in gate sit above
   what it gates", so this is a correctness fix and not a decision (CLAUDE.md §8).
   10060 clears the whole existing ladder, whose top is #acct-panel at 10050.
   The Solar Studio's ladder is not touched: #solar-studio is 2000 and every
   .ss-* rule is relative to it. */
#cloud-gate{ z-index:10060; }

/* ── v784 · FIRST RUN ─────────────────────────────────────────────────────────
   The welcome card asks for the three things that genuinely block a certificate
   and says WHY beside each one, because "self explanatory" is a property of the
   label, not of the field. The reasons are the design: a name, an address and a
   scheme number are things people fetch from a drawer, and being told what each
   is for is the difference between doing it now and doing it never. */
#onboard-modal .ob-req{ display:block; }
#onboard-modal .ob-why{
  display:block; font-weight:400; font-size:12px; line-height:1.45;
  color:var(--c-mut); margin:1px 0 4px;
}
#onboard-modal .ob-note{
  margin:-2px 0 4px; font-size:12.5px; line-height:1.5; color:var(--c-mut);
}
#onboard-modal .ob-logo{
  display:flex; align-items:center; gap:12px; flex-wrap:wrap;
  padding:11px 13px; border:1px solid var(--c-line); border-radius:12px;
}
#onboard-modal .ob-logo .muted{ flex:1; min-width:180px; }
#onboard-modal .modal-card{ gap:10px; }
/* the two exits sit under each other, both full width, both over the desktop
   floor and both over the phone floor - "I'll do this later" is a real answer
   and it gets a real control, not a link tucked in a corner */
#onboard-modal .btn.full{ min-height:44px; }

/* THE THIRD STATE. Steps we shipped ready are listed, not counted, and not
   crossed out - crossing them out would say the firm did them. */
.set-wiz-ready{
  margin-top:12px; padding-top:10px; border-top:1px solid var(--c-line);
  display:flex; flex-direction:column; gap:2px;
}
.set-wiz-ready-h{
  display:flex; align-items:baseline; gap:10px; flex-wrap:wrap;
  font-size:12.5px; font-weight:700; margin-bottom:4px;
}
.set-wiz-ready-h .muted{ font-weight:400; }
.set-wiz-readyrow{ min-height:34px; }
@media (pointer:coarse){ .set-wiz-readyrow{ min-height:44px; } }

/* ── v786 · BRANDING ──────────────────────────────────────────────────────────
   The colours were three tabs from the logo, all three defaulted to OUR charcoal,
   and there were three controls for one decision. One tab, two colours, and a
   preview built from the same CSS variables the real documents read - because a
   preview with its own invented swatches is decoration, and the whole complaint
   about this page was that the firm had to go and raise a quote to see a colour. */
#set-sec-branding .bd-logo{
  display:flex; align-items:center; gap:12px; flex-wrap:wrap;
  padding:11px 13px; border:1px solid var(--c-line); border-radius:12px; margin:8px 0 0;
}
#set-sec-branding .bd-logo-none{
  padding:14px 18px; border:1px dashed var(--c-line); border-radius:10px;
}
#set-sec-branding .bd-badge{
  height:52px; max-width:180px; object-fit:contain;
  background:#fff; border:1px solid var(--c-line); border-radius:8px; padding:6px;
}
#set-sec-branding .bd-grid{
  display:grid; grid-template-columns:repeat(auto-fit, minmax(0, 240px)); gap:12px; margin-top:10px;
}
#set-sec-branding .bd-col{ display:block; }
#set-sec-branding .bd-why{
  display:block; font-weight:400; font-size:12px; line-height:1.45; color:var(--c-mut); margin:1px 0 5px;
}
#set-sec-branding .bd-row{ display:flex; align-items:center; gap:8px; }
/* a colour input is a tap target like any other: 34 desktop, 44 touch */
#set-sec-branding input[type="color"]{
  width:52px; min-height:34px; padding:2px; border:1px solid var(--c-line); border-radius:8px; cursor:pointer;
}
#set-sec-branding .bd-hex{ width:110px; font-family:ui-monospace, SFMono-Regular, Menlo, monospace; }
@media (pointer:coarse){
  #set-sec-branding input[type="color"]{ min-height:44px; width:60px; }
  #set-sec-branding .bd-hex{ min-height:44px; }
  #set-sec-branding .btn.small{ min-height:44px; }
}
#set-sec-branding .bd-samp{
  display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin:10px 0 0;
  padding:10px 12px; border:1px solid var(--c-line); border-radius:12px;
}
#set-sec-branding .bd-chip{
  width:26px; height:26px; border-radius:7px; display:inline-block; flex:0 0 auto;
  box-shadow:inset 0 0 0 1px rgba(13,15,19,.35);
}
#set-sec-branding .bd-adv{ margin-top:12px; display:flex; flex-direction:column; align-items:flex-start; gap:8px; }
/* the one thing on this page worth saying loudly, and it is said as a sentence
   rather than as a colour: out of the box the firm's paperwork wears OURS */
#set-sec-branding .bd-warn{
  margin:8px 0 0; padding:10px 13px; border-radius:10px; font-size:13px; line-height:1.5;
  background:var(--c-pipe-bg); border:1px solid var(--c-line); color:var(--c-ink);
}

/* the preview. White ground in both themes, because paper is white in both themes. */
#set-sec-branding .bd-prev{ display:flex; flex-direction:column; gap:6px; margin-top:8px; }
#set-sec-branding .bd-prev-lab{
  font-size:10px; font-weight:700; letter-spacing:.09em; text-transform:uppercase; color:var(--c-mut);
}
#set-sec-branding .bd-quote,
#set-sec-branding .bd-cert{
  background:#fff; color:#242a33; border:1px solid var(--c-line); border-radius:10px;
  padding:14px 16px; overflow:hidden;
}
#set-sec-branding .bd-q-top{ display:flex; align-items:center; gap:10px; flex-wrap:wrap; }
#set-sec-branding .bd-q-logo{ max-height:30px; max-width:96px; object-fit:contain; }
#set-sec-branding .bd-q-biz{
  font-family:var(--c-font-display, inherit); font-size:19px; font-weight:700;
  letter-spacing:-.4px; color:var(--cqa); flex:1; min-width:0;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
#set-sec-branding .bd-q-eyebrow{
  font-size:9px; letter-spacing:2px; font-weight:800; color:var(--cqa);
  border:1.5px solid var(--cqa); border-radius:6px; padding:3px 8px; flex:0 0 auto;
}
/* the real masthead rule: 64% the firm's main colour, then their pop.
   v799: the tail was #ffdd33 here too, and this block's whole promise is "this
   is the real thing — the same CSS variables your quotes read". So when the
   document stopped wearing our yellow this preview had to stop with it, or it
   would be showing a firm a masthead they will never send. Kept selector for
   selector the same as css/style.css's .cq-head::after, fallback and all. */
#set-sec-branding .bd-q-rule{
  height:4px; border-radius:2px; margin:10px 0 12px;
  background:linear-gradient(90deg, var(--cqa) 0 64%, var(--cqa2,var(--cqa)) 64% 100%);
}
#set-sec-branding .bd-q-th{
  display:flex; justify-content:space-between; font-size:11px; font-weight:800;
  letter-spacing:.06em; text-transform:uppercase; color:#5a6069;
  border-bottom:2px solid var(--cqa); padding-bottom:6px;
}
#set-sec-branding .bd-q-tr{ display:flex; justify-content:space-between; font-size:12.5px; padding-top:8px; }
#set-sec-branding .bd-c-band{
  background:var(--cbar); color:#fff; text-align:center; font-weight:800;
  font-size:10.5px; letter-spacing:.05em; padding:8px 10px; border-radius:4px;
}
#set-sec-branding .bd-c-rule{
  height:3px; border-radius:2px; margin:5px 0 12px;
  background:linear-gradient(90deg, color-mix(in srgb, var(--cacc) 24%, #fff), var(--cacc));
}
#set-sec-branding .bd-c-bar{
  background:var(--cbar); color:#fff; font-weight:800; font-size:10px;
  padding:6px 10px 6px 22px; border-radius:3px; position:relative;
}
#set-sec-branding .bd-c-bar i{
  position:absolute; left:9px; top:50%; transform:translateY(-50%);
  width:7px; height:7px; border-radius:50%; background:var(--cacc);
}
#set-sec-branding .bd-prev-note{ margin:2px 0 0; }
.bd-xref{ margin-top:10px; }
/* the firm's own colour, measured against paper. Never a block, always a fact —
   a brand colour under 4.5:1 makes the small print on their own invoice hard
   work, and they should hear that from us and not from a customer. */
#set-sec-branding .bd-ratio{
  display:block; font-size:11.5px; line-height:1.45; font-weight:400; margin-top:5px;
}
#set-sec-branding .bd-ratio-ok{ color:var(--c-ok-ink); }
#set-sec-branding .bd-ratio-low{ color:var(--c-mut); }
@media (pointer:coarse){ #set-sec-branding select{ min-height:44px; } }

/* ── v787 · THE SETTINGS QUALITY FLOOR ────────────────────────────────────────
   Tool 1's remaining list, measured as rendered rectangles at 1440 and 390 with
   backgrounds composited, light first. */

/* A GLYPH IS NOT A WORD, AND THIS ONE WAS UNDER AA AS WELL.
   .au-ready-tick told the whole state with a ✓ or a ✗ and a colour. The cross
   measured 3.64:1 at 11px on its OWN tinted pill (rgb(249,216,216) composited,
   not plain white — measuring it against white would have read 4.83 and invented
   a pass). The word carries the state now; the mark is decoration and is
   aria-hidden. The ink is fixed anyway, because a decorative mark that cannot be
   made out is still a defect. Light #A32A22 on that tint = 5.43:1; dark
   #FF6E61 on its own = 4.95:1. */
.au-ready-word{
  font-size:10px; font-weight:700; letter-spacing:.09em; text-transform:uppercase;
  padding:2px 7px; border-radius:999px; margin-left:6px; white-space:nowrap;
  background:var(--c-sys-bg); color:var(--c-mut);
}
.au-ready-row.ok .au-ready-word{ background:var(--c-ok-bg); color:var(--c-ok-ink); }
.au-ready-row.no .au-ready-word{ background:var(--c-bad-bg); color:var(--c-bad-ink); }
body.theme-light .au-ready-row.no .au-ready-tick{ color:#A32A22; border-color:#A32A22; }
.au-ready-row.no .au-ready-tick{ color:#FF6E61; }

/* "Connected" was #059669 at 10.5px 700 on white — 3.77:1. It already says a
   word (connStateLabel), so this half is contrast only. DESIGN_SPEC's own light
   ok-ink is #04785C: 5.46:1. And "Partly connected" was NOT on anybody's list
   and is worse — var(--amber) is #b08900, 3.27:1 on white. --c-pipe-ink is
   #8A6800, 5.17:1. Both are fixed here because they are the same defect. */
body.theme-light .conn-pill-on{ color:var(--c-ok-ink); }
body:not(.theme-light) .conn-pill-on{ color:var(--c-ok-ink); }
body.theme-light .conn-pill-part{ color:var(--c-pipe-ink); }
body:not(.theme-light) .conn-pill-part{ color:var(--c-pipe-ink); }

/* THE TAP FLOOR ON SETTINGS, against Jamie's 30 Jul ruling and not against a
   blanket number: irreversible controls go to 44px on touch, everything else
   keeps 30px. None of these is irreversible — a removed bank holiday is one tap
   to add back, a help bubble does nothing at all — so what is being fixed is not
   30px, it is SEVENTEEN. .bh-x renders 17.2×17 and .sq-i renders 17×17, half the
   desktop floor, and that is not a Tier-2 acceptance, it is a miss.
   Grown with a pseudo-element rather than padding, so the chip they sit inside
   does not reflow — the same instrument design-c already uses for
   #marketing-body .sq-i, generalised to the surface that needed it too. */
#view-settings .bh-x, #view-settings .sq-i{ position:relative; }
#view-settings .bh-x::after, #view-settings .sq-i::after{
  content:""; position:absolute; inset:calc((var(--c-tap) - 17px) / -2);
}
/* AND THE TWO PART COMPANY ON TOUCH, because a grown hit area has a limit and
   hit-testing it found it. Six points around each control, elementFromPoint,
   scrolled into view: at 34px both clear 6/6, but at 44px two neighbouring 17px
   controls each grown to 44 OVERLAP, and the edge points start resolving to the
   control next door — 5/6 and 4/6, honestly measured.

   .sq-i keeps the hit area and nothing more, on the precedent already recorded a
   few hundred lines above for #marketing-body .sq-i: it is a help bubble sitting
   INSIDE a sentence, which is WCAG 2.5.8's own inline exception, and padding it
   to 44px would break the paragraph it lives in. Contradicting that decision here
   because a different surface was measured would be the design drifting one
   honest exception at a time.

   .bh-x has no such excuse — it is a control in a chip, not a word in a sentence
   — so on touch it gets a REAL 44px box. The chip reflows around it and the
   neighbours move apart, which is the only way two adjacent controls can both
   have a thumb-sized target. It costs vertical space on the bank-holiday list and
   that is the right trade. */
@media (pointer:coarse), (max-width:820px){
  #view-settings .sq-i::after{
    inset:calc((var(--c-tap-touch) - 17px) / -2);
  }
  #view-settings .bh-x{
    min-width:var(--c-tap-touch); min-height:var(--c-tap-touch);
    display:inline-grid; place-items:center;
  }
  #view-settings .bh-x::after{ content:none; }
  /* on a phone every settings button clears the floor. 30px is the accepted
     desktop baseline under the ruling; a thumb is not a mouse. */
  #view-settings .btn.small, #view-settings .btn, #view-settings select,
  #view-settings input:not([type=checkbox]):not([type=radio]):not([type=file]):not([type=color]){
    min-height:var(--c-tap-touch);
  }
}
/* the tax-code matrix: nine inputs that carried no label of any kind, on the tab
   that holds the bank details. The labels are in the markup now (scope=row,
   scope=col and an explicit pair on each control); this is only their geometry,
   lifted off an inline style so the touch rule above can reach them. */
#view-settings .tax-code-in{ width:100%; min-width:238px; min-height:var(--c-tap); }

/* ── v816's TICK, AND THE BARE-LABEL TRAP FOR THE SECOND TIME ─────────────────
   "What the team can do on the app" (setSecAppPerms, js/app.js) shipped today as a
   bare <label class="wide app-perm"> with a checkbox inside it. That is exactly the
   trap already written up a thousand lines below against .insp-defer-tick, and it
   cost TS-016 this time rather than a screenshot.

   TWO CORRECT RULES, DIFFERENT PROPERTIES, AND NEITHER IS WRONG — CLAUDE.md §7:
     · css/style.css:173  label{ display:flex; flex-direction:column }
       sets FLEX-DIRECTION. A column flex container leaves align-items at its
       initial `stretch`, so the tick is stretched along the CROSS axis and becomes
       as wide as the whole settings form — measured 735px of used width.
     · css/style.css:172  input[type=checkbox]{ width:auto; transform:scale(1.2) }
       sets TRANSFORM. It is the house look for every tick in the app and it is
       right. But a transform scales the RENDERED box about its centre, so those
       735px became 882px, bleeding 73.5px past each edge of a 742px form.
   They never compete — they set different properties — so no amount of specificity
   or source order was ever going to decide this, and nothing was overriding
   anything. The stretch is the whole defect and the scale only multiplies it.

   Measured at the seven widths TS-016 drives, light and dark: only 1024 is narrow
   enough for the right-hand bleed to clear the viewport (light 1059, dark 1068 —
   35 and 44 over). At 1280 and up the same 903.6px box lands at 1078 and falls
   inside; at 768 and down the sidebar is gone and the form is narrower still.

   The fix is to stop the STRETCH, which is one honest line: make the label the row
   it was always meant to be. The tick's own width is deliberately NOT touched —
   overriding it to `auto` closes the overflow just as well but flattens the 20px
   phone square at css/style.css:4377 into a 15.6x24 oblong. Left alone it renders
   15.6x15.6 on desktop and 24x24 on touch, square in both.
   Nothing here can reach the Solar Studio: .app-perm exists in one function in
   js/app.js and nowhere inside #solar-studio, so it needs no :not() guard. */
.app-perm{
  flex-direction:row; align-items:center; gap:10px;
  min-height:var(--c-tap); cursor:pointer;
  font-size:13px; font-weight:500; color:var(--c-ink);
  text-transform:none; letter-spacing:0;
}
.app-perm input[type=checkbox]{ flex:0 0 auto; margin:0; }
@media (pointer:coarse){ .app-perm{ min-height:var(--c-tap-touch); } }

/* ── AND THE SAME SHAPE ONE CLASS OVER: .vat-toggle, WHICH IS THE HOUSE TICK ──
   .app-perm above is a <label> wrapping a checkbox and it was given a tap floor
   for exactly that reason. `.vat-toggle` is the SAME shape — css/style.css:177,
   a row label with a tick in it — and it is the pattern the whole product uses
   for "a tick and the words that explain it": twenty-odd sites across quota,
   hours, docs, planner, staff, certs, bookcomms, emailtemplates and marketing.
   It had no floor at all.

   MEASURED, not assumed. TS-024 hit-tests the real target rather than the
   checkbox's own 16px picture, because clicking anywhere on a <label> toggles
   the tick inside it, so the label IS the target. Driven at 1440 on the Business
   tab, elementFromPoint walking outward from the centre: TWENTY PIXELS. That is
   two thirds of the floor Jamie left standing, on the control that turns bank-
   holiday booking suggestions on and off, and on "Send it automatically" in
   Journeys — a tick that decides whether a text goes to a customer on its own.

   Nothing here is a re-raise of the tap ruling. The ruling accepts an ORDINARY
   control at 30px and this one was not making 30. It is 19-20px, which is the
   same class of miss as the 17px `.bh-x`/`.sq-i` recorded a hundred lines up:
   not a Tier-2 acceptance, an omission.

   var(--c-tap) rather than a bare 30px, so it matches the two siblings that
   already solve this exact shape — .app-perm directly above, and
   #marketing-body .vat-toggle further up the file. One shape, one instrument.
   DELIBERATELY NO (pointer:coarse) BUMP: 34px already clears the 30px floor a
   thumb needs for an ordinary control, and pushing a tick to 44 on touch would
   be OVER-applying the ruling, which scripts/qa_tap_floor_irreversible.py fails
   in that direction on purpose.
   min-height competes with nothing — neither style.css:175 `label{}` nor
   style.css:177 `.vat-toggle{}` sets any height at all, so this is not two rules
   fighting and no specificity is in play (CLAUDE.md §7: read what property each
   rule sets BEFORE reasoning about which wins).
   Nothing here can reach the Solar Studio: `.vat-toggle` appears in no
   js/solar*.js and nowhere inside #solar-studio, so it needs no :not() guard. */
.vat-toggle{ min-height:var(--c-tap); }

/* ══ TS-015 · THE CONTRAST SWEEP'S REMAINING FAILURES ═════════════════════════
   The browser gate's composite-aware sweep over 20 views x 2 themes. Measured
   LIGHT first, backgrounds composited, and every number below re-measured in the
   browser after the rule was written — not predicted.

   ⚠ TWO OF THE TWENTY-ONE ROWS ON THE OLD WORKLIST WERE THE GATE'S OWN ARITHMETIC,
   AND THEY ARE FIXED IN scripts/qa_browser_validate.py, NOT HERE.
     · `color-mix(in srgb, …)` serialises as `color(srgb 0.02 0.59 0.41 / .14)` and
       those components are 0-1, NOT 0-255. The sweep's parser read them as
       rgb(0,1,0) — near BLACK — and the whole Journeys board is built out of
       color-mix backgrounds. It published `.pj-add.sm` at 3.23:1 when it renders
       at 15.9:1, and it HID `.pj-ico` ⏳ in dark at a reported 4.42:1 when the true
       figure is 3.51:1. PAGE-CHECKLIST warns the compositing trap "invents a PASS
       as readily as a failure"; this one did both, in the same sweep.
     · `.ca-u`, the "/h" on the Business Costs answer, was published at 1.00:1 —
       "invisible text". It is a child of a background-clip:text element, so it
       INHERITS a transparent fill while its glyphs are painted by the parent's
       gradient. A pixel read of that box gives #50E2ED on #15181E, 11.38:1. The
       guard written to catch exactly that was `+cs.webkitTextFillColor === 0`,
       which is `NaN === 0`, which is false, so it had never once fired.
   Nothing is changed in the app for either. Fixing the app there would have been
   fixing the measurement.

   Nothing in this block can reach the Solar Studio: `.so-ph-*`, `.so-phone`,
   `.so-plat-ico`, `.pj-*` and `.board-band-n` appear in no js/solar*.js and
   nowhere inside #solar-studio. The :not() guard is carried anyway, because it is
   also what wins the two cascades noted below.
   ───────────────────────────────────────────────────────────────────────────── */

/* 1 · THE SOCIAL PREVIEW PHONES ARE A FIXED LIGHT MOCK, SO THEY CARRY THE LIGHT
   PALETTE — css/style.css:7066 `.so-phone{background:#fff;color:#111}`, whose own
   comment says they deliberately mimic the real feeds. That is why "No photo yet"
   (1.71:1) and "Your caption shows here…" (1.92:1) appear in BOTH theme lists at
   IDENTICAL numbers: #bbb on #f2f2f2 and on #fff, and the page theme never
   touches them.

   So a plain `color:var(--c-mut)` is the wrong fix and was measured as such: in
   dark that token is #9AA1AB, which on the mock's #f2f2f2 is 2.30:1 — one failure
   swapped for another. The token is re-declared on the mock instead, which is the
   honest statement of what the surface is: a light panel that does not follow the
   page. Both values are the palette's own --c-mut, verbatim from :root and from
   the body:not(.theme-light) block at the top of this file.

   ⚠ AND THE TIKTOK MOCK IS BLACK, css/style.css:7088. It is NOT in the failure
   list because #bbb on #111 already measures 9.84:1 — so the naive one-liner
   would have broken a passing surface, taking it to 3.31:1, and the gate would
   never have said so because its fixture only selects Instagram and Facebook.
   It gets the dark muted, and lands at 7.25:1. */
.so-phone:not(.tt):not(#solar-studio *){ --c-mut:#5F6774; }   /* :root's --c-mut */
.so-phone.tt:not(#solar-studio *){ --c-mut:#9AA1AB; }         /* the dark block's */
.so-ph-noimg:not(#solar-studio *),
.so-ph-muted:not(#solar-studio *){ color:var(--c-mut); }
/*  measured after:  ig/fb "No photo yet" 1.71 -> 5.10 · "Your caption…" 1.92 -> 5.71
                     tt   "No photo yet" 9.84 -> 7.25 · "Your caption…" 9.84 -> 7.25
    all four in BOTH themes, because the mock does not follow the theme.          */

/* 2 · THE JOURNEY STEP GLYPH. `.pj-ico` is eight of the light failures and one of
   the dark ones (⚡ 2.71 · 🔀 3.10 · ⏳ 3.38 · 💬 📥 ⚙️ ✅ ⭐ 3.84 light, ⏳ 3.51 dark).

   THE INK IT IS BEING MEASURED ON PAINTS ALMOST NOTHING. These are colour-emoji
   glyphs drawn from the system font's own bitmaps: forcing `color:#FF00FF` on one
   and diffing the screenshots changed 0 of 900 pixels, in both themes. The fix is
   still right, and not merely to satisfy a measurement — `color` is what paints
   these on any stack WITHOUT a colour emoji font, where they fall back to
   monochrome outlines, and the value it inherits today would render those at 2.7:1.
   The glyph is decoration either way: `.pj-t` names the step in words beside it,
   and js/planner.js now marks it aria-hidden.

   ⚠ SPECIFICITY, AND IT IS CLAUDE.md §7's CASE EXACTLY — BOTH RULES SET `color`,
   so they genuinely compete and the ID inside :not() is the whole story. A bare
   `.pj-ico{color:…}` is (0,1,0) and LOSES to css/style.css:4837
   `.pj-action .pj-ico` at (0,2,0); it was tried and measured no change. With the
   solar guard this is (1,1,0) and wins outright. Remove the guard and the rule
   silently stops working. */
.pj-ico:not(#solar-studio *){ color:var(--c-ink); }
/*  measured after:  light 14.89 / 13.18 / 14.18 / 14.63 across the four node
                     types · dark 13.56 / 9.38 / 13.74 / 12.29                    */

/* 3 · THE FORK'S BRANCH LABEL. `.pj-lab.yes` — the word YES on a journey fork,
   which is load-bearing: it says which lane of the branch you are reading.
   css/style.css:4854 puts --green on a 14% --green tint; composited over
   .pj-lane and .pj-canvas that is #059669 on #D6EAE3, 3.01:1 at 10.5px/700.
   (The old worklist said 1.74 — that was the color-mix parse bug above.)

   Both properties have to move together. --c-ok-ink on the EXISTING 14% tint
   measures 4.35 and still fails; it is the ground lightening to --c-ok-bg's 10%
   that carries it over. The green tint is kept rather than flattened to --c-panel
   (which would measure 5.46) because the tint is what distinguishes the YES lane
   from the NO one at a glance, and the ruling is that status is a colour AND a
   word — losing the colour to buy contrast trades one defect for another.
   Scoped to .yes so .pj-lab.no and .pj-lab.tmo, neither of which failed, do not
   move. */
.pj-lab.yes:not(#solar-studio *){ color:var(--c-ok-ink); background:var(--c-ok-bg); }
/*  measured after:  light 3.01 -> 4.58 · dark 7.51 -> 7.75                        */

/* 4 · THE CALENDAR BOARD'S HOURS LINE, and it is a light-theme repair that was
   never scoped. Two rules, SAME specificity (0,1,0), decided by source order:
   css/style.css:4668 `.board-band-n{color:var(--muted)}` and css/style.css:6975
   `.board-band-n{color:#4c535e}` — the later one wins. Its own heading says it
   was written because the numerals "sat at 4.45 on the colour-mixed beige", i.e.
   it is a LIGHT fix, and with no `body.theme-light` on it, it forces a light ink
   onto the dark theme too: #4C535E on #1C1F27, 2.12:1. The patch caused the
   failure it is not in.

   One token for both themes puts the class back on the closed palette. Stated
   honestly: this also moves LIGHT, 7.04 -> 5.18. That is a real reduction and it
   still clears 4.5 comfortably; the alternative — scoping the fix to dark and
   leaving an off-palette hex driving light — keeps a number nobody can reason
   about from the tokens. "0h of labour booked" is the total labour on the board
   that week, so it is not decoration. */
.board-band-n:not(#solar-studio *){ color:var(--c-mut); }
/*  measured after:  light 7.04 -> 5.18 · dark 2.12 -> 6.32                        */

/* 5 · `.so-plat-ico` — white on the platform brand grounds, 4.23:1 on Facebook
   blue and 4.34:1 on Instagram pink, both a whisker under 4.5.
   FIXED IN js/social.js, NOT HERE, AND DELIBERATELY. The colour arrives as an
   inline `style="--pc:${p.col}"` on the badge itself (js/social.js:204), and an
   inline custom property cannot be overridden from a stylesheet — the fix would
   have to set `background` directly, which means putting two third-party brand
   hexes into this file, and §7's closed palette is the one rule this pass is not
   going to bend for a logo. `p.col` has exactly one consumer in the whole tree,
   so darkening the two values at source is a smaller change with no blast radius
   and leaves the palette clean. See the note there. */

/* ══════════════════════════════════════════════════════════════════════════════
   LANE A · CERTIFICATES ON THE DESKTOP — tool 2, frontend-design

   The surface where the QS signs, and the last major certificate surface never
   redesigned. Tool 1's headline: desktop circuit entry was a 31-column,
   372-input spreadsheet, 1,786px inside a 1,124px scroller, so 37% of a signed
   schedule of test results sat off-screen. Everything below either fixes that or
   fixes the accessibility floor underneath it.

   Nothing here can reach the Solar Studio: every selector is scoped to
   #certs-body, .cert-*, .ckt-* or .cpill-*, none of which exists inside it.
   ══════════════════════════════════════════════════════════════════════════════ */

/* ── STATUS PILLS ON THE CLOSED PALETTE ───────────────────────────────────────
   Measured before: UNSATISFACTORY and the number-clash pill rendered #dc2626 on
   #14161c = 3.74:1 in dark, against a 4.5:1 floor. Those are the two most
   consequential words the list can print — the verdict that an installation is
   dangerous, and the warning that two devices minted the same statutory number.

   The fix is one line per state and it is the palette doing the work. style.css
   drives the pill from --pc two different ways (light fills with
   color-mix(--pc 75%, #000) and white text at :4332/:1736; dark renders --pc as
   text on the panel at :624). Because --c-*-ink is ALREADY redefined per theme,
   setting --pc to the token makes both treatments correct at once — there is no
   second rule and no theme fork to keep in step.

   These are set on a CLASS, not inline, so the cascade can reach them. The old
   inline `style="--pc:#dc2626"` beat every selector, which is why it had to come
   out of js/certs.js at the same time. */
.status-pill.cpill-bad  { --pc: var(--c-bad-ink);  }
.status-pill.cpill-ok   { --pc: var(--c-ok-ink);   }
.status-pill.cpill-pipe { --pc: var(--c-pipe-ink); }
.status-pill.cpill-sys  { --pc: var(--c-sys-dot);  }
/* Light fills the pill and sets white text with !important at style.css:1736.
   Matched selector-for-selector at (0,3,1) rather than answering with a second
   !important, per DESIGN_SPEC §4 rule 4. */
body.theme-light .status-pill.cpill-sys{ background: var(--c-sys-ink); }

/* ── STATE BANNERS: A TINTED PANEL, NEVER A COLOURED LEFT-BORDER ──────────────
   DESIGN_SPEC §1: "Never a coloured left-border (the audit found 45 of them and
   they read as AI-generated). Always a tinted chip, or a dot plus a word."
   Three of them were here, and they were the three highest-stakes messages in
   the module: awaiting-QS, QS-sent-this-back, and the number clash. Two also
   failed the 3:1 non-text floor — the yellow measured 1.25:1 in light.

   Replaced by a tinted ground plus a chip that says the state in words, so the
   meaning survives with no colour at all. */
.cert-banner{
  margin-bottom:10px;
  border:1px solid var(--c-line);
  border-radius:var(--c-r-card);
}
.cert-banner .cb-tag{
  display:inline-flex; align-items:center; gap:6px;
  font-size:10px; font-weight:700; letter-spacing:.09em; text-transform:uppercase;
  padding:3px 9px; border-radius:var(--c-r-pill); white-space:nowrap;
}
.cert-banner .cb-tag .dot{
  width:7px; height:7px; border-radius:50%; flex:none;
  box-shadow: inset 0 0 0 1px rgba(13,15,19,.28);
}
.cert-banner.cb-pipe{ background:var(--c-pipe-bg); }
.cert-banner.cb-pipe .cb-tag{ color:var(--c-pipe-ink); background:var(--c-panel); }
.cert-banner.cb-pipe .cb-tag .dot{ background:var(--c-fill-action); }
/* v825 - QS REVIEW STAYS ON SCREEN WHILE YOU SCROLL. Jamie's call, 9 Aug: a
   real desktop split-view means rebuilding how the whole certificate editor
   lays out - too much risk on a page this size, this close to launch. The
   actual problem was narrower: Approve/Send-back scrolled out of view on a
   long certificate, so the QS had to scroll back to the top to act. cb-pipe
   is used in exactly one place (js/certs.js, the awaiting-qs banner) so this
   rule is unconditional - no new class, no HTML change, so the mutant gate in
   check-cert-desktop.js that replaces the exact string
   class="panel cert-banner cb-pipe" stays intact. top:56px matches the same
   offset .bell-lane already uses for the same reserved topbar height
   (main{padding-top:56px !important}) - both stick flush under it, and the
   bell (top-right, ends ~48px) never reaches this banner's start (56px), so
   there is no overlap between them. z-index:40 sits below the sidebar (1000),
   every modal (1150+) and the bell (95), and above ordinary page content -
   this only needs to clear the certificate body scrolling underneath it. */
.cert-banner.cb-pipe{ position:sticky; top:calc(56px + env(safe-area-inset-top, 0px)); z-index:40; box-shadow:0 2px 10px rgba(13,15,19,.14); }

/* v824 — the readiness tray (certReadyTray, js/certs.js). Same banner family
   and same layout rule as cb-pipe (flex row; .cb-spacer already has a general
   .cert-banner rule at :5848 that pushes trailing buttons right — nothing new
   needed here for that) but on --c-ok-bg/--c-ok-ink — the token already used
   everywhere else in the product for "this checked out clean" (fc-lst-ok,
   fc-insec-n-done, au-ready-row.ok). No new colour introduced. */
.cert-banner.cb-ready{ display:flex; gap:10px; align-items:center; flex-wrap:wrap; background:var(--c-ok-bg); }
.cert-banner.cb-ready .cb-tag{ color:var(--c-ok-ink); background:var(--c-panel); }
.cert-banner.cb-ready .cb-tag .dot{ background:var(--c-ok-dot); }
.cert-banner.cb-bad{ background:var(--c-bad-bg); }
.cert-banner.cb-bad .cb-tag{ color:var(--c-bad-ink); background:var(--c-panel); }
.cert-banner.cb-bad .cb-tag .dot{ background:var(--c-bad-dot); }

/* ── THE BAND BAR ─────────────────────────────────────────────────────────────
   Four views over one schedule. Reads as a control, not as decoration, because
   the thing it does — hide 22 of 31 columns — has to be obvious enough that
   nobody thinks the columns are gone. */
.ckt-bandbar{
  display:flex; align-items:center; gap:6px; flex-wrap:wrap;
  margin:10px 0 8px; padding-top:8px;
  border-top:1px solid var(--c-line);
}
.ckt-bandlab{
  font-size:10px; font-weight:700; letter-spacing:.09em; text-transform:uppercase;
  color:var(--c-mut); margin-right:2px;
}
.ckt-bandbar .ckt-band{
  min-height:var(--c-tap);
  display:inline-flex; align-items:center; gap:6px;
}
.ckt-bandbar .ckt-band.on{
  background:var(--c-fill-action);
  border-color:var(--c-fill-action);
  color:var(--c-action-ink);
  font-weight:700;
}
/* v799: this rule was correct and losing. css/style.css carries a bare global
   `.tab-ct{ color:var(--c-muted) !important }`, so the ink here was overruled
   and the count rendered --c-mut on the yellow chip, then diluted again by the
   .72 — 2.69:1 in light and 1.61:1 in dark, measured composited. You cannot
   out-specify an !important without one; DESIGN_SPEC allows that as a documented
   exception and this is the documentation. .85 keeps the count quieter than its
   label without taking it under the floor: 10.6:1 in both themes.
   Derive the rule it is fighting:  grep -n "^.tab-ct{" css/style.css */
.ckt-bandbar .ckt-band.on .tab-ct{ color:var(--c-action-ink) !important; opacity:.85; }

/* ── THE ROW RESULT: the column that made the bands possible ──────────────────
   A dot AND a word (DESIGN_SPEC §7 — never colour alone), pinned left in every
   band, so a QS can scan a board for failing circuits without dragging sideways
   past fifteen columns. The reason is on the title, so it is never a bare
   adjective.

   Tinted with the *-bg token and inked with the *-ink token — deliberately NOT
   the same token on both, which is the trap PAGE-CHECKLIST names ("never tint a
   container with the same token as a chip inside it"). */
.ckt-res{
  display:inline-flex; align-items:center; gap:5px;
  padding:2px 8px; border-radius:var(--c-r-pill);
  font-size:11px; font-weight:700; white-space:nowrap;
}
.ckt-res .dot{
  width:7px; height:7px; border-radius:50%; flex:none;
  box-shadow: inset 0 0 0 1px rgba(13,15,19,.28);
}
.ckt-res-bad { background:var(--c-bad-bg);  color:var(--c-bad-ink);  }
.ckt-res-bad  .dot{ background:var(--c-bad-dot); }
.ckt-res-warn{ background:var(--c-pipe-bg); color:var(--c-pipe-ink); }
.ckt-res-warn .dot{ background:var(--c-fill-action); }
.ckt-res-ok  { background:var(--c-ok-bg);   color:var(--c-ok-ink);   }
.ckt-res-ok   .dot{ background:var(--c-ok-dot); }
.ckt-res-none{ background:var(--c-sys-bg);  color:var(--c-sys-ink);  }
.ckt-res-none .dot{ background:var(--c-sys-dot); }

/* ── STICKY IDENTITY: what actually fixes the 662px ───────────────────────────
   In "All 31 columns" the table is still wider than the scroller, by design —
   it is the printed schedule and the office is entitled to see it that way. What
   was wrong was losing track of WHICH CIRCUIT a reading belonged to once you
   scrolled. No, Description and Result now stay pinned, so the far-right columns
   are read against their own row rather than counted across from memory.

   background is required on a sticky cell or the scrolled content shows through.
   It is --c-panel rather than transparent, and the zebra/hover rules in
   style.css paint OVER it on the td, so those still read.

   ⚠ v799 — THAT LAST SENTENCE WAS THE DEFECT, AND IT WAS WRITTEN AS A PREDICTION.
   They do paint over it. What they paint is 98% transparent, and it does not sit
   ON the panel, it REPLACES it: the zebra and the hover are the `background`
   SHORTHAND, which resets background-color outright. So every even row and every
   hovered row lost its opaque ground and the columns scrolling underneath came
   straight through the pinned ones. Measured, in both themes: sticky cells read
   rgb(255,255,255) on odd rows and rgba(16,24,40,0.02) on even. Driven: a Result
   chip on circuit 1 read "Passes 36", where 36 was the Pts value from a column
   that had scrolled underneath it.

   THE CASCADE, RESOLVED THE WAY CLAUDE.md ASKS — read what property each rule
   sets FIRST. All of them set `background`, so they genuinely compete, and then
   it is specificity: the sticky rule below is (0,2,1); style.css's zebra is
   (0,2,3) dark and (0,3,4) light; its hover the same. The sticky rule loses on
   every one. Source order cannot help, because these are not different
   properties — that is the whole trap.

   THE FIX IS NOT !important AND IT IS NOT MAKING THE PINNED CELLS FLAT. The
   zebra is doing real work: banding is how a QS keeps a row while dragging
   sideways, and stripping it from three columns would cost the very thing the
   pinning exists to give. So the tint is re-stated ON the pinned cells as a
   LAYERED background — the same tint, over an opaque --c-panel, in one
   declaration. Identical pixels on an unscrolled table, opaque underneath.
   That idiom is not new here: .ckt-unconf's pinned cells have used it since the
   scan sweep landed. It was never generalised to the zebra.

   A RECTANGLE TEST CANNOT SEE ANY OF THIS. The cells are positioned perfectly
   and report zero overlap; tool 2's own gate passed.
   scripts/check-cert-p1-pixels.mjs asserts the COMPOSITED PIXEL instead: it
   samples the same patch of a pinned cell with the table at rest and at full
   right-scroll and requires the two to be byte-identical, in both themes, on odd
   rows and even — AND requires the even patch to still differ from the odd one,
   so "make it opaque" cannot be satisfied by flattening the banding away. */
.ckt-table thead th:nth-child(-n+3),
.ckt-table tbody td:nth-child(-n+3){
  position:sticky; z-index:2; background:var(--c-panel);
}
/* The two tints, mirrored from css/style.css so the banding is unchanged to the
   eye. Base is DARK (dark is the absence of .theme-light), exactly as style.css
   declares them. Derive the source rules rather than trusting these:
     grep -n "cert-table tbody tr:nth-child(even) td" css/style.css
     grep -n "cert-table tbody tr:hover td"           css/style.css
   check-ckt-pinned.mjs reads both files and fails if the four values drift
   apart, so a change to style.css's zebra cannot silently leave these behind. */
.ckt-table{ --ckt-zebra:rgba(255,255,255,.02); --ckt-hover:rgba(255,221,51,.05); }
body.theme-light .ckt-table{ --ckt-zebra:rgba(16,24,40,.02); --ckt-hover:rgba(255,221,51,.08); }
/* (0,4,3) — four classes beats style.css's three, so it takes the light rules at
   (0,3,4) as well as the dark ones at (0,2,3); the element counts never come into
   it, because classes are compared first.
   :not(.ckt-unconf) keeps a row still carrying its scan stamp on the pipe tint
   the block further down gives it, which is a different signal and outranks the
   zebra today. Hover is declared AFTER even so it wins on a hovered even row. */
.ckt-table tbody tr:nth-child(even):not(.ckt-unconf) td:nth-child(-n+3){
  background:linear-gradient(0deg, var(--ckt-zebra), var(--ckt-zebra)), var(--c-panel);
}
.ckt-table tbody tr:hover:not(.ckt-unconf) td:nth-child(-n+3){
  background:linear-gradient(0deg, var(--ckt-hover), var(--ckt-hover)), var(--c-panel);
}
.ckt-table thead tr:nth-child(2) th:nth-child(-n+3){ z-index:3; }
/* The three pinned columns are given EXACT widths, not left to the table's auto
   layout. A sticky `left` offset is a hard number, so it is only correct while
   the columns to its left are the width you assumed — and in a band view the
   cells stretch to fill, which would move column 3's natural position while its
   `left` stayed put and pin it ON TOP of column 2. Fixing the two widths makes
   the offsets true in every band and at every viewport, including a band narrow
   enough to scroll at 1024. */
.ckt-table th:nth-child(1),  .ckt-table td:nth-child(1){ left:0;     width:44px;  min-width:44px;  max-width:44px; }
.ckt-table th:nth-child(2),  .ckt-table td:nth-child(2){ left:44px;  width:162px; min-width:162px; max-width:162px; }
.ckt-table th:nth-child(3),  .ckt-table td:nth-child(3){ left:206px; }
/* the seam, so a pinned cell reads as pinned rather than as an overlap */
.ckt-table th:nth-child(3),  .ckt-table td:nth-child(3){
  box-shadow: 1px 0 0 var(--c-line);
}
/* ── v799: A CUT-OFF VALUE MUST LOOK CUT OFF ─────────────────────────────────
   An <input> clips its overflow dead — no ellipsis, no scrollbar, nothing. On a
   filled board five of twelve circuit descriptions are too long for the pinned
   162px column at 1440, and "Ring final — ground flo" reads as a complete circuit
   name rather than a truncated one. text-overflow does apply to an input in
   Chromium while it is not focused, so the cut becomes visible; the full value is
   on the title (js/certs.js) and on the aria-label, and clicking in still shows
   and edits the whole thing.
   Widening the column instead was measured across 834 / 900 / 1024 / 1180 / 1280
   / 1440 at 162 / 200 / 214 / 230px: 230 does clear every clipped description at
   1440, and it also makes the Test results band start scrolling there — 0 hidden
   px becomes 113. Three bands hiding zero columns at 1440 is the whole point of
   the four-view design, so the column stays at 162 and the truncation is made
   honest instead.

   ⚠ NOT ON EVERY CELL, and the first cut was. Put on all of them it made things
   WORSE, visibly, in the after-shot: "Test V" went from reading 500 to reading 5
   and an ellipsis. Those numeric inputs overflow by a FRACTION OF A PIXEL — under
   the 1px tolerance a clipping probe uses, so they had never once shown up as
   clipped — and Chrome applies the ellipsis on ANY overflow, which on a three
   character value costs two of the three.

   ⚠ AND THEN NOT ONLY ON desc, WHICH WAS THE OVER-CORRECTION. Scoping it to the
   description column left `notes` hard-clipping dead, and that is the same "reads
   as a complete value when it is truncated" dishonesty this rule exists to remove.
   So the boundary is MEASURED rather than guessed, on a filled 12-way board
   carrying the notes an electrician actually writes:

     desc    154px box · 5 of 12 clipped  · worst overflow  +61px  -> ellipsis
     notes   108px box · 11 of 12 clipped · worst overflow +110px  -> ellipsis
     wiring   76px box · 9 of 12 clipped  · worst overflow  +22px  -> HARD CLIP
     testV    52px box · 0 of 12 clipped  · overflow          0px  -> hard clip
     zs       46px box · 0 of 12 clipped  · overflow          0px  -> hard clip

   ⚠ AND `wiring` IS WHY THE RULE IS NOT "OVERFLOW IN TENS OF PIXELS". By that
   number it qualifies, and I put it in — then looked at the render, which is the
   only thing that settles it. At 76px "Twin & earth" clips to "Twin & ear"; with
   the ellipsis it becomes "Twin …", so the ellipsis costs SIX of the ten
   characters that were surviving, and "Twin …" no longer distinguishes twin and
   earth from twin and earth in conduit. That is the Test V regression again at a
   smaller scale, caught the same way and pulled back the same way.

   THE RULE, corrected: an ellipsis is worth having when the column is wide enough
   that it costs about one character out of twenty — desc and notes — and it is
   destructive when the box is narrow enough that it eats a quarter of what was
   visible. Overflow size is what makes a column a CANDIDATE. What decides it is
   how much of the value survives, and that is read off a picture, not a number.
   Add a column, render it both ways, and look.

   WHAT AN ELLIPSIS DOES NOT FIX, named rather than invented around: the TITLE
   recovers the full value on a desktop, and A TITLE DOES NOT EXIST UNDER A COARSE
   POINTER. Driven at 834 WITH touch — engModeOn is false, the field app does NOT
   take the surface, and this desktop table is what a tablet renders. There the
   ellipsis is the only on-screen signal and the only recovery is tapping into the
   field and scrolling it. That is a disclosure question for Impeccable and it is
   in the report, not answered here with a control nobody asked for. (At 390 with
   touch the field app DOES take over, so this table is not on screen at all —
   that surface is Lane B's, not this one's.) */
.ckt-table tbody td[data-f="desc"] input,
.ckt-table tbody td[data-f="notes"] input{ text-overflow:ellipsis; }
/* the result cell keeps its chip and its Confirm on one line, so a board part
   way through a scan sweep does not render as ragged rows of two heights */
.ckt-table .ckt-rescell{ white-space:nowrap; }
.ckt-table .ckt-rescell{ padding-right:10px; }
.ckt-table th.ckt-reshead{ color:var(--c-mut); }

/* ── THE FAB WAS SITTING ON THE PRIMARY FORWARD ACTION ────────────────────────
   Hit-tested at 1440x900: document.elementFromPoint inside the "Next >" button's
   own rectangle returned .ai-fab, not the button. The FAB is position:fixed at
   right:22px/bottom:22px (54x54), the step footer is sticky at the bottom, and
   they overlap by 40 of the button's 96px once the page is long enough to push
   the footer down — which the Inspections step always is. A click there opened
   the AI assistant instead of advancing the step.

   Fixed by giving the footer its own right-hand clearance rather than moving the
   FAB, because the FAB is app-wide furniture and the footer is this module's.

   ⚠ v799 — AND THE CLEARANCE WAS SWITCHED OFF AT EXACTLY THE WIDTHS THAT NEEDED
   IT. The media query below read max-width:860px, which turned the padding off
   from 860px down. But the FAB is NOT hidden at 860 — it is hidden only in
   engineer mode — and it does not move out of the way until 600px, where
   style.css lifts it to bottom:74px. So the clearance was present where the FAB
   already cleared the footer and absent where it did not.

   Swept 1440 / 1280 / 1024 / 900 / 880 / 860 / 834 / 800 / 768 / 700 / 600 /
   500 / 430 / 390 with the FAB's and the button's real rectangles, and the
   failure band is exactly 601px to 860px: the rectangles overlap at 860, 834,
   800, 768 and 700, and at 834 the FAB owns the CENTRE of "Next" rather than its
   edge, so a real mouse click opens the AI panel and the step does not move. At
   600 and below the FAB's own rect is at y 776 against a footer at 848 and there
   is nothing to clear; above 880 the padding was already doing its job.

   So the breakpoint is 600, which is style.css's own — derive it rather than
   trusting this line:  grep -n "ai-fab{ bottom:74px" css/style.css
   Keeping it at 12px below 600 matters: 82px of padding on a 390px footer that
   already carries two 96px buttons would push the page sideways, and there is
   currently zero horizontal overflow at every width. */
.cert-step-foot{ padding-right:calc(var(--fab, 54px) + 28px); }
@media (max-width:600px){
  .cert-step-foot{ padding-right:12px; }
}
/* engineer mode hides the FAB outright (css/style.css, body.eng-mode .ai-fab),
   so there is nothing to clear and the footer gets its width back. */
body.eng-mode .cert-step-foot{ padding-right:12px; }

/* ── A KEYBOARD ROUTE INTO A CERTIFICATE ──────────────────────────────────────
   The list row was <tr onclick> with no tabindex, no role and no keydown: Delete
   and Print were reachable, Open was not. Rather than bolt role="button" onto a
   table row and break its semantics for a screen reader, the certificate number
   and the job number are now real buttons that happen to look like links. */
.dt-open{
  background:none; border:0; padding:0; margin:0;
  font:inherit; color:inherit; cursor:pointer;
  text-align:left; min-height:var(--c-tap);
}
.dt-open:hover{ text-decoration:underline; }

/* ── REAL FONT WEIGHTS ONLY ───────────────────────────────────────────────────
   Satoshi ships 400/500/700 (css/style.css:7-9); Clash ships 500/600/700. The
   certificate surfaces were rendering `650 Satoshi` on 12 elements and
   `800 Satoshi` on 10, so the browser was synthesising both. Rounded to the
   weights that exist. Scoped to the certificate surfaces because these are
   app-wide rules and re-weighting the whole product is not this pass's call. */
#certs-body h3{ font-weight:700; }
#certs-body .cn-step,
#certs-body .obs-num,
#certs-body .tab-ct,
#certs-body .ct-id strong,
#certs-body .cert-banner strong{ font-weight:700; }

/* ── A FOCUS RING SHOULD NOT FADE IN ──────────────────────────────────────────
   style.css:1243 sets `transition: all .13s ease` on .btn/.chip-btn/.icon-btn/
   .nj-type, and `all` includes outline-width — so the ring animates from 0.
   That is a real (if small) problem for anyone tracking focus by eye, and it is
   the documented cause of three separate false "missing focus ring" reports on
   this project. Named properties only, scoped to this module: #certs-body can
   never contain the Solar Studio. */
#certs-body .btn,
#certs-body .chip-btn,
#certs-body .icon-btn,
#certs-body .nj-type{
  transition-property: background-color, border-color, color, box-shadow, transform;
}

/* ── THE STEPPER BROKE ITS OWN NUMBERED SEQUENCE ──────────────────────────────
   Ten chips on two lines at 1440 (measured: tops 179 and 215). The fix is not to
   shrink padding until ten fit — that only moves the width at which it breaks,
   and the six certificate types emit different step counts anyway.
   The actual fault is that TWO OF THE TEN ARE NOT STEPS. "Check" and "Show all"
   are tools, and they were flowing in the middle of a numbered 1..8 sequence, so
   when the row wrapped it wrapped through the sequence. Separated: the steps are
   one group that wraps as a unit, the tools sit at the far end and are visibly
   not part of the count. */
.cert-nav{ align-items:flex-start; }
/* display:contents, not flex. As a flex ITEM the steps group is as wide as its
   widest wrapped line — nearly the whole bar — so the tools could never sit
   beside it and always took a row of their own: 3 rows and a 118px sticky bar at
   1280, worse than the 2 it was sent to fix. Dissolving the box lets the chips
   flow directly in .cert-nav and wrap naturally, and the sequence still reads
   1..8 continuously because DOM order does that, not the wrapper. What the
   wrapper is actually for is the divider below, which is what stops the two
   tools reading as steps 9 and 10. */
.cert-nav .cert-steps{ display:contents; }
/* Deliberately NOT margin-left:auto. That claims the rest of the line, which
   pushed the tools onto a THIRD row and made a sticky bar 119px tall — worse
   than the two rows it was sent to fix. They flow straight after the last step
   and are told apart by the divider, not by position. */
.cert-nav .cert-tools{
  display:flex; gap:6px; flex-wrap:wrap;
  padding-left:11px; margin-left:5px;
  border-left:1px solid var(--c-line);
}
/* Measured at 1440: the eight step chips summed to 1,119.9px plus 42px of gaps
   in 1,158px of bar — over by 3.9px, so the sequence broke for the sake of four
   pixels. 8px padding and a 5px gap buy ~23px, which puts all eight on one line
   at 1440 with room for the badges to grow (86/86 is wider than 1/86). */
@media (min-width:1200px){
  .cert-nav{ gap:5px; }
  .cert-nav .cert-step-chip{ padding-left:8px; padding-right:8px; gap:5px; }
}
/* Below 1200 the divider costs more than it earns. Measured at 1024: the eight
   chips pack 4+4, leaving 142px on the second row, and the divided tools group
   is atomic at 191px — so it took a third row and a 119px sticky bar, where the
   flat original managed two. Dissolving the group there restores exactly the
   pre-existing flow: no regression, and the separation simply is not offered at
   a width that has no room for it. */
@media (max-width:1199px){
  .cert-nav .cert-tools{ display:contents; }
}

/* the Check chip carried its status colour as an inline hex (#34d399, outside the
   closed palette). It is a token now, and the class is what makes it green. */
.cert-nav .cert-tools .cn-dot{ background:var(--c-ok-dot); }

/* ── REAL FONT WEIGHTS, PART 2: THE DOCUMENTED !important ─────────────────────
   css/style.css:2125 sets `font-weight:650 !important` on `.panel h3, .cert-sec
   h3, ...`. DESIGN_SPEC §4/§7: you cannot out-specify an !important without one,
   and those cases are documented exceptions rather than a habit. This is one, and
   this is the documentation: Satoshi has no 650, so the browser was synthesising
   a weight on 12 headings per certificate. Scoped to #certs-body so it changes
   nothing outside this module and can never reach the Solar Studio. */
#certs-body h3{ font-weight:700 !important; }

/* ── LOADING / ERROR / OFFLINE (DESIGN_SPEC §6) ───────────────────────────────
   The desktop had an empty state and a partial error state, and no loading or
   offline state at all — the only offline rules in the tree were .fa-offline,
   the field app's. A certificate is written in a van with two bars of signal and
   reviewed in an office, so both surfaces need to say what is happening. */
.cert-skel{ display:grid; gap:8px; }
.cert-skel-row{
  height:28px; border-radius:var(--c-r-input);
  background:linear-gradient(90deg, var(--c-sys-bg) 0%, var(--c-line) 50%, var(--c-sys-bg) 100%);
  background-size:200% 100%;
  animation: cert-skel-slide 1.15s ease-in-out infinite;
}
@keyframes cert-skel-slide{ from{ background-position:200% 0; } to{ background-position:-200% 0; } }
@media (prefers-reduced-motion: reduce){
  .cert-skel-row{ animation:none; background:var(--c-sys-bg); }
}
.cert-offline{
  display:flex; align-items:center; gap:8px; flex-wrap:wrap;
  padding:9px 12px; margin-bottom:10px;
  border:1px solid var(--c-line); border-radius:var(--c-r-card);
  background:var(--c-sys-bg); color:var(--c-ink); font-size:13px;
}
.cert-offline .dot{
  width:8px; height:8px; border-radius:50%; background:var(--c-sys-dot); flex:none;
  box-shadow: inset 0 0 0 1px rgba(13,15,19,.28);
}

/* ── THE BOARD-SCAN CONFIRM SWEEP, DESKTOP ────────────────────────────────────
   Tinted with the pipeline ground because it is a state to resolve, not a fault.
   It is NOT a modal: it is part of the board panel, so closing anything cannot
   dismiss it and it survives every re-render — a scan stays pending until the
   ways are actually confirmed. */
.ckt-scanchk{
  margin:10px 0 0; padding:10px 12px;
  border:1px solid var(--c-line); border-radius:var(--c-r-card);
  background:var(--c-pipe-bg);
  display:grid; gap:7px;
}
.ckt-scanchk-head{ display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
.ckt-scanchk-note{
  font-size:12.5px; color:var(--c-ink);
  background:var(--c-panel);
  border:1px solid var(--c-line); border-radius:var(--c-r-input);
  padding:6px 9px;
}
.ckt-scanchk-acts{ display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
.ckt-scanchk-acts .btn{ min-height:var(--c-tap); }
.ckt-scan-arch{ margin:10px 0 0; }
/* a way still carrying the scan stamp reads differently from a typed one, in
   every band, without colour being the only signal — the Result cell says
   "From a photo" in words. */
.ckt-table tr.ckt-unconf td{ background:var(--c-pipe-bg); }
.ckt-table tr.ckt-unconf td:nth-child(-n+3){
  background:linear-gradient(var(--c-pipe-bg), var(--c-pipe-bg)), var(--c-panel);
}
.ckt-confirm{ margin-left:6px; min-height:26px; padding:2px 9px; }

/* ── THE BULK-ASSERT BUTTONS WERE THE FASTEST ROUTE THROUGH AN INSPECTION ─────
   Twenty of them — "all ✓" and "all N/A" on each of ten sections — sat in the
   section HEADERS, reachable without ever opening a section. So the quickest way
   through a statutory BS 7671 inspection was ten taps without reading an item,
   and the bulk-assert was more prominent and closer to hand than the act of
   inspecting.

   They are not removed: marking a genuinely inspected section in one go is a real
   and legitimate thing to do, and removing it would be losing a feature (§8).
   They are simply not offered until the section is open — which is the same click
   that puts the items themselves on screen. Collapsed, the header shows the
   progress badge and nothing to press.

   CSS rather than JS on purpose: <details open> is DOM state, not certificate
   data, so gating it in the renderer would have meant inventing a data rule
   ("record one item first") that nobody asked for and that costs a tap on every
   section an inspector HAS walked. */
.insp-sec:not([open]) .insp-quick{ display:none; }

/* the flat library picker's myth list, kept quiet until asked for */
.obs-myths{ margin-top:8px; }
.obs-myths > summary{ cursor:pointer; font-weight:700; }
.obs-myths[open] > summary{ margin-bottom:4px; }

/* ── THE ISSUE MOMENT (#7) ────────────────────────────────────────────────────
   Check / Send for QS / Mark issued were three same-weight buttons in a row with
   nothing connecting the validator to the door. The toolbar now carries the
   check's own count, read live from certCheck — the same function certQSApprove
   consults, so the two can no longer disagree — and the control that SKIPS review
   no longer looks identical to the one that goes through it. */
.ct-chk{
  display:inline-flex; align-items:center; gap:6px;
  height:40px; padding:0 12px; border-radius:var(--c-r-input);
  font-size:12px; font-weight:700; white-space:nowrap;
  border:1px solid var(--c-line);
}
.ct-chk .dot{
  width:8px; height:8px; border-radius:50%; flex:none;
  box-shadow: inset 0 0 0 1px rgba(13,15,19,.28);
}
.ct-chk-bad { background:var(--c-bad-bg);  color:var(--c-bad-ink);  }
.ct-chk-bad  .dot{ background:var(--c-bad-dot); }
.ct-chk-warn{ background:var(--c-pipe-bg); color:var(--c-pipe-ink); }
.ct-chk-warn .dot{ background:var(--c-fill-action); }
.ct-chk-ok  { background:var(--c-ok-bg);   color:var(--c-ok-ink);   }
.ct-chk-ok   .dot{ background:var(--c-ok-dot); }
/* ── v799: A TINTED CHIP MUST NOT INHERIT THE TINT OF THE BAR IT SITS ON ──────
   PAGE-CHECKLIST names this trap by name: "never tint a container with the same
   token as a chip inside it — two semi-transparent layers of one token composite
   to a ground neither was measured against." All three states are alpha tints,
   and they were landing on the top bar's own tone rather than on panel. Measured
   composited, light: warn 4.30 against a 4.5 floor at 12px/700, and bad 4.89 with
   almost nothing in hand. Compositing each tint over --c-panel in the declaration
   itself fixes the ground wherever the chip is dropped: warn 4.54, ok 4.88, bad
   ~5.0. Same idiom as the pinned circuit cells above and as .ckt-unconf.
   The 0.04 of headroom on warn is the CLOSED PALETTE's own number — --c-pipe-ink
   #8A6800 on --c-pipe-bg over white is 4.54 and there is no darker pipe ink in
   DESIGN_SPEC. Inventing one is a design-system change across 58 screens, so it
   is recorded rather than done. */
.ct-chk-bad { background:linear-gradient(0deg, var(--c-bad-bg),  var(--c-bad-bg)),  var(--c-panel); }
.ct-chk-warn{ background:linear-gradient(0deg, var(--c-pipe-bg), var(--c-pipe-bg)), var(--c-panel); }
.ct-chk-ok  { background:linear-gradient(0deg, var(--c-ok-bg),   var(--c-ok-bg)),   var(--c-panel); }

/* ══ v799: THE FILLED-CERTIFICATE CONTRAST FAILURES ═══════════════════════════
   Six of these only exist on a certificate somebody has actually worked on —
   .done needs a section complete and a step behind you — which is why an empty
   fixture kept reporting the module clean. Every number below was measured with
   the background COMPOSITED, on a 12-circuit EICR with all 86 inspection items
   answered, light first. All of them come from css/style.css's older --green and
   --amber, which are not this pass's file; they are corrected from here, matched
   selector for selector, and the ID lifts the specificity over the originals
   rather than reaching for !important.

   --amber is the trap worth naming. In LIGHT it resolves to #8A6800: it is a
   text-on-white token, the same value DESIGN_SPEC calls --ink-money and marks
   TEXT ONLY. style.css uses it as the ink on a near-black disc, which is the
   brown-blob mistake pointing the other way. On charcoal the palette's answer is
   --c-fill-action, and DESIGN_SPEC §1 says so in as many words: a chip on the
   charcoal frame cannot use the light inks. */
/* 3.42 → 5.01. --green #059669 on --panel2 #F4F4F0. "86/86" on a finished
   section, 11 of them on one screen. --c-ok-ink was chosen to be legible. */
#certs-body .cn-badge.done{ color:var(--c-ok-ink); }
/* 3.77 light and 1.92 dark → 5.19 and 9.79. White on --green, on 115 step
   circles. design-c.css already recorded #34d399 as 1.92:1 in its own comment
   further up this file; the number was written down in the tree and live on
   screen. One pairing serves both themes: the solid ok DOT as the disc, with the
   action ink on it, so the "done" step stays a solid disc rather than a tint. */
#certs-body .cert-step-chip.done .cn-step{ background:var(--c-ok-dot); color:var(--c-action-ink); }
/* 3.85 → 14.30. #8A6800 on #08090B — the light theme's text token used as ink on
   a charcoal disc. --c-frame is the palette's charcoal and --c-fill-action is
   what goes on it. */
#certs-body .cert-step-chip.on .cn-step{ background:var(--c-frame); color:var(--c-fill-action); }
/* the fifth, the band chip's count at 2.69 light and 1.61 dark, is fixed at its
   own rule further up this file rather than duplicated here — search
   .ckt-bandbar .ckt-band.on .tab-ct. */
/* The bypass is a real and legitimate action for a one-person firm that IS its
   own QS, so it is not hidden and not disabled — it is simply not dressed as the
   default. Quieter than its neighbour, and its label now says what it skips. */
.ct-bypass, .cpv-bypass{ font-weight:400; opacity:.9; }
.ct-bypass:hover, .cpv-bypass:hover{ opacity:1; }

/* ── THE OUTPUT BAR (#12) ─────────────────────────────────────────────────────
   Six buttons at four different heights (38/34/34/30/34/30) in the module's most
   important action row. One height, and a disabled control now says why on the
   surface rather than in a title — a disabled button never takes focus, so a
   tooltip on one is unreachable by keyboard and silent to a screen reader. */
#certprint-actions{ display:flex; align-items:flex-start; gap:8px; flex-wrap:wrap; }
#certprint-actions .btn{
  min-height:var(--c-tap); display:inline-flex; align-items:center; justify-content:center;
}
.cpv-hold{ display:inline-flex; flex-direction:column; gap:3px; max-width:230px; }
.cpv-why{ font-size:11px; line-height:1.35; color:var(--c-mut); }

/* the visible reason under a "Same as client" pair that is following the client */
.csame-why{ line-height:1.4; }

/* ── THE BANDS HAVE ROOM THE SPREADSHEET NEVER DID — USE IT ───────────────────
   Every circuit input carried a hard-coded pixel width sized for the 31-column
   spreadsheet, where the only thing that mattered was fitting. So "500" rendered
   as "50(" in a 48px Test V box and ">299" as ">2!" in a 50px IR box — on the
   schedule of test results, where the value IS the document.
   In a band there is spare width by construction, so the declared width becomes
   a FLOOR and the input fills its cell. "All 31 columns" keeps the exact original
   widths, because there the table is deliberately wider than the scroller and
   growing the inputs would only add to the scroll. */
.ckt-table input{ width:var(--ckt-w); }
.ckt-table:not(.ckt-band-all) input{ width:100%; min-width:var(--ckt-w); }

/* ── VALUES THAT DID NOT FIT THEIR OWN BOX ────────────────────────────────────
   Measured with scrollWidth against clientWidth on a filled 12-way board: in the
   full 31-column view FIFTEEN of the 31 columns clipped their own content.
   "60898" and "61008" (the device standards), "PVC T&E" (the wiring type), ">299"
   (both insulation readings), "500" (the test voltage), "N/A", "0.4", "100" — all
   truncated mid-character on a schedule of test results whose entire purpose is
   to state those values. It is pre-existing and it is the direct cost of sizing
   31 columns to fit rather than to be read.

   Widened to what each actually needs. The full view grows by roughly 100px and
   therefore scrolls slightly further, which is the right trade now that the
   circuit, its description and its result stay pinned while you scroll: a value
   you cannot read is worse than a scroll you can.

   Applied here rather than in CKT_COLS because those three-tuples are the
   declared column contract that assertCktGrid and assertCktBands check, and this
   is a rendering minimum, not a change to the schedule. */
.ckt-table tbody input{ min-width:38px; }
.ckt-table td[data-f="wiring"] input{ min-width:78px; }
.ckt-table td[data-f="bs"] input,
.ckt-table td[data-f="rcdBS"] input{ min-width:70px; }
.ckt-table td[data-f="irLN"] input,
.ckt-table td[data-f="irLE"] input{ min-width:62px; }
.ckt-table td[data-f="ref"] input,
.ckt-table td[data-f="testV"] input{ min-width:54px; }
.ckt-table td[data-f="afdd"] input,
.ckt-table td[data-f="disc"] input,
.ckt-table td[data-f="idn"] input{ min-width:50px; }
/* a two-digit device rating (32 A, 40 A) did not fit a box sized for one digit */
.ckt-table td[data-f="rating"] input{ min-width:46px; }

/* ══════════════════════════════════════════════════════════════════════════════
   LANE C · COMPLIANCE & FLEET — the Direction C pass, v792
   frontend-design (tool 2 of seven), against audit/page-specs/laneC-tool1-impeccable.md

   ── WHY THERE IS SO MUCH OF IT ──
   This surface had never had the Direction C pass. Every `.cmp-*` rule lives at
   css/style.css 4915-4977 and every `.insp-*` rule at 7840-7893, on the older
   --bg/--bg2/--fg/--muted/--accent tokens plus raw hex outside the closed palette:
   #34d399, #f87171, #ffab00, #e6c93a, #f0a020, #8aa. DESIGN_SPEC §1 closes that
   palette. The page header had Direction C; the body had not, and the seam was
   visible in the screenshots.

   Nothing below edits style.css. Source order does the work (this file loads after
   it) except where a light-theme copy or a shorthand out-specifies a longhand — the
   two places that happens are named at the rule.

   ── THE ONE HONEST WAY OF SHOWING A DATED OBLIGATION ──
   An MOT, a public liability certificate, a staff qualification, a scheme deadline
   and a withdrawn British Standard are the same kind of fact: true until a date and
   false after it. js/compliance.js already models them as one shape. The INTERFACE
   spelled that one idea five ways:

     1. a bordered pill, colour-only, five off-palette hues        (.cmp-pill.cmp-*)
     2. a bare emoji in the row's text                             (🔴 🟠 🟡 ⚪)
     3. a tinted cell border on a van                              (.cmp-veh-cell.cmp-*)
     4. a coloured left edge on the panel                          (.cmp-remind.has-exp)
     5. the pack's must/expected/if-it-applies badges              (.insp-badge.*)

   Five is now ONE — `.cmp-due`, below — with two deliberate exceptions:
     · (4) goes, because DESIGN_SPEC §1 bans the coloured left-border outright and
       it was dead in the light theme anyway (see the border-color note further down).
     · (5) STAYS SEPARATE ON PURPOSE. PAGE-CHECKLIST: "an obligation grade is not a
       status." Must-have / expected / if-it-applies is a property of the REQUIREMENT
       and is constant whether the row is done or not. Painting it in the same
       vocabulary as "expired" would say a mandatory item that is fully evidenced is
       a problem. It gets its own quiet, un-alarming treatment.

   ── SEVERITY RANKS BY RING AND WEIGHT, NEVER BY TINT ALPHA ──
   PAGE-CHECKLIST records the trap: --c-bad-bg is .09 and --c-pipe-bg is .16, so
   tinting by severity renders EXPIRED paler than DUE SOON. So the ranking is carried
   by the ring (solid --c-bad-dot -> 40% pipe -> hairline -> none) and by the weight,
   and the ground is secondary. Checked in a browser, both themes, composited.

   ── AND NEVER COLOUR ALONE ──
   DESIGN_SPEC §7. Every state carries its WORD inside the chip. The emoji stays
   because it is fast to scan and it is this product's voice, but it is now
   aria-hidden and it is no longer the only carrier — a screen reader announced
   "red circle", which is not a compliance status.
   ══════════════════════════════════════════════════════════════════════════════ */

/* ── 1. THE DUE CHIP. One component, five states, closed palette. ───────────── */
.cmp-pill{
  display:inline-flex; align-items:baseline; gap:6px;
  font-size:12px; line-height:1.35; padding:3px 10px;
  border-radius:var(--c-r-pill); border:1px solid var(--c-line);
  background:var(--c-panel); color:var(--c-ink);
  font-variant-numeric:tabular-nums; white-space:nowrap;
}
/* the chip carries a word AND a date AND a distance now, so at 390 it has to be
   allowed to break BETWEEN those parts — never inside one of them */
.cmp-due{ flex-wrap:wrap; white-space:normal; }
.cmp-due > span{ white-space:nowrap; }
/* the state word: display face, so it reads as a label rather than as more prose */
.cmp-due .cmp-state{ font-family:var(--c-font-display); font-weight:700; letter-spacing:.01em; }
.cmp-due .cmp-when{ font-weight:400; }

/* ⚠ css/style.css:4926 is `.cmp-ok{ color:#34d399 }` — UNQUALIFIED. renderCompliance
   emits `class="cmp-veh-cell cmp-${cmpSev(...)}"`, so the van cell whose date is FINE
   inherited green over its whole box and rendered its heading at 1.79:1, while the
   EXPIRED cell rendered in normal ink. Severity was inverted in the fleet grid, by
   exactly the bare-modifier collision DESIGN_SPEC §4 (.btn.s / .lrow .s) was written
   about: .cmp-ok was doing double duty as a pill modifier and a cell modifier.
   One line takes the bare rule's reach away; the pill keeps its colour below. */
.cmp-ok{ color:inherit; }

.cmp-pill.cmp-exp{
  background:var(--c-bad-bg); color:var(--c-bad-ink); border-color:transparent;
  box-shadow:inset 0 0 0 1px var(--c-bad-dot); font-weight:600;
}
.cmp-pill.cmp-soon{
  background:var(--c-pipe-bg); color:var(--c-pipe-ink); border-color:transparent;
  /* solid first, color-mix second: an engine without color-mix drops that line and
     keeps the ring rather than losing the severity signal altogether */
  box-shadow:inset 0 0 0 1px var(--c-pipe-ink);
  box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--c-pipe-ink) 40%, transparent);
  font-weight:600;
}
.cmp-pill.cmp-watch{
  background:var(--c-sys-bg); color:var(--c-ink); border-color:var(--c-line-strong);
  border-style:solid; font-weight:500;
}
/* an undated item is a GAP IN THE RECORD, not a deadline. The dashed ring is the
   existing sheet's own signal for that and it is kept, because it is right. */
.cmp-pill.cmp-nodate{
  background:transparent; color:var(--c-ink); border-color:var(--c-line-strong);
  border-style:dashed; font-weight:500;
}
.cmp-pill.cmp-ok{
  background:var(--c-ok-bg); color:var(--c-ok-ink); border-color:transparent;
  font-weight:500;
}

/* ── 2. THE QUEUE. A ring per row, never a fill — so the chip inside it is never
   composited over a second layer of its own token (PAGE-CHECKLIST's trap, which
   turned .cmp-pill.cmp-soon from 1.79 measured flat into 1.58 as rendered). ──── */
.cmp-remind-list{ gap:6px; }
.cmp-remind-row{
  background:var(--c-panel); border:1px solid var(--c-line); color:var(--c-ink);
  border-radius:var(--c-r-chip); font-size:13.5px;
  display:flex; flex-wrap:wrap; align-items:center; gap:8px 10px;
  padding:9px 12px; min-height:var(--c-tap);
}
.cmp-remind-row.cmp-exp{ border-color:var(--c-bad-dot); }
.cmp-remind-row.cmp-soon{ border-color:var(--c-pipe-ink); border-color:color-mix(in srgb, var(--c-pipe-ink) 45%, var(--c-line)); }
.cmp-remind-row.cmp-watch{ border-color:var(--c-line); }
.cmp-remind-row.cmp-nodate{ border-color:var(--c-line-strong); border-style:dashed; }
/* the subject takes the slack so the chip and the actions never get pushed off the
   right edge — see the 390 note below */
.cmp-remind-row > .cmp-subj{ flex:1 1 190px; min-width:0; }
.cmp-remind-act{ display:flex; flex-wrap:wrap; align-items:center; gap:6px; margin-left:auto; }
.cmp-told, .cmp-hist{ flex-basis:100%; margin:0; }

/* ⚠ 390 AND 400 OVERFLOWED HORIZONTALLY — 44px and 34px, measured, office renderer.
   DESIGN_SPEC §5 requires no horizontal scroll at 390/400/768/1024/1280/1440/1920,
   and tool 1 measured "zero overflow at 390" because at 390 WITH TOUCH the field app
   replaces this renderer with the van check. Without touch — a narrow desktop window,
   or a phone browser in Office view — the office queue renders and v779's own new
   `Renewed until…` / `Show me` buttons sat 44px past the right edge, because
   .cmp-remind-row was a nowrap flex row with justify-content:space-between.
   The wrap above is the fix; this holds the actions to their own line when they
   cannot sit beside the subject. */
@media (max-width:560px){
  .cmp-remind-act{ margin-left:0; flex-basis:100%; }
  .cmp-remind-row > .cmp-subj{ flex-basis:100%; }
}
.cmp-more{ margin-top:2px; align-self:flex-start; }

/* ── 3. THE REMINDERS PANEL EDGE. ──────────────────────────────────────────────
   `.cmp-remind.has-exp{ border-left-color:#f87171 }` (0,2,0) is a LONGHAND and
   `body.theme-light .panel{ border-color:var(--line) }` (0,2,1) is a SHORTHAND that
   expands to it, so in the DEFAULT theme the "something has expired" edge measured
   rgb(216,215,204) — plain --line, no signal at all. CLAUDE.md §7's
   shorthand-beats-longhand trap, live, in the theme Jamie actually looks at.
   DESIGN_SPEC §1 bans the coloured left-border anyway ("the audit found 45 of them
   and they read as AI-generated"), so the edge is not restored — the panel says it
   in words and in the chips, and the count in the header carries the alarm. */
.cmp-remind, .cmp-remind.has-exp, .cmp-remind.is-empty{
  border-left-width:1px; border-left-color:var(--c-line);
}

/* ── 4. THE FLASH REGION — loading, error and offline, which the surface had none
   of (DESIGN_SPEC §6 requires all four). Each one earns its place:
     · LOADING is real here. compAssemble() awaits cmpResolve(), which pulls up to
       200 offloaded files before the pack window opens — press the button and
       nothing happened for seconds.
     · ERROR is real here. cmpUpload rejects anything over ~3.2MB with a blocking
       alert(); a designed message in the page is the same information without the
       modal.
     · OFFLINE is real here. Everything is recorded locally and syncs later, and the
       person recording an insurance renewal in a van needs to know it is safe.
   ─────────────────────────────────────────────────────────────────────────────── */
.cmp-flash{
  display:flex; align-items:flex-start; gap:8px; margin:0 0 10px;
  padding:9px 12px; border-radius:var(--c-r-chip); font-size:13px; line-height:1.45;
  border:1px solid var(--c-line); background:var(--c-panel); color:var(--c-ink);
}
.cmp-flash.is-work{ background:var(--c-in-bg); border-color:transparent;
  box-shadow:inset 0 0 0 1px var(--c-in-dot);
  box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--c-in-dot) 45%, transparent); }
.cmp-flash.is-bad{ background:var(--c-bad-bg); border-color:transparent; box-shadow:inset 0 0 0 1px var(--c-bad-dot); }
.cmp-flash.is-off{ background:var(--c-sys-bg); border-color:var(--c-line-strong); }
.cmp-flash b{ font-family:var(--c-font-display); font-weight:700; }
.cmp-flash-x{
  margin-left:auto; border:0; background:none; color:var(--c-mut); cursor:pointer;
  font:inherit; line-height:1; padding:2px 4px; min-width:var(--c-tap); min-height:var(--c-tap);
}

/* ── 5. A VAN HAS A STATE. ─────────────────────────────────────────────────────
   Four cells with four tints and no answer: looking at YJ71 KLM you could not say
   whether it was legal to drive today without reading four boxes, and with three
   vans that was twelve. The card now answers it once, at the top, and it keeps the
   two questions apart on purpose — insurance, tax and MOT decide whether the van can
   go out; finance does not, and rolling them together would put a van off the road
   for a lease ending. ─────────────────────────────────────────────────────────── */
.cmp-veh-state{
  display:flex; flex-wrap:wrap; align-items:center; gap:8px;
  margin:-2px 0 10px; padding-bottom:9px; border-bottom:1px solid var(--c-line);
  font-size:13px; color:var(--c-ink);
}
.cmp-veh-state .cmp-legal{ font-family:var(--c-font-display); font-weight:700; font-size:14px; }
.cmp-veh-state.is-bad .cmp-legal{ color:var(--c-bad-ink); }
.cmp-veh-state.is-soon .cmp-legal{ color:var(--c-pipe-ink); }
.cmp-veh-state.is-ok .cmp-legal{ color:var(--c-ok-ink); }
.cmp-veh-state.is-unknown .cmp-legal{ color:var(--c-ink); }
.cmp-veh-state .cmp-why{ color:var(--c-mut); }

/* ── 6. THE VAN CELLS, on tokens, with the severity the right way round. ─────── */
.cmp-veh{ background:var(--c-panel); border-color:var(--c-line); border-radius:var(--c-r-card); }
.cmp-veh-cell{
  background:var(--c-panel); border:1px solid var(--c-line); color:var(--c-ink);
  border-radius:var(--c-r-chip);
}
.cmp-veh-cell.cmp-exp{ border-color:var(--c-bad-dot); }
.cmp-veh-cell.cmp-soon{ border-color:var(--c-pipe-ink); border-color:color-mix(in srgb, var(--c-pipe-ink) 45%, var(--c-line)); }
.cmp-veh-cell.cmp-watch{ border-color:var(--c-line); }
.cmp-veh-cell.cmp-nodate{ border-color:var(--c-line-strong); border-style:dashed; }
.cmp-veh-cell.cmp-ok{ border-color:var(--c-line); }
.cmp-cell-h{ color:var(--c-ink); font-family:var(--c-font-display); font-weight:600; font-size:13px; }
.cmp-in{ background:var(--c-panel); border-color:var(--c-line); color:var(--c-ink); border-radius:var(--c-r-input); }
.cmp-datelab{ color:var(--c-mut); }
.cmp-datelab input, .cmp-datelab select{ background:var(--c-panel); border-color:var(--c-line); color:var(--c-ink); border-radius:var(--c-r-input); }
.cmp-count{ background:var(--c-sys-bg); border-color:var(--c-line); color:var(--c-ink); border-radius:var(--c-r-pill); }
.cmp-doc, .cmp-doc-view{ background:var(--c-panel); border-color:var(--c-line); color:var(--c-ink); }
.cmp-doc-x{ color:var(--c-mut); }
.cmp-cert-row{ border-top-color:var(--c-line); }
.cmp-veh-docs, .cmp-note{ border-top-color:var(--c-line); }

/* ── 7. TWENTY-TWO UPLOAD CONTROLS, NONE REACHABLE BY A KEYBOARD. WCAG 2.1.1, A.
   Every 📎 attach / 📎 Upload / 📎 Add a copy / ＋ add / replace was a <label>
   wrapping <input type="file" hidden>. A <label> is not focusable and `hidden`
   takes the input out of the tab order, so on the module whose whole purpose is
   holding documents, a keyboard could not attach one. Measured: 19 labels, 19
   hidden inputs, 265 tab stops inside the view, 0 of them an upload.

   The fix keeps NATIVE semantics rather than bolting role="button" onto a label:
   the input stays a real file input, is clipped instead of hidden so it keeps its
   tab stop and its Enter/Space activation, and the label carries the ring via
   :focus-within. js/teamportfolio.js emits two more of these and is another lane's
   file — those two keep `hidden` and are named in the report, not silently fixed. */
.cmp-up{
  position:relative;
  color:var(--c-ink); border:1px dashed var(--c-line-strong); border-radius:var(--c-r-input);
  background:var(--c-panel); font-size:12.5px;
}
.cmp-up:hover{ border-color:var(--c-in-dot); }
.cmp-file{
  position:absolute; inset:0; width:100%; height:100%;
  opacity:0; cursor:pointer;
}
.cmp-up:focus-within{ outline:2px solid var(--c-in-dot); outline-offset:2px; border-color:var(--c-in-dot); }
/* the clipped input must not be allowed to paint its own ring on top */
.cmp-file:focus, .cmp-file:focus-visible{ outline:none; }

/* ── 8. THE INSPECTION PACK, on the closed palette. ────────────────────────────
   The grade badges are deliberately QUIET: a grade is not a status (see the header).
   "Must have" was #f87171 at 2.77:1 and read as an alarm on a row that may be
   perfectly in order. It is now a plain, legible label with weight doing the ranking. */
.insp-pack{ border-left-width:1px; border-left-color:var(--c-line); }
.insp-setup{ background:var(--c-surface); border-color:var(--c-line); border-radius:var(--c-r-input); }
.insp-bar{ background:var(--c-sys-bg); border-color:var(--c-line); }
.insp-bar-f{ background:var(--c-ok-dot); }
.insp-count{ color:var(--c-mut); }
.insp-left{ color:var(--c-pipe-ink); font-weight:700; }        /* was #f0a020 at 2.15:1 */
.insp-group{ border-color:var(--c-line); border-radius:var(--c-r-input); }
.insp-group-h{ background:var(--c-surface); color:var(--c-ink); }
.insp-group.open .insp-group-h{ border-bottom-color:var(--c-line); }
.insp-chev{ color:var(--c-mut); }
.insp-group-n{ color:var(--c-mut); border-color:var(--c-line); }
.insp-group-n.all{ color:var(--c-ok-ink); border-color:var(--c-ok-dot); border-color:color-mix(in srgb, var(--c-ok-dot) 45%, transparent); }
.insp-row{ border-top-color:var(--c-line); }
.insp-row-t{ color:var(--c-ink); }
.insp-help{ color:var(--c-mut); }
.insp-note{ background:var(--c-pipe-bg); border-color:transparent; color:var(--c-ink);
  box-shadow:inset 0 0 0 1px var(--c-pipe-ink);
  box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--c-pipe-ink) 35%, transparent); }
.insp-noteinput{ background:var(--c-panel); border-color:var(--c-line); color:var(--c-ink); border-radius:var(--c-r-input); }
.insp-badge{ color:var(--c-mut); border-color:var(--c-line); background:var(--c-panel); font-weight:700; }
.insp-badge.insp-must{ color:var(--c-ink); border-color:var(--c-line-strong); }
.insp-badge.insp-expect{ color:var(--c-mut); border-color:var(--c-line); }
.insp-badge.insp-if{ color:var(--c-mut); border-color:var(--c-line); border-style:dashed; }
.insp-badge.insp-law{ color:var(--c-mut); border-color:var(--c-line); border-style:dotted; }
.insp-dot{ border-color:var(--c-line-strong); color:var(--c-ink); }
.insp-dot-ok{ background:var(--c-ok-dot); border-color:var(--c-ok-dot); color:#fff; }
.insp-dot-na{ color:var(--c-mut); border-color:var(--c-line); }
.insp-dot-red{ border-color:var(--c-bad-dot); }
.insp-src{ color:var(--c-in-ink); }

/* ── 9. THE TAP FLOOR, where Jamie's 30 Jul ruling puts it. ────────────────────
   His ruling: irreversible controls go to 44px on touch, everything else keeps 30px.
   Zero irreversible controls on this surface are under the floor and the 44px block
   in style.css is doing its job, so the 114 ordinary controls under 44px on a touch
   laptop are ACCEPTED and are not lifted. Two controls were under even the accepted
   30px on a plain desktop and those are the whole of this rule: "Not for us" at 19px
   removes an obligation from the pack's score, and "source ↗" at 17.8px is the link
   to the rule the row is quoting. Neither is irreversible; both are too small. */
.insp-tick, .insp-src{ min-height:30px; display:inline-flex; align-items:center; }
@media (pointer:coarse){ .insp-tick, .insp-src{ min-height:var(--c-tap-touch); } }

/* ── 10. HEADINGS. H1 then five H3s, with level 2 skipped, is a broken outline for
   anyone navigating by heading. The panels are H2 now; style.css's whole `.panel h3`
   treatment (which is nine !important declarations) is restated here for the new tag,
   scoped to this view so it cannot reach a panel in another lane. ──────────────── */
#view-compliance .panel > h2{
  font-family:var(--c-font-body); font-size:11px; font-weight:700;
  letter-spacing:.09em; text-transform:uppercase; color:var(--c-mut);
  display:flex; align-items:center; gap:8px; flex-wrap:wrap;
  border-bottom:1px solid var(--c-line); margin:-2px 0 12px; padding:0 0 8px;
  background:none; border-radius:0;
}
#view-compliance .panel > h2::before{ display:none; }
#view-compliance .panel > h2 .muted{ display:inline; text-transform:none; letter-spacing:0; font-weight:400; }

/* ── 11. THE FIRST RUN. ────────────────────────────────────────────────────────
   Day one for every new customer was: an empty box whose only primary control built
   a document saying "Nothing recorded for this section" seven times, then seventeen
   questions about two scheme assessments they have not booked, and only then
   "＋ Add vehicle" about 1,100px down. Measured on a blank account: FIVE yellow
   primaries competing on one screen, three of them "make a document out of nothing".
   DESIGN_SPEC §5's empty-state law is "says what this is for and gives the one action
   that fills it". There is one action now, and it fills it. ───────────────────── */
.cmp-first{
  border:1px solid var(--c-line); border-radius:var(--c-r-card);
  padding:16px 18px; background:var(--c-panel);
}
.cmp-first h4{
  font-family:var(--c-font-display); font-weight:600; font-size:16px;
  margin:0 0 6px; color:var(--c-ink); letter-spacing:-.2px;
}
.cmp-first p{ margin:0 0 12px; color:var(--c-mut); font-size:13.5px; line-height:1.5; max-width:62ch; }
.cmp-first .cmp-first-go{ display:flex; flex-wrap:wrap; gap:8px; align-items:center; }

/* ── 12. THE MCS DEFERRAL. ─────────────────────────────────────────────────────
   Not a greyed-out panel and not a "coming soon" badge on a live control — both are
   the stub §8 bans. One honest line where the panel was, in the product's own voice,
   with exactly one control that genuinely records something. All 284 lines of
   INSP.mcs stay in js/inspection.js; they cost nothing while unrendered and deleting
   them is the only way to make this deferral expensive. */
.insp-defer{ background:var(--c-panel); }
.insp-defer p{ color:var(--c-mut); font-size:13.5px; line-height:1.5; margin:0 0 10px; max-width:66ch; }
/* ⚠ css/style.css:173 is a bare `label{ display:flex; flex-direction:column }`, so any
   new <label> in this codebase stacks its control ABOVE its text unless it says
   otherwise — which is why .insp-tick and .fa-lv-half both carry an explicit
   flex-direction:row and a width:auto, with a comment saying so. This one did not, and
   the checkbox rendered above and left of its own words. Caught by looking at the
   screenshot, not by any measurement. */
.insp-defer .insp-defer-tick{
  display:inline-flex; flex-direction:row; align-items:center; gap:8px;
  width:auto; min-height:var(--c-tap);
  font-size:13px; font-weight:500; color:var(--c-ink); cursor:pointer;
  text-transform:none; letter-spacing:0;
}
.insp-defer .insp-defer-tick input{ flex:0 0 auto; margin:0; }
@media (pointer:coarse){ .insp-defer .insp-defer-tick{ min-height:var(--c-tap-touch); } }

/* ── 13. `transition: all` ANIMATES `outline-width`. ───────────────────────────
   css/style.css:1243 puts `transition: all .13s ease` on .btn/.icon-btn, so the
   keyboard focus ring FADES IN over 130ms. It is a real (if small) defect for a fast
   keyboard user and it is also the mechanism that has produced three false
   "no focus ring" reports on this project — the base rate is three false positives
   to zero true ones. Naming the properties on this surface makes the ring instant
   and takes the measurement trap away with it. Nothing else about the transition
   changes. */
#view-compliance .btn, #view-compliance .icon-btn, #view-compliance .cmp-up{
  transition-property:background-color, border-color, color, box-shadow, transform;
}

/* the panel-level "something has expired" tell, in words as well as colour — see the
   note in js/compliance.js. It replaces a 3px left edge that DESIGN_SPEC §1 bans and
   that was measurably dead in the default theme. */
.cmp-expflag{
  font-size:11px; font-weight:700; letter-spacing:.04em; text-transform:none;
  padding:2px 9px; border-radius:var(--c-r-pill);
  background:var(--c-bad-bg); color:var(--c-bad-ink);
  box-shadow:inset 0 0 0 1px var(--c-bad-dot);
}
/* the queue's one-click route to the pack, which now sits below the two registers */
.cmp-packline{ display:flex; flex-wrap:wrap; align-items:center; gap:8px; margin:10px 0 0; }
.cmp-packjump{ flex:0 0 auto; }

/* ══════════════════════════════════════════════════════════════════════════════
   v792 — EMERGENCY LIGHTING PERIODIC INSPECTION AND TEST

   One rule, and it exists because the periodic document has something the
   completion certificate does not: a per-luminaire VERDICT that the engineer
   reads down a list at arm's length in a plant room.

   `.emlp-st` is the state of one luminaire in that list. DESIGN_SPEC §7: status
   is a colour AND a word, never colour alone, so the dot always carries its
   label. Built from the same `--c-*` status tokens as `.ct-chk`, selector for
   selector, so it flips with the theme and cannot drift from the rest of the
   system. The inset hairline on the dot is what keeps a light dot legible on a
   light row — the same trick `.ct-chk .dot` uses.

   ⚠ AND NOTHING HERE TOUCHES THE PRINTED PAGE. READ THIS BEFORE ADDING ANY.
   `certPrintHTML`'s output is fetched into a popped-out window that loads
   `css/style.css` AND NOTHING ELSE — three `fetch('css/style.css')` call sites in
   js/certs.js and four more in js/docs.js. A print rule written in THIS file
   would apply in the on-screen preview, which lives in index.html and does load
   design-c.css, and then vanish from the PDF the customer receives: a silent
   divergence between what the office checks and what actually goes out. The
   periodic's ten-column schedule is therefore sized by inline `width` attributes
   on its own `<th>`s, which travel with the markup, exactly as the rest of the
   print template already does with its `L(t,w)` label cells.
   ══════════════════════════════════════════════════════════════════════════════ */
.emlp-st{ display:inline-flex; align-items:center; gap:5px; white-space:nowrap; font-weight:700; }
.emlp-st .dot{
  width:7px; height:7px; border-radius:50%; flex:none;
  box-shadow: inset 0 0 0 1px rgba(13,15,19,.28);
}
.emlp-st-bad { color:var(--c-bad-ink); }  .emlp-st-bad  .dot{ background:var(--c-bad-dot); }
.emlp-st-warn{ color:var(--c-ink-money); } .emlp-st-warn .dot{ background:var(--c-fill-action); }
.emlp-st-ok  { color:var(--c-ok-ink); }   .emlp-st-ok   .dot{ background:var(--c-ok-dot); }
.emlp-st-none{ color:var(--c-mut); }      .emlp-st-none .dot{ background:var(--c-sys-dot); }
/* the phone list row's subtitle is --muted at 11.5px; the state word inside it has
   to carry its own colour and weight or the tell is lost at arm's length */
.fc-ckt-main span .emlp-st{ font-size:11.5px; }

/* ── 14. TWELVE YELLOW BUTTONS IS NOT A HIERARCHY. ─────────────────────────────
   v779 gave every queue row the action that ends it, which was the right move and is
   the best thing on this page. It gave each one `.btn-amber` — so a queue with twelve
   rows put TWELVE instances of --c-fill-action on screen, all above the fold, all
   competing with the page's actual primary. DESIGN_SPEC §1 calls --c-fill-action
   "THE primary fill", singular.

   The row action is not demoted in importance, only in volume: it is the only control
   in its row, so it does not need a fill to be found. It keeps the display face and
   700 weight, gains a real ring, and reads as the row's own button rather than as
   twelve page-level shouts. The page's one yellow thing stays the page's one yellow
   thing — Prepare assessment pack on a populated account, Add your first van on a
   blank one.

   Specificity, deliberately: the base rule is `body.theme-light .btn-amber:not(#solar-studio *)`
   at (1,3,1). An ID inside :not() counts, so this needs (2,x,y) to win, which is what
   #view-compliance plus the guard gives it. Reasoning from source order alone would
   have been wrong here — see CLAUDE.md §7. */
#view-compliance .cmp-remind-act .btn-amber:not(#solar-studio *),
#view-compliance .cmp-remind-act .btn-amber:not(#solar-studio *):hover{
  background:var(--c-panel);
  border-color:var(--c-line-strong);
  color:var(--c-ink);
  box-shadow:none;
  transform:none;
}
#view-compliance .cmp-remind-act .btn-amber:not(#solar-studio *):hover{
  border-color:var(--c-in-dot);
}

/* ═══ v794 — THE SCAN COST CHOICE (js/certs.js certScanChoiceHTML) ═══
   A billed timeout no longer retries by reflex; it asks. These are the two
   buttons that ask, plus the Stop waiting control that did not exist at all.
   "Go again" SPENDS MONEY, so it is an irreversible control and takes the 44px
   touch floor from Jamie's 30 Jul ruling; "Add the circuits by hand" sits beside
   it at the same size so the free route is never the smaller target. */
.cs-choice-acts:not(#solar-studio *){
  display:flex; flex-wrap:wrap; gap:8px; margin-top:10px;
}
.cs-choice-acts .btn:not(#solar-studio *){
  min-height:34px; flex:1 1 auto;
}
@media (pointer:coarse){
  .cs-choice-acts .btn:not(#solar-studio *){ min-height:44px; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   v795 — BUSINESS COSTS (js/costs.js). Lane D tool 2, frontend-design.

   This page had no rules in this file at all: every one of its 30-odd classes
   was still on style.css's older tokens, and the page carried two inline
   font-sizes plus six inline layout styles, which DESIGN_SPEC bans outright.

   THE ONE DECISION WORTH READING. The signature gradient was on
   `.cost-card.amber .cc-val` (css/style.css). On this page that matched TWO
   figures, and brand law is one hero per surface — and neither of the two was
   the number the page exists to produce. Worse, measured LIGHT first and
   composited onto the real white card ground, the light gradient's leading stop
   (#c99400) is 2.72:1 against a 3:1 floor for 28px/700, so the failing end was
   the pound sign and the first digits.

   So the gradient moves to ONE element — the sticky answer bar's charge-out
   rate, which IS the page's output — and only in DARK, where it measures
   16.17:1 and 14.22:1. In LIGHT it is a flat --c-ink-money (#8A6800), 5.12:1
   on --c-panel. Darkening the gradient stops would have meant inventing two
   hexes outside the closed palette, which section 7 forbids; a token that is
   already in the palette and already passes is the answer.
   --c-ink-money is used as TEXT here and nowhere as a fill.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── the answer, and it follows you down the page ───────────────────────── */
.cost-answer:not(#solar-studio *){
  position:sticky; top:0; z-index:3;
  display:flex; align-items:baseline; flex-wrap:wrap; gap:var(--c-s3) var(--c-s5);
  margin:0 0 var(--c-s4); padding:var(--c-s3) var(--c-s4);
  background:var(--c-panel); border:1px solid var(--c-line);
  border-radius:var(--c-r-card);
}
.cost-answer .ca-main:not(#solar-studio *){
  display:flex; align-items:baseline; gap:var(--c-s2); min-width:0;
}
.cost-answer .ca-lab:not(#solar-studio *){
  font-family:var(--c-font-body); font-size:10px; font-weight:700;
  letter-spacing:.09em; text-transform:uppercase; color:var(--c-mut);
}
.cost-answer .ca-num:not(#solar-studio *){
  font-family:var(--c-font-display); font-weight:700; font-size:26px;
  letter-spacing:-.4px; font-variant-numeric:tabular-nums;
  color:var(--c-ink-money);            /* TEXT colour. 5.12:1 on --c-panel. */
}
.cost-answer .ca-u:not(#solar-studio *){ font-size:.56em; font-weight:500; }
.cost-answer.is-none .ca-num:not(#solar-studio *){
  font-size:19px; color:var(--c-ink);   /* not an answer, so it does not wear the answer's weight */
}
.cost-answer .ca-side:not(#solar-studio *){
  display:flex; flex-wrap:wrap; gap:var(--c-s1) var(--c-s4);
  font-size:12.5px; color:var(--c-mut); margin-right:auto;
}
.cost-answer .ca-side strong:not(#solar-studio *){
  font-family:var(--c-font-display); font-variant-numeric:tabular-nums; color:var(--c-ink);
}
/* DARK gets the signature gradient, once, on this one element. */
body:not(.theme-light) .cost-answer:not(.is-none) .ca-num:not(#solar-studio *){
  background:var(--c-gradient); -webkit-background-clip:text; background-clip:text;
  color:transparent; -webkit-text-fill-color:transparent;
}
/* and it comes OFF the two amber card figures it used to sit on, both themes.
   (1,3,0) beats style.css's `body.theme-light .cost-card.amber .cc-val` at (0,4,1). */
#costs-body .cost-card.amber .cc-val{
  background:none; -webkit-background-clip:border-box; background-clip:border-box;
  color:var(--c-ink-money); -webkit-text-fill-color:currentColor;
}
body:not(.theme-light) #costs-body .cost-card.amber .cc-val{ color:var(--c-fill-action); }

/* The six cards under the answer are SUPPORTING detail now that the strip above carries
   the headline, and style.css's `minmax(190px,1fr)` auto-fit laid them 5 across at 1440
   with the charge-out card orphaned alone on a second row. 300px gives three and three.
   Only the top grid: the door panel below has four cards and reads correctly at four. */
#costs-body .cost-summary-top{ grid-template-columns:repeat(auto-fit,minmax(300px,1fr)); }

/* saved feedback — a live region, so it is announced as well as shown */
.biz-saved:not(#solar-studio *){
  margin-left:auto; align-self:center; font-size:12px; font-weight:700;
  color:var(--c-ok-ink); opacity:0;
}
.biz-saved.is-on:not(#solar-studio *){ animation:bizSaved 2.2s ease forwards; }
@keyframes bizSaved{ 0%{opacity:0} 8%{opacity:1} 70%{opacity:1} 100%{opacity:0} }
body:not(.theme-light) .biz-saved:not(#solar-studio *){ color:var(--c-ok-dot); }

/* ── a card that refuses. A colour AND a word, never colour alone. ───────── */
.cost-card.cc-refused:not(#solar-studio *){
  border-color:var(--c-line-strong); background:var(--c-sys-bg);
}
.cost-card .cc-none:not(#solar-studio *){
  color:var(--c-mut); font-weight:700; font-size:19px; letter-spacing:0;
}
.cost-card.cc-wide:not(#solar-studio *){ grid-column:1/-1; }
.cc-explain:not(#solar-studio *){ font-size:12px; line-height:1.5; }
.cc-badlist:not(#solar-studio *){ margin:var(--c-s2) 0 0; padding-left:1.1em; }
.cc-badlist li:not(#solar-studio *){ margin-top:2px; }
.cc-sanity:not(#solar-studio *){
  margin-top:var(--c-s2); padding:var(--c-s2); border-radius:var(--c-r-chip);
  background:var(--c-pipe-bg); color:var(--c-ink); line-height:1.5;
}
/* Use as my rate reprices every future job: irreversible, so it takes the 44px touch
   floor from Jamie's 30 Jul ruling. Written at #costs-body .btn.cc-apply deliberately:
   the first spelling was `.cc-apply:not(#solar-studio *)` at (1,1,0) and it LOST to this
   file's own `#costs-body .btn.small` at (1,2,0), which is the two-correct-rules-fighting
   shape CLAUDE.md names as the commonest defect in this CSS. Measured: 30px, not 34. */
#costs-body .btn.cc-apply{ margin-top:var(--c-s2); min-height:var(--c-tap); }
@media (pointer:coarse){ #costs-body .btn.cc-apply{ min-height:var(--c-tap-touch); } }

/* ── the two empty states, and the way out of the worked example ────────── */
.cost-blank:not(#solar-studio *), .cost-none:not(#solar-studio *){
  padding:var(--c-s5) var(--c-s4); margin-bottom:var(--c-s3);
}
.cost-blank h3:not(#solar-studio *), .cost-none h3:not(#solar-studio *){ margin:0 0 var(--c-s2); }
.cost-blank p:not(#solar-studio *), .cost-none p:not(#solar-studio *){
  margin:0 0 var(--c-s3); max-width:62ch; line-height:1.55;
}
.cost-blank-acts:not(#solar-studio *){ display:flex; flex-wrap:wrap; gap:var(--c-s2); }
.cost-blank-acts .btn:not(#solar-studio *), .cost-none .btn:not(#solar-studio *){ min-height:var(--c-tap); }
@media (pointer:coarse){
  .cost-blank-acts .btn:not(#solar-studio *), .cost-none .btn:not(#solar-studio *){ min-height:var(--c-tap-touch); }
}
.biz-example:not(#solar-studio *){ margin-bottom:var(--c-s3); }
.biz-example p:not(#solar-studio *){ margin:0 0 var(--c-s3); }
.biz-clearex:not(#solar-studio *){ min-height:var(--c-tap); }
@media (pointer:coarse){ .biz-clearex:not(#solar-studio *){ min-height:var(--c-tap-touch); } }

/* ── the door-cost panel, replacing six inline styles ───────────────────── */
.cost-open:not(#solar-studio *){ margin:var(--c-s4) 0; padding:var(--c-s4); }
.cost-open-h:not(#solar-studio *){ margin-bottom:var(--c-s2); }
.cost-open-ctl:not(#solar-studio *){
  display:flex; gap:var(--c-s3) 18px; flex-wrap:wrap; align-items:center; margin-bottom:var(--c-s3);
}
/* style.css stacks a bare <label> into a column, which put every one of these three
   captions on its own line above a 34px input. They read as a sentence, so they sit on
   one: `flex-direction:row` is the whole fix and it has to be said out loud, because
   `display:flex` alone inherits the column. */
.cost-open-ctl label:not(#solar-studio *){
  display:flex; flex-direction:row; align-items:center; gap:var(--c-s2); min-height:var(--c-tap);
}
/* the margin input sits INSIDE the card's label, mid-sentence: "Charge-out rate for
   [20] % net margin". It must not become a block. */
#costs-body .cost-card .cc-label input.mini{ display:inline-block; vertical-align:middle; }
.staff-note:not(#solar-studio *){ grid-column:1/-1; margin:0; }

/* ── the register: a real header row, a collapsible category, a hit area ── */
.ovh-table thead th:not(#solar-studio *){
  text-align:left; font-size:10px; font-weight:700; letter-spacing:.09em;
  text-transform:uppercase; color:var(--c-mut);
  padding:4px 6px; border-bottom:1px solid var(--c-line);
}
.ovh-table thead th:nth-child(4):not(#solar-studio *){ text-align:right; }
.ovh-table td.ovh-yr:not(#solar-studio *){ text-align:right; white-space:nowrap; }
.ovh-table tr.ovh-bad td:not(#solar-studio *){ background:var(--c-bad-bg); }
details.ovh-cat > summary.ovh-cat-head:not(#solar-studio *){
  display:flex; align-items:center; justify-content:space-between; gap:var(--c-s3);
  cursor:pointer; min-height:var(--c-tap); list-style:none;
}
details.ovh-cat > summary::-webkit-details-marker{ display:none; }
details.ovh-cat > summary .ovh-cat-name:not(#solar-studio *){ display:flex; align-items:center; gap:var(--c-s2); }
details.ovh-cat > summary .ovh-cat-name:not(#solar-studio *)::before{
  content:'▾'; font-size:11px; color:var(--c-mut); transition:transform .12s ease;
}
details.ovh-cat:not([open]) > summary .ovh-cat-name:not(#solar-studio *)::before{ transform:rotate(-90deg); }
details.ovh-cat > summary .ovh-cat-sum:not(#solar-studio *){
  font-variant-numeric:tabular-nums; color:var(--c-ink-money);
}
body:not(.theme-light) details.ovh-cat > summary .ovh-cat-sum:not(#solar-studio *){ color:var(--c-fill-action); }
details.ovh-cat > summary .icon-btn:not(#solar-studio *){ margin-left:var(--c-s2); }
.cost-open-none:not(#solar-studio *){ margin:0; }
.cost-prose:not(#solar-studio *){ margin:-14px 0 20px; line-height:1.6; }
/* The last inline font-size and the last off-palette hex in js/costs.js, both on the
   example warning that bizProfitPanel paints onto the QUOTE page. It read
   `style="color:#f0a020;font-size:12px"`: #f0a020 is in no token list, and DESIGN_SPEC
   bans an inline font-size outright. --c-pipe-ink is the palette's warning TEXT colour
   and is never used here as a fill. */
.biz-exwarn:not(#solar-studio *){ color:var(--c-pipe-ink); font-size:12px; }
body:not(.theme-light) .biz-exwarn:not(#solar-studio *){ color:var(--c-fill-action); }
.ovh-adds:not(#solar-studio *){ display:flex; gap:var(--c-s2); margin-top:10px; flex-wrap:wrap; }
.ovh-adds .btn:not(#solar-studio *){ min-height:var(--c-tap); }
@media (pointer:coarse){ .ovh-adds .btn:not(#solar-studio *){ min-height:var(--c-tap-touch); } }

/* ── the floor: every control on this page clears 34px, 44px on touch ───── */
#costs-body .mini{ min-height:var(--c-tap); }
#costs-body .ovh-table select{ min-height:var(--c-tap); }
#costs-body .btn.small{ min-height:30px; }   /* Tier-2, Jamie 30 Jul: not irreversible */
/* the Employment Allowance tick is worth up to GBP 10,500 and its clickable label
   was a 1158x19 strip. The box is what people aim at; the label is what they hit. */
.biz-ea:not(#solar-studio *){
  display:inline-flex; align-items:center; gap:10px;
  min-height:var(--c-tap); padding:var(--c-s1) 0;
}
.biz-ea input[type=checkbox]:not(#solar-studio *){ width:18px; height:18px; }
.biz-ea-note:not(#solar-studio *){ margin:var(--c-s1) 0 var(--c-s3); }
@media (pointer:coarse){
  .biz-ea:not(#solar-studio *){ min-height:var(--c-tap-touch); }
  .biz-ea input[type=checkbox]:not(#solar-studio *){ width:24px; height:24px; }
  #costs-body .btn.small{ min-height:var(--c-tap); }
}
@media (prefers-reduced-motion: reduce){
  .biz-saved.is-on:not(#solar-studio *){ animation:none; opacity:1; }
  details.ovh-cat > summary .ovh-cat-name:not(#solar-studio *)::before{ transition:none; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   THE WITHHOLDING GRAIN — THREE STATES, AND TWO OF THEM HAD NO FACE AT ALL
   Impeccable, 3 Aug 2026. The ruling and every measurement behind it are in
   audit/page-specs/laneD-impeccable-withholding.md. The short version:

   A figure derived from Business Costs can be in one of THREE conditions, and
   until now the app could only draw two of them.

     STATED    the figure, and nothing else needed
     CAVEATED  the figure IS sound and is shown in full — but something else on
               the page of record is broken and is holding a DIFFERENT figure
               back, so the page says so once, underneath
     REFUSED   this figure's own axis cannot be read, so there is no figure

   The missing state is CAVEATED, and its absence is why five surfaces refuse
   figures that are arithmetically correct and knowable: with nowhere to put
   "sound, but the page it came from is partly broken", the code puts it in
   REFUSED. That is CLAUDE.md section 4a's class running backwards — a two-state
   control asked to express three things puts the third in whichever of the two
   looks safest. There it was the good answer; here it is the honest-looking one.

   HOW A READER TELLS THEM APART, and it is deliberately not colour first:
     - REFUSED  removes the number. Words sit in the column where every other
                row has a figure. In a right-aligned tabular money column that
                is unmissable, and it survives greyscale, dark theme and a
                screen reader.
     - CAVEATED adds a line. The number is untouched — same ink, same weight,
                same size — because a caveated figure is exactly as correct as
                any other, and dimming it would teach the office to distrust a
                number that is right. What is qualified is the PAGE, not the
                figure, so the qualification is a separate object below it.
     One state takes a thing away, the other adds a thing. They cannot be
     confused at a glance, and neither depends on hue (section 7: a status is a
     colour AND a word — these are a shape and a word, with colour spare).

   NOTHING EMITS THESE TWO CLASSES YET, and that is deliberate rather than
   dead CSS: the emitters are a js/ change this pass was not permitted to make
   (setOverheadFeedHTML in js/app.js, jobActualPL in js/extras.js, the P&L pane
   in js/customers.js, bizProfitPanel in js/costs.js). The face is settled here
   so the pass that wires it up composes it instead of inventing a second one.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── CAVEATED. A hairline, a bold lead-in, and plain words. No fill and no hue,
      so it reads as a footnote about the page rather than a warning about the
      number above it. Depth is a hairline (DESIGN_SPEC section 3). */
.fig-caveat:not(#solar-studio *){
  margin:var(--c-s2) 0 0; padding-top:var(--c-s2);
  border-top:1px solid var(--c-line);
  font-size:12px; line-height:1.5; color:var(--c-mut);
}
/* the lead-in is the scannable half — "Still to sort:" in body ink, so the eye
   catches the label without the sentence having to be read to the end. That is
   the exact failure measured on Settings > Pricing, where the statement and the
   refusal were one 12.5px --c-mut paragraph apiece and differed only in prose. */
.fig-caveat strong:not(#solar-studio *){ color:var(--c-ink); font-weight:700; }

/* ── REFUSED, at line level. The card-level face already exists (.cc-refused +
      .cc-none); this is its sibling for a row in a money table, and it says the
      same two words so the two surfaces cannot describe one state differently. */
.fig-none:not(#solar-studio *){
  font-family:var(--c-font-body);          /* NOT the display face: this is not a
                                              number, and tabular-nums on letters
                                              only makes them sit oddly */
  font-weight:700; color:var(--c-mut);
  font-variant-numeric:normal; letter-spacing:0;
}
/* the reason, on its own line under the row it belongs to. Same register as
   .fig-caveat but with no rule above it, because the row above IS its subject. */
.fig-why:not(#solar-studio *){
  margin:2px 0 var(--c-s2); font-size:12px; line-height:1.5; color:var(--c-mut);
}

/* ── AND ONE MEASURED CONTRAST FAILURE ON THE WAY PAST, light theme only.
      style.css:579 is `.cc-sub .ok{color:var(--green)}`, and --green resolves to
      rgb(5,150,105) = #059669, which is --c-ok-DOT: a fill token being used as an
      ink. Composited over --c-panel it measures 3.77:1 at 11px/400, against the
      4.5:1 floor — on the one sentence that tells an electrician their rate is
      fine. It is the --ink-money-as-a-fill mistake running the other way, and
      dark was never affected (10.36:1).
      --c-ok-ink (#04785C) is the token for this job and measures 5.46:1.
      The other two were passing but off-palette (#DC2626, and an --amber that
      happens to equal #8A6800), so all three come onto the closed palette
      together rather than one honest exception at a time (section 7).
      BOTH rules set `color`, so they genuinely compete: this one wins on
      specificity (1,2,0) over (0,2,0) AND on source order. Either alone would
      do it — the ID inside :not() is not doing the work here, and saying so
      is section 7's own three-times-corrected lesson. */
.cc-sub .ok:not(#solar-studio *){ color:var(--c-ok-ink); }
.cc-sub .warn:not(#solar-studio *){ color:var(--c-pipe-ink); }
.cc-sub .bad:not(#solar-studio *){ color:var(--c-bad-ink); }

/* ══ LANE A7 · THE QUALIFIED SUPERVISOR QUEUE — v807 ═════════════════════════
   ⚠ THIS BLOCK IS INSIDE check-compliance-design.js's "Lane C" slice, whether it
   means to be or not: that gate takes `css.slice(css.lastIndexOf('/*', laneCText))`
   and never stops, so EVERYTHING appended to the end of this file is measured by
   it — no raw hex outside the closed palette, no --c-ink-money / --c-pipe-ink as
   a FILL, a solid fallback before any color-mix, no single-letter modifier class.
   Checked before appending rather than discovered by a red build. Nothing below
   needs an exception: it is all closed-palette tokens on text.               */

/* The tab row now carries TWO axes — type (what am I looking at) and status
   (where is it up to) — and without a seam they read as one long undifferentiated
   run of chips where "EML" and "Drafts" look like the same kind of answer. One
   hairline, on the existing --c-line, is the whole treatment: it is the cheapest
   thing that makes the grouping visible and it adds no colour to the page.
   align-self:center because .tabs is `align-items:flex-end` for the tab lip, and
   a stretched rule would hang below the baseline of the shorter chips. */
.cert-tabsep:not(#solar-studio *){
  flex:0 0 1px; width:1px; min-height:22px; align-self:center;
  margin:0 var(--c-s2); background:var(--c-line);
}

/* v824 — the seam above is now a ROW BREAK instead of a hairline (see the
   comment in js/certs.js at the split). .cert-tabs-types keeps the standard
   .tabs spacing below it; .cert-tabs-states gets a small top margin so the
   two questions ("what kind" / "where is it up to") read as two groups
   without a border, which would compete with the .tabs-row's own baseline
   rule elsewhere on the page. "Work state" is a plain small-caps label, same
   treatment already used for .cert-tabs-label-style captions on this page —
   it says what the row below it is answering, nothing else. */
.cert-tabs-types:not(#solar-studio *){ margin-bottom:2px; }
.cert-tabs-states:not(#solar-studio *){ margin-top:8px; margin-bottom:14px; }
.cert-tabs-label:not(#solar-studio *){
  display:flex; align-items:center; align-self:center;
  font-size:11px; font-weight:700; letter-spacing:.04em; text-transform:uppercase;
  color:var(--c-muted); padding:0 var(--c-s2) 0 4px; flex:0 0 auto;
}

/* ── THE AUDIT LINE (J2) ──────────────────────────────────────────────────────
   Who sent it up, who approved it, and — Jamie's ruling, 5 Aug — plainly when
   they are the same person. Self-approval is ALLOWED here (a sole trader is
   legitimately their own QS); this is a record, not a warning, so the self case
   is stated in body ink and never in the bad ink. The only line that gets the
   serious ink is the one where nobody reviewed it at all.
   No modifier class on .cert-banner, so it takes the neutral ground — a tint
   would rank it against the pipe and bad banners it sits beneath, and it is not
   competing with them, it is summarising them. */
.qs-audit .qsa-list:not(#solar-studio *){
  margin:var(--c-s2) 0 0; padding-left:18px;
  font-size:12.5px; line-height:1.6; color:var(--c-ink);
}
.qs-audit .qsa-verdict:not(#solar-studio *){
  margin:var(--c-s2) 0 0; font-size:12.5px; line-height:1.5; color:var(--c-mut);
}
/* the same person did both — allowed, and said as a fact in the body voice */
.qs-audit .qsa-verdict.qsa-self:not(#solar-studio *){ color:var(--c-ink); font-weight:600; }
/* nobody reviewed it at all. This is the one that is worth an eyebrow, and it is
   deliberately NOT worded or coloured as self-approval — see certQSAudit. */
.qs-audit .qsa-verdict.qsa-none:not(#solar-studio *){ color:var(--c-bad-ink); font-weight:600; }
/* we cannot say, because nobody is set up on this device. An honest gap reads in
   the in-progress ink, not the failure ink — it is a thing to go and fix. */
.qs-audit .qsa-verdict.qsa-unsure:not(#solar-studio *){ color:var(--c-pipe-ink); }


/* ══ SEVENS · CERTIFICATE DUPLICATE · TOOL 2 — frontend-design, v814 ══════════
   ⚠ APPENDED, SO check-compliance-design.js's "Lane C" slice OWNS THIS BLOCK —
   it slices from the last comment opener before the Lane C marker to the END of
   the file, so everything below that marker is measured by it. Checked before
   appending, not discovered from a red build. Nothing here needs an exception:
   every value is a closed-palette token or a spacing/size token, there is no
   color-mix, no raw hex outside these comments, no --c-ink-money or --c-pipe-ink
   used as a fill, and no single-letter modifier class. Every new class name was
   grepped against css/style.css (8,000+ lines) first: cert-fresh, cf-hole,
   cb-fresh, cb-spacer, cert-fromtag and cert-dup-btn all returned zero.

   ══ 1 · THE DIALOG THAT NOW WRITES THE CERTIFICATE COULD NOT BE SEEN ══════════
   certDuplicate's fork moved off the native confirm() onto soChoose, which is
   the right control and was already in the product. Driving it with a REAL Tab
   walk, each stop left 260ms to settle, in both themes, at 1440 and at 390 with
   touch, found three measured failures — all of them in the LIGHT theme, which
   is the default (CLAUDE.md §3) and the one Jamie looks at:

     · the focus ring          1.54:1  (2px #3FE3FF on the composited #F6F6F6
                                        choice ground). Dark measured 11.75:1,
                                        so this was light-only and invisible to
                                        any pass that started in dark.
     · the primary choice edge 1.34:1  (1px --yel on white)
     · the other choice edge   1.42:1  (1px rgba(127,127,127,.30) on white)

   WCAG 1.4.11 / 2.4.11 want 3:1 for a control boundary and its focus indicator.
   So in the light theme the two cards a person picks between had no perceptible
   edge, and the ring saying which one the keyboard was on was fainter still.

   THE RING'S TOKEN WAS NOT A JUDGEMENT CALL — IT WAS ALREADY ON THE PAGE. The
   .btn sitting inside the same dialog focuses at 3.31:1 and 3.78:1 with
   rgb(13,143,174), which is --c-in-dot. Using the same token makes this ring
   identical to every other focus ring in the app and it carries its own dark
   value (#3FE3FF), so one declaration is right in both themes.

   THE EDGES RANK BY WEIGHT, NOT BY HUE. --c-ink on the recommended option,
   --c-mut on the other: 16.9:1 and 5.7:1 light, 16.7:1 and 6.8:1 dark. The
   yellow inset bar stays on the primary as the brand accent it was meant to be,
   but it is no longer the ONLY thing marking the recommended branch — it cannot
   be, at 1.34:1. --c-line-strong was rejected deliberately: its own comment in
   this file says it is 2.09:1 and "deliberately NOT a WCAG 3:1 line" for
   STRUCTURAL rules, and the boundary of a control a person clicks is not one.

   SCOPE, STATED HONESTLY. #so-prompt is the only host soChoose renders into, so
   this reaches every soChoose in the product, not only the certificate one.
   That is a contrast REPAIR on a measured WCAG failure, not a restyle: nothing
   about the layout, size, type, spacing or colour identity of the control moves,
   and the two other certificate dialogues plus the quote and journey ones get
   the same fix rather than being left below the floor. */
#so-prompt .so-choice:not(.primary):not(#solar-studio *){ border-color:var(--c-mut); }
#so-prompt .so-choice.primary:not(#solar-studio *){ border-color:var(--c-ink); }
#so-prompt .so-choice:hover:not(#solar-studio *){ border-color:var(--c-ink); }
#so-prompt .so-choice:focus-visible:not(#solar-studio *){
  outline:2px solid var(--c-in-dot); outline-offset:2px;
}

/* ══ 2 · DUPLICATE IS IRREVERSIBLE, AND IT MEASURED 40px UNDER A THUMB ════════
   Jamie's closed ruling (CLAUDE.md §7): irreversible controls go to 44px on
   touch, everything else keeps 30px. Duplicate writes a statutory document that
   cannot be taken back from the screen that made it, so it is irreversible — and
   the editor's topbar button measured 40.0x91.5 at 390 with a coarse pointer,
   which is the surface an owner quotes and copies from in the van. The ⧉ in the
   office list already measured 34px desktop / 44px touch off the app-wide
   .icon-btn floor, so this rule only moves the one that was short; naming both
   keeps them from drifting apart if that floor is ever rescoped. Nothing else in
   .ct-acts is touched — Preview, Ask and All certs are all recoverable. */
@media (pointer:coarse){
  .cert-dup-btn:not(#solar-studio *){ min-height:var(--c-tap-touch); }
}

/* ══ 3 · A FRESHENED CERTIFICATE DID NOT LOOK FRESHENED ══════════════════════
   229 of 311 inputs came back empty with zero marking, two input border colours
   and both of them the theme default, and no reference to the source anywhere on
   the screen. The record READ finished because everything expensive to type
   survives the copy on purpose; the emptiness is spread invisibly across two
   hundred boxes.

   The banner joins the .cert-banner family the QS states already use and takes
   the IN-PROGRESS ground, --c-in-*. That is the whole ranking argument: a fresh
   draft is not a warning (--c-pipe, which is the QS queue) and not a failure
   (--c-bad, which is a sent-back report or an override). It is work in hand, and
   it sits above both without competing with either. */
.cert-banner.cb-fresh:not(#solar-studio *){
  display:flex; align-items:center; flex-wrap:wrap; gap:var(--c-s3);
  background:var(--c-in-bg);
}
.cert-banner.cb-fresh .cb-tag:not(#solar-studio *){ color:var(--c-in-ink); background:var(--c-panel); }
.cert-banner.cb-fresh .cb-tag .dot:not(#solar-studio *){ background:var(--c-in-dot); }
.cert-banner .cb-spacer:not(#solar-studio *){ flex:1 1 0; min-width:0; }

/* The hole itself. A 3px inset edge and NOTHING else — the first draft tinted
   the whole box with --c-in-bg and 229 tinted boxes read as a carpet of errors,
   which is the opposite of the message. One accessory: a quiet column of marks
   down the form saying "these are yours to fill in". --c-in-dot measures 3.78:1
   on the input's white ground in light and 11.7:1 on --c-panel in dark, so the
   mark clears the 3:1 non-text floor in both.

   It rides a class the renderer toggles on the live `input` event rather than an
   [value=""] attribute selector, because cSet does not re-render the editor — an
   attribute mark would still say "empty" after somebody had typed in the box,
   and a mark that is wrong is worse than no mark at all. */
#certs-body.cert-fresh .cf-hole:not(#solar-studio *){
  box-shadow: inset 3px 0 0 var(--c-in-dot);
}

/* Two drafts of one certificate both read "EICR draft". The source is recorded
   at copy time, so the row says which one it came from — quiet, because it is
   provenance, not status. --c-mut is 5.71:1 light / 6.82:1 dark. */
.cert-fromtag:not(#solar-studio *){
  display:inline-block; margin-left:var(--c-s2);
  font-size:11px; line-height:1.4; color:var(--c-mut); white-space:nowrap;
}

/* The phone's one-line twin of the fresh-draft banner. Same --c-in-* ranking as
   the desktop, so a re-test reads the same on both surfaces. .fc-flag's own base
   in css/style.css sets padding, radius and type; only the ground, the edge and
   the ink are set here, and all three come from the closed palette. */
.fc-flag-fresh:not(#solar-studio *){
  color:var(--c-ink); background:var(--c-in-bg); border:1px solid var(--c-in-dot);
}

/* ══ v818 · THE SIGNATURE BOX ═════════════════════════════════════════════════
   Jamie, 7 Aug: "We need a signature box to be added - one where you type your
   name and it creates a signature".

   ⚠ PLACED HERE, ABOVE THE HQ MARKER, ON PURPOSE — and the marker strings are
   deliberately NOT quoted anywhere in this comment. Two gates slice this file
   from a marker to EOF (the compliance one and the HQ one), and each finds its
   marker with a plain indexOf. Writing either marker's text into a comment ABOVE
   the real one makes that comment the start of the slice: on the first attempt
   this block quoted both and the HQ gate promptly failed its two scoping
   assertions against rules that have nothing to do with HQ. Derive the markers
   from the gates rather than copying them here. This block therefore sits inside
   the compliance slice and outside the HQ one, and obeys the compliance slice's
   rules: closed-palette tokens only, no raw hex, no color-mix.

   ⚠ AND THE SIGNATURE ITSELF IS NOT STYLED FROM HERE. The printed certificate
   inlines css/style.css and nothing else, so the mark carries its own font, fill
   and stroke as SVG presentation attributes (certSigSVG, js/certs.js). What is
   below is only the EDITOR's box — the input, the pad it draws on and the
   caption. If every rule here vanished, the signature would still print. */
.csig:not(#solar-studio *){
  display:flex; flex-direction:column; gap:var(--c-s2); margin:var(--c-s2) 0 var(--c-s3);
}
.csig-wide:not(#solar-studio *){ grid-column:1/-1; }
.csig-lab:not(#solar-studio *){
  font-size:12px; font-weight:600; color:var(--c-mut); letter-spacing:.01em;
}
.csig-in:not(#solar-studio *){
  min-height:var(--c-tap); border-radius:var(--c-r-input);
  border:1px solid var(--c-line); background:var(--c-surface); color:var(--c-ink);
  padding:0 var(--c-s3); font-family:var(--c-font-body); font-size:15px;
}
/* The pad. A signature sits ON a line — that is what makes the empty state read
   as "nothing has been signed" rather than as a missing control. The line is
   always drawn; the ink is only there when somebody has typed a name. */
.csig-pad:not(#solar-studio *){
  position:relative; display:flex; flex-direction:column; justify-content:flex-end;
  min-height:64px; padding:var(--c-s3) var(--c-s3) var(--c-s2);
  border:1px solid var(--c-line); border-radius:var(--c-r-card); background:var(--c-panel);
}
.csig-ink:not(#solar-studio *){
  display:block; min-height:40px; color:var(--c-ink); line-height:1;
}
.csig-rule:not(#solar-studio *){
  display:block; height:1px; background:var(--c-line-strong); margin:var(--c-s2) 0 var(--c-s1);
}
.csig-cap:not(#solar-studio *){ font-size:11px; color:var(--c-mut); }
.csig-note:not(#solar-studio *){ margin:0; font-size:12px; line-height:1.5; color:var(--c-mut); }
/* The read-only face an engineer sees on the QS block. Same pad, no input — and
   the words carry the state, never the presence or absence of the mark alone. */
.csig-ro:not(#solar-studio *){
  display:flex; flex-wrap:wrap; align-items:center; gap:var(--c-s3);
  min-height:56px; padding:var(--c-s3);
  border:1px solid var(--c-line); border-radius:var(--c-r-card); background:var(--c-panel);
}
.csig-ro-name:not(#solar-studio *){ font-size:14px; font-weight:600; color:var(--c-ink); }
.fc-sig:not(#solar-studio *){ display:block; }
.fc-sig .csig-in:not(#solar-studio *){ min-height:var(--c-tap-touch); width:100%; }

/* ══ v818 · PICK ONE, CREATE ONE, OR SKIP — the certificate's customer bar ════ */
.certcust:not(#solar-studio *){
  display:flex; flex-direction:column; gap:var(--c-s2);
  margin:0 0 var(--c-s3); padding:var(--c-s3);
  border:1px solid var(--c-line); border-radius:var(--c-r-card); background:var(--c-panel);
}
.certcust-head:not(#solar-studio *){ margin:0; font-size:14px; font-weight:600; color:var(--c-ink); }
.certcust-note:not(#solar-studio *){ margin:0; font-size:12.5px; line-height:1.5; color:var(--c-mut); overflow-wrap:anywhere; }
.certcust-say:not(#solar-studio *){ margin:0; font-size:12.5px; line-height:1.5; color:var(--c-ok-ink); }
.certcust-acts:not(#solar-studio *){ display:flex; flex-wrap:wrap; gap:var(--c-s2); }
.certcust-acts .btn:not(#solar-studio *){ min-height:var(--c-tap); }
/* v821 — DESIGN_SPEC §3: 34px minimum on desktop, 44px on the phone, "no exceptions
   on the phone — it gets used with cold and gloved hands". These three measured 38px
   at 390px. Every variant of this bar carries a button that CREATES a customer record
   ("Save these details as a customer", "No, different person"), which is the
   irreversible class Jamie's tap-floor ruling lifts to 44 on touch, and the rest of
   the bar is its neighbours. Measured after the change: 44px on all three, at 390px,
   in both themes. The rule is restored rather
   than the number overwritten — 34 stays for a mouse — which is exactly what c24a082
   did to the circuit sheet's section chips. */
@media (pointer:coarse), (max-width:820px){
  .certcust-acts .btn:not(#solar-studio *){ min-height:var(--c-tap-touch); }
}
.certcust-q:not(#solar-studio *){
  min-height:var(--c-tap); border-radius:var(--c-r-input);
  border:1px solid var(--c-line); background:var(--c-surface); color:var(--c-ink); padding:0 var(--c-s3);
}
.certcust-hits:not(#solar-studio *){
  display:flex; flex-direction:column; gap:var(--c-s1); max-height:280px; overflow-y:auto;
}
/* One row per customer, and FOUR FIELDS ONLY — name, phone, email, address. No
   figure of any kind reaches this markup; see certCustHitsHTML for why that is a
   construction guarantee rather than something hidden with CSS.

   ⚠ v819 — flex:0 0 auto IS THE FIX FOR THE SQUASHED CHOOSER, AND IT IS THE
   flex-shrink HALF THAT MATTERS. This row is a flex ITEM of .certcust-hits, which
   is flex-direction:column with max-height:280px just above. The row declared its
   own display:flex and never declared flex-shrink, so it kept the initial 1 — and
   the moment the rows' natural heights exceeded the 280px cap, the container shrank
   EVERY row below its own content instead of scrolling. Measured at 390 with six
   realistic records: rows of natural height 69px and 106px were handed 42px boxes,
   the second line spilled 23-60px out of its own row and overlapped the row below,
   and the host sat pinned at exactly 280.00 with scrollHeight 424 — so overflow-y:auto
   never got the chance to do its job because the items yielded first. Not viewport
   dependent: it starts at the 4th record at 390 and the 6th at 1440, so a media
   query would have left it live on the desk. Do not "tidy" this to flex-shrink
   alone without re-measuring; 0 0 auto is the declaration that was driven.

   overflow-wrap:anywhere on the two lines is a SECOND, INDEPENDENT defect that
   survives the flex fix. .certcust-hit-d computes overflow-wrap:normal, so one
   unbreakable token — an email like
   maintenance.requests@northbankpropertymanagement.co.uk — measured
   scrollWidth − clientWidth = 76px and ran off the right edge of the row. */
.certcust-hit:not(#solar-studio *){
  display:flex; flex-direction:column; gap:2px; text-align:left; width:100%;
  flex:0 0 auto;
  min-height:var(--c-tap-touch); padding:var(--c-s2) var(--c-s3);
  border:1px solid var(--c-line); border-radius:var(--c-r-chip);
  background:var(--c-surface); color:var(--c-ink); cursor:pointer; font:inherit;
}
.certcust-hit:hover:not(#solar-studio *){ background:var(--c-frame-hover); }
.certcust-hit-n:not(#solar-studio *){ font-weight:600; font-size:14px; overflow-wrap:anywhere; }
.certcust-hit-d:not(#solar-studio *){ font-size:12px; color:var(--c-mut); overflow-wrap:anywhere; }
.certcust-empty:not(#solar-studio *){ margin:0; }

/* ══ v818 · TO QS — the section certificates land in for sign-off ════════════ */
.cert-toqs:not(#solar-studio *){ margin-bottom:var(--c-s4); }
.cert-toqs h3:not(#solar-studio *){ display:flex; align-items:center; gap:var(--c-s2); }
.toqs-n:not(#solar-studio *){
  display:inline-flex; align-items:center; justify-content:center; min-width:22px;
  padding:0 6px; height:22px; border-radius:var(--c-r-pill);
  background:var(--c-pipe-bg); color:var(--c-pipe-ink); font-size:12px; font-weight:700;
}
.toqs-rows:not(#solar-studio *){ display:flex; flex-direction:column; gap:var(--c-s2); margin:var(--c-s3) 0; }
.toqs-row:not(#solar-studio *){
  display:flex; align-items:center; gap:var(--c-s3); width:100%; text-align:left;
  min-height:var(--c-tap); padding:var(--c-s2) var(--c-s3);
  border:1px solid var(--c-line); border-radius:var(--c-r-chip);
  background:var(--c-surface); color:var(--c-ink); cursor:pointer; font:inherit;
}
.toqs-row:hover:not(#solar-studio *){ background:var(--c-frame-hover); }
/* WHOSE IT IS, as a word — never a colour on its own, and never an icon on its
   own. "I tested this myself" and "Josh tested this" are different reviews. */
.toqs-who:not(#solar-studio *){
  flex:0 0 auto; max-width:34%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  padding:2px 8px; border-radius:var(--c-r-pill);
  background:var(--c-sys-bg); color:var(--c-sys-ink); font-size:12px; font-weight:600;
}
.toqs-mine:not(#solar-studio *){ background:var(--c-in-bg); color:var(--c-in-ink); }
.toqs-main:not(#solar-studio *){ flex:1 1 auto; display:flex; flex-direction:column; min-width:0; }
.toqs-sub:not(#solar-studio *){ font-size:12px; color:var(--c-mut); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.toqs-go:not(#solar-studio *){ flex:0 0 auto; font-size:13px; font-weight:600; color:var(--c-action-ink); }
.fc-qsrow:not(#solar-studio *){ display:flex; flex-direction:column; gap:2px; margin-bottom:var(--c-s2); }
.fc-qswho:not(#solar-studio *){
  align-self:flex-start; padding:1px 8px; border-radius:var(--c-r-pill);
  background:var(--c-sys-bg); color:var(--c-sys-ink); font-size:11.5px; font-weight:600;
}
.fc-qsmine:not(#solar-studio *){ background:var(--c-in-bg); color:var(--c-in-ink); }
/* the QS review sheet on the phone: every finding in full, then the signature */
.fc-qsrev:not(#solar-studio *){ display:flex; flex-direction:column; gap:var(--c-s3); }
.fc-qsacts:not(#solar-studio *){ display:flex; flex-direction:column; gap:var(--c-s2); }
/* Approve is irreversible and reaches a statutory number, so it takes the touch
   floor from Jamie's 30 Jul ruling. Send-it-back and Not yet are ordinary. */
.fc-qs-go:not(#solar-studio *){ min-height:var(--c-tap-touch); font-weight:700; }
.fc-endacts:not(#solar-studio *){ display:flex; gap:var(--c-s2); align-items:center; }

/* ══ v818 · THE INSTRUMENT REGISTER ══════════════════════════════════════════ */
.mftline:not(#solar-studio *){
  display:flex; flex-wrap:wrap; align-items:center; gap:var(--c-s2);
  margin:var(--c-s1) 0 0; font-size:12.5px;
}
.mftstate:not(#solar-studio *){ display:inline-flex; align-items:center; gap:6px; color:var(--c-mut); }
.mftstate .dot:not(#solar-studio *){ width:8px; height:8px; border-radius:50%; background:var(--c-mut); }
/* FOUR STATES AND A WORD FOR EACH. The colour never carries it alone — an
   expired calibration says "calibration ran out 3 Feb 2026" in the sentence. */
.mftstate-ok .dot:not(#solar-studio *){ background:var(--c-ok-dot); }
/* --c-in-dot, NOT --c-pipe-ink: pipe-ink is a TEXT colour and renders as a brown
   blob when it is used as a fill (CLAUDE.md §7). Caught by the gate on the first
   run of this block, which is exactly what that rule is in the file for. */
.mftstate-due .dot:not(#solar-studio *){ background:var(--c-in-dot); }
.mftstate-expired:not(#solar-studio *){ color:var(--c-bad-ink); font-weight:600; }
.mftstate-expired .dot:not(#solar-studio *){ background:var(--c-bad-dot); }
.mftstate-nocal:not(#solar-studio *){ color:var(--c-bad-ink); }
.mftstate-nocal .dot:not(#solar-studio *){ background:var(--c-bad-dot); }
.mftstate-none .dot:not(#solar-studio *){ background:var(--c-line-strong); }
.mftreg:not(#solar-studio *){ display:flex; flex-direction:column; gap:var(--c-s3); }
.mftrow:not(#solar-studio *){
  padding:var(--c-s3); border:1px solid var(--c-line); border-radius:var(--c-r-card); background:var(--c-panel);
}
.mftrow-expired:not(#solar-studio *), .mftrow-nocal:not(#solar-studio *){ border-color:var(--c-bad-dot); }
.mftrow-grid:not(#solar-studio *){
  display:grid; grid-template-columns:repeat(auto-fit,minmax(170px,1fr)); gap:var(--c-s2) var(--c-s3);
}
.mftrow-state:not(#solar-studio *){
  display:flex; align-items:center; gap:6px; margin:var(--c-s2) 0 0; font-size:12.5px; color:var(--c-mut);
}
.mftrow-state .dot:not(#solar-studio *){ width:8px; height:8px; border-radius:50%; background:var(--c-mut); }
.mftrow-rm:not(#solar-studio *){ margin-top:var(--c-s2); min-height:var(--c-tap-touch); }
.mftreg-acts:not(#solar-studio *){ display:flex; flex-wrap:wrap; gap:var(--c-s2); }
.mftreg-empty:not(#solar-studio *){ margin:0; }

/* ══ v818 · THE BOARD BUTTON ROW ═════════════════════════════════════════════
   Jamie's screenshot: five controls in two ragged rows, each sized by its own
   label, with Remove board sat between two ordinary ones. Grouped by what the
   actions ARE — two adds in one equal grid, the destructive one on its own line
   under a rule at the touch floor. */
.fc-board-acts:not(#solar-studio *){
  display:grid; grid-template-columns:1fr 1fr; gap:var(--c-s2); margin:var(--c-s3) 0 0;
}
.fc-board-acts .btn:not(#solar-studio *){
  min-height:var(--c-tap-touch); width:100%; justify-content:center; text-align:center;
}
/* v825 - CAPTURE-FIRST. Details left this row for the board header (see
   .fc-board-details below), so .fc-board-top is no longer the 1fr/1fr grid it
   was under v823 (that grid still exists as .fc-board-acts, unchanged, for the
   +Circuit/template row further down) - it is now a simple stack of one full-
   width hero button and one full-width secondary button underneath it. */
.fc-board-top:not(#solar-studio *){
  display:flex; flex-direction:column; gap:var(--c-s2); margin:var(--c-s2) 0 var(--c-s3);
}
.fc-board-top .btn:not(#solar-studio *){ width:100%; min-height:var(--c-tap-touch); justify-content:center; text-align:center; }
.fc-board-scan:not(#solar-studio *){ display:inline-flex; align-items:center; justify-content:center; cursor:pointer; }
/* v825 - Scan the board is the hero: the only control on its own row, filled
   amber (same fill as +Circuit's amber below - both are "the common action"
   in their own group), and a touch taller than the 44px floor so it reads as
   the obvious first move on the board card. */
.fc-board-hero:not(#solar-studio *){ min-height:calc(var(--c-tap-touch) + 6px); font-size:15px; font-weight:700; }
/* v823 - the second way into a board (a PDF or a previous certificate) still
   gets its own full-width line under the hero. It is the longest label of the
   two remaining routes and it is the less common one, so it stays quieter than
   the hero above it rather than competing with it for width. text-align
   because the label wraps on a 360px screen. */
.fc-board-import:not(#solar-studio *){ text-align:center; }
/* v825 - Details, moved out of the button row and into the board's own header
   next to its name. Ordinary and reversible under Jamie's 30 Jul tap-floor
   ruling, so it deliberately does NOT take the 44px floor - .btn.small already
   gives it a compact, quiet shape (css/style.css:5116) and flex:0 0 auto stops
   it stretching the header row the way a flex:1 child would. */
.fc-board-h .fc-board-details:not(#solar-studio *){ flex:0 0 auto; margin-left:8px; }
.fc-board-danger:not(#solar-studio *){
  display:flex; flex-wrap:wrap; align-items:center; gap:var(--c-s2);
  margin:var(--c-s3) 0 0; padding:var(--c-s3) 0 0; border-top:1px solid var(--c-line);
}
.fc-board-rm:not(#solar-studio *){ min-height:var(--c-tap-touch); color:var(--c-bad-ink); }
.fc-board-why:not(#solar-studio *){ font-size:12px; color:var(--c-mut); }
.fc-board-add:not(#solar-studio *){ width:100%; min-height:var(--c-tap-touch); margin-top:var(--c-s3); }

/* ══ v818 · THE BOARD SCAN REVEALS LINE BY LINE ══════════════════════════════
   The rows are already rendered and already correct; this only staggers them in.
   NO tick, NO green, NO flourish of completion — a way read off a photo keeps
   the amber "From a photo" flag it arrives with, and animating it into place
   looking settled would be the §4a defect dressed as motion. */
@keyframes sq-scan-in{ from{ opacity:0; transform:translateY(-4px); } to{ opacity:1; transform:none; } }
.scan-in:not(#solar-studio *){ animation:sq-scan-in .26s ease-out both; }
@media (prefers-reduced-motion: reduce){
  .scan-in:not(#solar-studio *){ animation:none; opacity:1; transform:none; }
}

/* ══ v818 · MAISIE'S PANEL ═══════════════════════════════════════════════════
   Jamie: "stuck to the bottom of the page, i just feel its a bit too low, it
   doesnt quite look right."

   It was the shared field sheet, which is glued to the bottom edge by design —
   right for a circuit form you scroll through, wrong for a conversation. Lifted
   off the edge and rounded on all four corners so it reads as a panel she is
   talking from, clear of the phone's home indicator. Scoped with :has so ONLY
   the ask sheet moves and every other sheet in the app is untouched. */
.fa-sheet-wrap:has(.fc-ask-sheet):not(#solar-studio *){
  padding:0 var(--c-s3) calc(var(--c-s4) + env(safe-area-inset-bottom,0px));
}
.fa-sheet-wrap:has(.fc-ask-sheet) .fa-sheet:not(#solar-studio *){
  border-radius:var(--c-r-card); border-bottom:1px solid var(--c-line);
}
/* the sheet's own safe-area padding is now outside it, so the composer must not
   add it a second time or the input floats above a band of empty panel */
.fa-sheet-wrap:has(.fc-ask-sheet) .fc-ask-bar:not(#solar-studio *){ padding-bottom:var(--c-s2); }
.fc-ask-head:not(#solar-studio *){ display:inline-flex; align-items:center; gap:var(--c-s2); }
/* her face beside every answer, at 28px — drawn to hold at 24 */
.fc-ask-airow:not(#solar-studio *){ display:flex; align-items:flex-start; gap:var(--c-s2); max-width:96%; }
.fc-ask-face:not(#solar-studio *){ flex:0 0 auto; line-height:0; margin-top:2px; }
.fc-ask-airow .fc-ask-ai:not(#solar-studio *){ align-self:flex-start; max-width:100%; }
/* the thinking indicator: three dots, because "…thinking" as text reads as a
   message she has sent rather than a pause before one */
.fc-ask-dots:not(#solar-studio *){ display:inline-flex; align-items:center; gap:4px; height:14px; }
.fc-ask-dots i:not(#solar-studio *){
  width:6px; height:6px; border-radius:50%; background:var(--c-mut);
  animation:sq-ask-dot 1.05s ease-in-out infinite;
}
.fc-ask-dots i:nth-child(2):not(#solar-studio *){ animation-delay:.16s; }
.fc-ask-dots i:nth-child(3):not(#solar-studio *){ animation-delay:.32s; }
@keyframes sq-ask-dot{ 0%,70%,100%{ opacity:.28; } 35%{ opacity:1; } }
@media (prefers-reduced-motion: reduce){
  .fc-ask-dots i:not(#solar-studio *){ animation:none; opacity:.6; }
}

/* ══ SEVENS · HQ · TOOL 2 — frontend-design, v812 ════════════════════════════
   ⚠ APPENDED, SO check-compliance-design.js's "Lane C" slice OWNS THIS BLOCK:
   it slices from the last comment opener before the Lane C marker and never
   stops, so everything below that marker is measured by it. Checked before
   appending, not discovered from a red build. Nothing here needs an exception:
   every value is a closed-palette token on text or a spacing/size token, there
   is no color-mix, no raw hex outside these comments, no --c-ink-money or
   --c-pipe-ink used as a fill, and no single-letter modifier class.

   ── WHY HQ IS IN THIS FILE AT ALL, AND WHAT THAT IS NOT ─────────────────────
   HQ chrome lives ENTIRELY in css/style.css on hardcoded hex, with two rules in
   this file before today. That is not the result of an exclusion rule — the
   "HQ is not in scope yet" note further up governs ONE list, the irreversible
   tap-floor lift, and nothing else. HQ simply never had rules written here.
   This block does the part that is needed and provable, and deliberately does
   NOT rewrite HQ's palette wholesale: that would reach leads, agents, billing,
   the email studio and the affiliate desk, none of which any tool in this wave
   has assessed, and a blast radius nobody measured is not a design decision.

   ══ 1 · THE TWO OVERLAYS ARE FRAME SURFACES, AND THEIR TEXT DID NOT KNOW IT ══
   .hq-card and .onb-card set a near-black ground. That ground IS --c-frame, to
   the byte, and the ink they were built for IS --c-frame-ink and --c-frame-mut,
   also to the byte: HQ was written in the frame palette years before the tokens
   were named. The text inside, though, runs on the THEME tokens, which flip to
   dark ink under body.theme-light — so in the theme Jamie actually looks at,
   dark type sits on a dark card.

   Measured at 1440 and 834, light, composited through the real paint stack:
     plan editor   27 of 37 text elements fail, every one of them at 3.20:1
     wizard        13 of 49 fail, worst h2.onb-t at 1.14:1 (the step heading)
     the DESK      0 of 63 — because style.css already remediates it, scoped to
                   #view-hqdesk, which the two overlays are not inside.
   So the defect is "the remediation was scoped to one host", not "the global
   label rule is wrong". Rewriting label{color:var(--muted)} would reach 8,000
   lines of style.css for a fault that lives in two overlays.

   THE FRAME TOKENS DO NOT FLIP — that is the whole reason they are the right
   answer here rather than --c-ink. Their light values are identical to the dark
   theme's --c-ink and --c-mut, so this block cannot move a single dark-theme
   pixel, and the gate asserts exactly that rather than asserting it in prose. */
#hq-plan .hq-card, #hq-onb .onb-card,
#hq-plan .hq-row, #hq-plan .hq-row strong,
#hq-onb .onb-t, #hq-onb .onb-bolt b{ color:var(--c-frame-ink); }
/* ⚠ .onb-total b IS DELIBERATELY NOT IN THAT LIST, AND IT WAS UNTIL THE
   BEFORE/AFTER DIFF CAUGHT ME. It is the price — "£19.99 a month", 22px Clash
   Display in brand yellow — and it is the one hero on the package step. Sweeping
   it into the body ink flattened the number the whole screen is about, in the
   dark theme, where nothing was wrong with it: #FFDD33 on the frame ground
   measures about 12:1. The diff reported it as two elements whose ink moved and
   that is the only reason it was noticed. Spend the boldness in one place and
   leave that place alone. */

/* secondary: the price slot, the parenthetical, the hints and the field labels.
   --c-frame-mut on the row ground measures 7.02:1, so this is not a squeak. */
#hq-plan .hq-row > span:last-child, #hq-plan .hq-row i, #hq-plan .hq-hint,
#hq-onb .onb-sub, #hq-onb .onb-hint, #hq-onb .onb-f > span,
#hq-onb .onb-bolt span, #hq-onb .onb-total span, #hq-onb .onb-x{ color:var(--c-frame-mut); }

/* ══ 2 · TWO CORRECT RULES FIGHTING, ON A PROPERTY ONLY ONE OF THEM SETS ═════
   style.css:173 is `label{ display:flex; flex-direction:column }`. .hq-row sets
   `display:flex; align-items:center` and NEVER sets flex-direction, so the
   generic label rule wins uncontested — not because it out-specifies anything,
   but because nothing contests it. CLAUDE.md section 7's commonest defect shape,
   and a grep cannot see it.

   Driven: all 16 label.hq-row compute flex-direction:column at ~98px each, and
   the card is scrollHeight 1862 against clientHeight 844 — 2.2x its own viewport
   to reach Save plan, which renders at y=1828 and is below the fold at BOTH 1440
   and 834. The operator cannot see the button that commits the sale.

   Cascade, read as section 7 requires — WHICH PROPERTY, then the rest: both
   rules set flex-direction, so they genuinely compete. This one is (1,1,1)
   against (0,0,1) and design-c.css loads after style.css, so it wins twice over.
   Neither an !important nor the solar guard is doing any work here. */
#hq-plan label.hq-row{ flex-direction:row; }

/* …and that alone is not enough, which the after-measurement showed rather than
   the before-reasoning. Unstacking the rows takes the card from scrollHeight
   1862 to 1150 against a clientHeight of 844, so Save plan moves from y=1828 to
   y=1116 — better by 712px and STILL below the fold. The commit control on the
   screen that changes what a paying account can open should not have to be
   hunted for. The card is the scroller, so the button sticks to its bottom edge:
   it is opaque brand yellow already, so nothing shows through it, and it stays
   in the flow so it never overlaps the last row. */
#hq-plan .hq-cta{ position:sticky; bottom:0; }

/* ══ 3 · THE CONTROLS THAT WERE UNDER THE DESKTOP FLOOR ══════════════════════
   HQ is a desktop product, so the floor is --c-tap (34px), not 44. Two controls
   were under it and both are measured rendered rectangles, not computed styles:
     .onb-x            24.2 x 20px — and it discards a seven-step discovery
     .hq-row select    392 x 33px  — one pixel under, which is still under
   The bolt and module CHECKBOXES are NOT in this list and must not be: each is
   wrapped in its own label, so the whole 60px row is the target. Measuring the
   17px input instead is how a sibling pass produced ~110 phantom failures. */
#hq-onb .onb-x{
  min-width:var(--c-tap); min-height:var(--c-tap);
  display:inline-flex; align-items:center; justify-content:center;
  border-radius:var(--c-r-chip);
}
#hq-plan .hq-row select,
#hq-plan .hq-row input:not([type="checkbox"]){ min-height:var(--c-tap); }

/* ══ 4 · THE NOTICE BAR STOPS SITTING ON THE PAGE IT IS TALKING ABOUT ════════
   sqBillingBar() publishes its own rendered height as --sq-billbar-h and marks
   the body, so the reservation is the bar's ACTUAL height in that notice, at
   that width, with that wrapping — 55px, 59px and 78px were all measured on the
   same three notices — instead of a constant that is wrong two times in three.

   The AI orb is the reason this is not just tidiness. It is position:fixed at
   the bottom right and the bar now has an OPAQUE ground, so without this the
   orb is completely covered: 54x54 at 834, a full collision. Making the notice
   readable would have made the assistant unreachable. Both move together. */
body.has-billbar{ padding-bottom:var(--sq-billbar-h, 0px); }
body.has-billbar .ai-fab{ bottom:calc(var(--sq-billbar-h, 0px) + var(--c-s3)); }

/* ══ 5 · A DISABLED ROW MAY NOT CLAIM TO BE CLICKABLE ════════════════════════
   The four modules a launch-tier customer already pays for render ticked and
   disabled, which is right. They also rendered cursor:pointer, which says the
   opposite — an affordance promising something the control refuses. The row
   keeps its full ground and its full-strength ink deliberately: these are the
   things the customer IS buying, and dimming them is what made them the four
   faintest rows on the screen before tool 1 removed the opacity. */
#hq-onb .onb-bolt.onb-inc{ cursor:default; }

/* ══ 6 · v815 · AND THE SAME REMEDY WAS NEVER CARRIED TO THE WIZARD ══════════
   Rule 2 above pinned Save plan because "the commit control on the screen that
   changes what a paying account can open should not have to be hunted for". The
   WIZARD is where the sale is actually made, and its primary action was off
   screen on exactly one step of seven — step 5, The package, the one carrying
   the rate card, the price and the twelve bolt-ons.

   Measured on all seven steps at 1440x900 and 834x1112, rendered rectangles:
     step 1 The business      Next y=733   in view
     step 2 People & size     Next y=635   in view
     step 3 Their setup       Next y=649   in view
     step 4 Goals & pain      Next y=599   in view
     step 5 THE PACKAGE       Next y=1075  OUT — 220px below the card's own
                              bottom edge and 219px below the fold, scroll 285
     step 6 Billing & go-live Next y=635   in view
     step 7 Their brand       Next y=574   in view
   `position` on both the button and its footer was `static`; nothing was pinned.

   WHY THE FOOTER AND NOT THE BUTTON, which is where this differs from rule 2.
   .hq-cta is one full-width opaque yellow bar, so sticking the button alone is
   enough — nothing can show through it. .onb-nav is a transparent flex ROW with
   Back at one end and Next at the other, so the card's own content would scroll
   through the gap between them. The strip therefore carries the ground, and the
   ground is --c-frame because rule 1 above establishes that IS .onb-card's own
   near-black, to the byte. Both buttons keep their own paint.

   ⚠ AND THE CARD'S BOTTOM PADDING MOVES ONTO THE STRIP, WHICH IS NOT TIDINESS.
   A sticky `bottom:0` resolves against the scrollport's CONTENT box, so .onb-card's
   own padding-bottom:20px (css/style.css) stays BELOW the pinned strip and content
   scrolls through it — measured, and visible in the first after-shot as a row of
   bolt-on cards sliding under the buttons. Moving that 20px from the card onto the
   strip's own painted padding closes the gap with no magic offset and no coupling
   to a number in another file: there is simply nothing left under the strip to
   scroll through, and the breathing room below the buttons is unchanged because it
   is the same 20px, painted by whoever is now at the bottom. */
#hq-onb .onb-card{ padding-bottom:0; }
#hq-onb .onb-nav{
  position:sticky; bottom:0;
  margin:var(--c-s4) 0 0;
  padding:var(--c-s3) 0 var(--c-s5);
  background:var(--c-frame);
}

/* ⚠ APPENDING TO THIS FILE: THREE GATES SLICE FROM A MARKER TO THE END OF IT, AND
   THEY WILL SILENTLY OWN WHATEVER YOU ADD. Each one does a ONE-ARGUMENT
   .slice(marker) with no closing bound, so each runs to the literal EOF:

     check-settings-floor.js     from "v787 · THE SETTINGS QUALITY FLOOR"  (:3711)
     check-compliance-design.js  from "LANE C · COMPLIANCE & FLEET"        (:4628)
     check-hq-reads.js           from "SEVENS · HQ · TOOL 2"               (:5809)
   (re-derived v821 after the certificate row block above grew by 107 lines —
    the numbers are only ever right until the next insertion above them, so
    grep for the marker text, never for the number.)

   The HQ one requires every selector in its slice to be scoped to #hq-plan,
   #hq-onb or has-billbar; the Settings one requires #view-settings, .au-ready,
   .conn-pill, .tax-code-in or #set-sec-; the Lane C one bans raw hex, six retired
   hues and single-letter modifier classes. The v814 certificate block was appended
   at the end, checked against the Lane C slice only, and turned the HQ gate red on
   its first full build; it now sits ABOVE the HQ marker instead.

   ⚠ THIS COMMENT ITSELF SAID "TWO" UNTIL v819, AND THE MISSING THIRD WAS THE
   SETTINGS ONE — whose marker is the EARLIEST of the three, so it silently owns the
   most. It stays green today only because the intervening rules happen not to match
   its single-line selector regex, which is luck, not design. A warning that
   enumerates the hazard incompletely is worse than none: it reads as a checklist
   somebody already completed.

   Check EVERY marker-to-EOF slice before you append — grep scripts/ for
   design-c.css rather than trusting this list — or insert above them all. The
   safest placement for a genuinely new block is ABOVE line 3604. */
