/* ═══════════════════════════════════════════════════════════════════════════
   NUSHAKE style-v2.css — Phase 2 shared component library (production candidate)
   Direction: D1 — JOBSITE SIGNAGE as amended (design/DIRECTION.md C1-C6, G1-G6).
   Token law: design/tokens/tokens.css (locked 2026-08-15) — embedded verbatim
   below as this sheet's :root. No value in any component rule may bypass it.
   Gate: tasks/nushake-token-lint.py (G2). Classes only. Tokens only.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ══ 1 · LOCKED TOKENS — verbatim copy of design/tokens/tokens.css :root ══ */

:root {

  /* ── COLOR ─────────────────────────────────────────────────────────────
     THE GOLD LAW (audit rank 1 — encoded here, not in prose):
     1. Gold is an ACTION/SAFETY color. Its only legal token names are the
        *-action / *-hazard / *-focus aliases below. There is deliberately
        NO --gold-surface, NO --gold-tint, NO --gold-glow token. If a
        component "needs" one, the component is wrong.
     2. GOLD TEXT sits on --ink or --surface-form ONLY (12.1:1 / 10.3:1).
        Gold text on white/paper is a build error (1.6:1 — AA fail).
     3. GOLD BACKGROUNDS carry --ink text ONLY (12.1:1). Never white text
        on gold (1.5:1 — the rank-2 defect this system deletes).
     4. Off-brand golds are dead: #FEBD17, #f5b400, #b8860b, #e8a900-as-
        decoration do not exist in this system. One gold. (audit rank 6)
     5. GOLD BUDGET: ≤5% of the pixels in any viewport. Legal carriers:
        buttons, form submit, hazard stripes/rules, 4px card top-borders,
        focus halo, the frozen .trust-bar band, and inline text links on
        dark surfaces. Nothing else. */

  --gold-action:   #FFCD00;  /* CTAs, submit, hazard marks. THE only gold. */
  --gold-pressed:  #e0b400;  /* :active/:hover state of --gold-action ONLY.
                                Never used at rest, never as decoration.   */
  --link-on-light: #6b5500;  /* dark gold for gold-flavored links on white —
                                6.2:1 on --paper; replaces every #b8860b   */

  --ink:        #090909;     /* plates, slabs, rules, body-on-gold          */
  --ink-2:      #161616;     /* deep panel variant (footer sub-surfaces)    */
  --surface-form: #1b1b1b;   /* the dark lead form — kept, now a token
                                (audit "deliberately not fixed" #14)       */
  --white:      #ffffff;
  --paper:      #fafaf8;     /* light section alt — the only off-white     */
  --line:       #d9d7cc;     /* hairline rules on light                    */
  --line-dark:  rgba(255,255,255,.16);  /* hairline rules on ink           */
  --tx:         #161616;     /* body text on light — 16.6:1                */
  --tx-2:       #3f3f3f;     /* secondary text on light — 9.9:1            */
  --tx-3:       #5c5c5c;     /* captions ≥14px on light — 6.3:1 (AA)       */
  --tx-inv:     #ffffff;     /* body text on ink                           */
  --tx-inv-2:   rgba(255,255,255,.80);  /* secondary on ink — 13.1:1       */
  --tx-inv-3:   rgba(255,255,255,.64);  /* captions on ink — 8.6:1         */
  --field-line: rgba(255,255,255,.44);  /* input borders on --surface-form:
                                composites ≈#8a8a8a on #1b1b1b = 3.6:1 —
                                fixes the 1.19:1 rank-5 boundary failure   */

  /* ── TYPE ──────────────────────────────────────────────────────────────
     FONT BUDGET (§4): 2 families, 4 weights TOTAL, self-hosted, subset.
     Family 1: Archivo variable (wght 100-900, ONE 35KB file, already
               self-hosted at /assets/fonts/archivo-latin-var.woff2).
               Used weights: 400 (body), 800 (UI labels/buttons).
     Family 2: Barlow Condensed — THE display face. Highway-signage DNA
               (drawn from CA public-works lettering), goes to 900 where
               Oswald stops at 700 and Archivo Narrow isn't condensed
               enough. Used weights: 700 (sub-display), 900 (display).
               PREVIEW loads Google Fonts CDN; PRODUCTION ships two
               self-hosted subset woff2 files per R15. 400+800+700+900
               = 4 weights. Budget met exactly. */
  --font-display: 'Barlow Condensed','Archivo Fallback','Arial Narrow',sans-serif;
  --font-body:    'Archivo','Archivo Fallback',system-ui,sans-serif;

  /* CONDENSED FLOOR (§4, stated): Barlow Condensed never renders below
     18px (1.125rem). Anything smaller is set in Archivo. Condensed under
     18px smears at 200% zoom and on low-DPI panels. */
  --fs-display-floor: 1.125rem;

  /* Type scale — signage ratios: big jumps, tight leading. */
  --fs-hero:    clamp(3rem, 8vw, 6rem);        /* Barlow 900 UPPER, lh .92 */
  --fs-h2:      clamp(2.2rem, 5vw, 3.8rem);    /* Barlow 900 UPPER, lh .95 */
  --fs-h3:      clamp(1.4rem, 2.4vw, 1.9rem);  /* Barlow 700 UPPER, lh 1.05*/
  --fs-plate:   clamp(1.125rem, 1.8vw, 1.5rem);/* Barlow 700 UPPER — floor */
  --fs-body-lg: 1.15rem;                       /* Archivo 400, lh 1.6      */
  --fs-body:    1rem;    /* 17px via html base */
  --fs-small:   .875rem;
  --fs-label:   .72rem;  /* Archivo 800 UPPER, letter-spacing .12em        */
  --lh-display: .95;
  --lh-body:    1.55;
  --track-label:.12em;

  /* ── SPACE — 8px grid, no arbitrary values ──────────────────────────── */
  --sp-1: 4px;  --sp-2: 8px;  --sp-3: 12px; --sp-4: 16px;
  --sp-5: 24px; --sp-6: 32px; --sp-7: 48px; --sp-8: 64px; --sp-9: 96px;
  --sect-pad: clamp(56px, 9vw, 112px);   /* desktop section rhythm        */
  --wrap-max: 1200px;
  --wrap-pad: clamp(16px, 4vw, 48px);

  /* ── RADIUS — the discipline is the identity ────────────────────────── */
  --radius: 0;   /* LAW. No second radius token exists. 547 inline
                    overrides get swept to this. (audit rank 7)           */

  /* ── SHADOW — signage has no shadows ────────────────────────────────── */
  --shadow: none;  /* LAW. Depth is drawn with rules and plates, never
                      blur. All --shadow-card/-hover/-warm are deleted.   */

  /* ── RULES (borders are this direction's whole decoration system) ───── */
  --rule-1: 1px solid var(--line);
  --rule-2: 2px solid var(--ink);
  --rule-3: 3px solid var(--ink);   /* trust-bar frame (frozen)           */
  --rule-4: 4px solid var(--ink);
  --rule-gold-4: 4px solid var(--gold-action); /* card signature — KEPT   */

  /* ── HAZARD STRIPE (the .hazard-rule seed, systematized) ────────────── */
  /* Hard-stop stripe: two flat colors, zero interpolation. This is
     GEOMETRY, not a gradient — the kill-list bans color RAMPS. Any
     stop pair that interpolates is illegal. */
  --hazard: repeating-linear-gradient(-45deg,
              var(--gold-action) 0 12px, var(--ink) 12px 24px);
  --hazard-h-sm: 6px;    /* sub-section divider  */
  --hazard-h:    10px;   /* section divider      */
  --hazard-h-lg: 16px;   /* page-level divider   */

  /* ── FOCUS (two-tone system kept verbatim — audit keep-list #12) ────── */
  --focus-ring: 3px solid var(--ink);
  --focus-halo: 0 0 0 6px var(--gold-action);

  /* ── BREAKPOINTS (reference — use in @media literally) ──────────────── */
  /* --bp-sm: 375px   --bp-md: 768px   --bp-lg: 1100px   --bp-xl: 1440px */

  /* ── MOTION ─────────────────────────────────────────────────────────── */
  --ease: cubic-bezier(.25,.1,.25,1);
  --dur:  .18s;   /* signage snaps; nothing floats or lifts               */
}

/* ══ 2 · CARRIED + AMENDMENT TOKENS ══════════════════════════════════════
  --error: carried forward from production R12 (style.css tail, deployed
   2026-08-14) — the form error components below are its consumers.
   --focus-ring-inv / --focus-halo-inv: the on-ink INVERSION of the locked
   two-tone focus pair (identity keep-list #12 — production nav/footer
   already invert; same colors, swapped roles, no new values).
   --font-display: A2 is now SATISFIED — barlow-condensed-{700,900}.woff2
   exist on disk under /assets/fonts/ and the Barlow-metric fallback face
   is declared below, so the display stack swaps its fallback from the
   Archivo-matched face (which A2 outlaws for Barlow) to 'Barlow Fallback'. */
:root{
  --error:#b3261e;
  --nav-h:68px;
  --focus-ring-inv:3px solid var(--gold-action);
  --focus-halo-inv:0 0 0 6px var(--ink);
  --font-display:'Barlow Condensed','Barlow Fallback','Arial Narrow',sans-serif;
}

/* ══ 3 · FONTS — self-hosted, subset, metric-matched fallbacks (C2/R15) ══ */
@font-face{font-family:'Archivo';font-style:normal;font-weight:100 900;font-display:swap;src:url('/assets/fonts/archivo-latin-var.woff2') format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
@font-face{font-family:'Archivo Fallback';src:local('Arial');size-adjust:119.62%;ascent-override:73.40%;descent-override:17.56%;line-gap-override:0%}
@font-face{font-family:'Barlow Condensed';font-style:normal;font-weight:700;font-display:swap;src:url('/assets/fonts/barlow-condensed-700.woff2') format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
@font-face{font-family:'Barlow Condensed';font-style:normal;font-weight:900;font-display:swap;src:url('/assets/fonts/barlow-condensed-900.woff2') format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
/* Barlow-metric fallback — computed from the real barlow-condensed font tables (C2) */
@font-face{font-family:'Barlow Fallback';src:local('Arial Narrow'),local('Roboto Condensed');size-adjust:124.10%;ascent-override:80.58%;descent-override:16.12%;line-gap-override:0%}

/* ══ 4 · RESET + BASE ════════════════════════════════════════════════════ */
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
html{scroll-behavior:smooth;-webkit-text-size-adjust:100%;font-size:17px;scroll-padding-top:84px}
body{font-family:var(--font-body);font-size:var(--fs-body);line-height:var(--lh-body);color:var(--tx);background:var(--white);padding-top:var(--nav-h)}
/* --nav-h: the fixed .nav measures 68px at every width (measured 375 and 1440). Without this
   offset the first 68px of every page — the mobile breadcrumb included — renders under the
   opaque nav. Found by the service canonical, verified on both templates. */
img{max-width:100%;display:block}
a{color:inherit;text-decoration:none}
@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}html{scroll-behavior:auto}}

/* ── two-tone focus (locked pair; inverted on ink chrome). .s-focus is the
   gallery's forced duplicate of :focus-visible — same declarations. ── */
:focus-visible{outline:var(--focus-ring);outline-offset:2px;box-shadow:var(--focus-halo)}
.s-focus{outline:var(--focus-ring);outline-offset:2px;box-shadow:var(--focus-halo)}
.on-ink :focus-visible,.nav :focus-visible,.footer :focus-visible,.mob-menu :focus-visible{outline:var(--focus-ring-inv);outline-offset:2px;box-shadow:var(--focus-halo-inv)}
.on-ink .s-focus,.nav .s-focus,.footer .s-focus,.mob-menu .s-focus{outline:var(--focus-ring-inv);outline-offset:2px;box-shadow:var(--focus-halo-inv)}

/* ── layout + type utilities ── */
.wrap{max-width:var(--wrap-max);margin:0 auto;padding:0 var(--wrap-pad)}
.section{padding:var(--sect-pad) 0}
.display{font-family:var(--font-display);font-weight:900;text-transform:uppercase;line-height:var(--lh-display);letter-spacing:.01em}
.label{font-family:var(--font-body);font-weight:800;font-size:var(--fs-label);letter-spacing:var(--track-label);text-transform:uppercase}
.visually-hidden{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
.botcheck{display:none!important}
/* NS-D1 photo grade — one grade, applied in CSS; originals untouched (G3/§4) */
.img-grade{filter:saturate(.85) contrast(1.12) brightness(.94)}
@media(max-width:768px){:root{--sect-pad:clamp(40px,10vw,56px)}}

/* ── hazard divider system (hard-stop geometry, not a color ramp) ── */
.hz{height:var(--hazard-h);background:var(--hazard)}
.hz-sm{height:var(--hazard-h-sm);background:var(--hazard)}
.hz-lg{height:var(--hazard-h-lg);background:var(--hazard)}
.hazard-rule{height:4px;width:100%;background:var(--gold-action)} /* the seed, kept */

/* ══ 5 · SKIP LINK ═══════════════════════════════════════════════════════ */
.skip-link{position:absolute;left:8px;top:-56px;z-index:200;background:var(--gold-action);color:var(--ink);padding:14px 20px;border:var(--rule-2);font-weight:800;font-size:var(--fs-small);transition:top var(--dur)}
.skip-link:focus,.skip-link.s-focus{top:0}

/* ══ 6 · PLATES · SLABS · SPEC (section-header system) ═══════════════════
   A1: eyebrows are stenciled labels INSIDE the plate — never gold chips.
   Gold appears here only as the single legal 4px rule. ══ */
.plate{display:inline-block;background:var(--ink);color:var(--tx-inv);border-left:var(--rule-gold-4);padding:10px 22px 10px 18px}
.plate .label{color:var(--tx-inv-2);display:block;margin-bottom:2px}
.plate h1,.plate h2,.plate h3,.plate .plate-h{font-family:var(--font-display);font-weight:900;text-transform:uppercase;font-size:var(--fs-h2);line-height:var(--lh-display);letter-spacing:.01em;color:var(--tx-inv)}
@media(max-width:768px){.plate{display:block;margin:0 calc(-1 * var(--wrap-pad))}}
.slab span{background:var(--ink);color:var(--tx-inv);padding:.08em .35em;-webkit-box-decoration-break:clone;box-decoration-break:clone}
.slab em{font-style:normal;color:var(--gold-action)} /* legal ONLY when the em phrase is a call to act (tokens law) */
.spec{border:var(--rule-2);background:var(--white)}
.spec-row{display:flex;flex-wrap:wrap;gap:var(--sp-4);justify-content:space-between;align-items:baseline;padding:12px 16px;border-bottom:var(--rule-1)}
/* Overflow law (2026-09-08, Dublin 390/768/1440): a row never leaks past its column.
   The key keeps its full width (no shrink) and only wraps when it is wider than
   the column itself (max-width:100%). The value sits beside the key and wraps
   there, exactly as before, and drops to its own line ONLY when it cannot fit
   beside the key at its min-content width (flex-basis 0 + min-width:min-content
   is what makes the line-break test use min-content instead of max-content).
   Rows that fit are pixel-identical to the pre-2026-09-08 rendering. */
.spec-row:last-child{border-bottom:none}
.spec-key{flex:0 0 auto;max-width:100%;font-family:var(--font-display);font-weight:700;text-transform:uppercase;font-size:var(--fs-plate);letter-spacing:.03em}
.spec-val{flex:1 1 0;min-width:min-content;font-size:var(--fs-small);color:var(--tx-2);text-align:right}

/* ══ 7 · BUTTONS — exactly three. Hover = background swap only; no
   translateY, no shadow (kill-list). ══ */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:var(--sp-2);min-height:48px;padding:12px 30px;font-family:var(--font-body);font-weight:800;font-size:var(--fs-small);letter-spacing:.04em;text-transform:uppercase;border:var(--rule-2);cursor:pointer;transition:background var(--dur) var(--ease),color var(--dur) var(--ease);text-decoration:none}
.btn-primary{background:var(--gold-action);color:var(--ink)}
.btn-primary:hover,.btn-primary.s-hover{background:var(--gold-pressed)}
.btn-primary:active,.btn-primary.s-active{background:var(--gold-pressed)}
.btn-secondary{background:transparent;color:var(--ink)}
.btn-secondary:hover,.btn-secondary.s-hover,.btn-secondary:active,.btn-secondary.s-active{background:var(--ink);color:var(--white)}
.on-ink .btn-secondary{color:var(--white);border-color:var(--white)}
.on-ink .btn-secondary:hover,.on-ink .btn-secondary.s-hover,.on-ink .btn-secondary:active,.on-ink .btn-secondary.s-active{background:var(--white);color:var(--ink)}
.btn-inv{background:var(--ink);color:var(--white);border-color:var(--ink)} /* on gold surfaces only */
.btn-inv:hover,.btn-inv.s-hover,.btn-inv:active,.btn-inv.s-active{background:var(--ink-2)}
.btn[disabled],.btn.s-disabled{opacity:.45;cursor:not-allowed;pointer-events:none}
.btn.s-loading{opacity:.75;cursor:wait;pointer-events:none}

/* ══ 8 · NAV (C4: collapse breakpoint raised to 1100px; closed menu leaves
   the tab order via visibility) ══ */
.nav{position:fixed;top:0;left:0;right:0;z-index:100;background:var(--ink);border-bottom:var(--rule-gold-4)}
.nav--demo{position:static}
.nav-in{max-width:var(--wrap-max);margin:0 auto;display:flex;align-items:center;justify-content:space-between;gap:var(--sp-4);height:64px;padding:0 var(--wrap-pad)}
.nav-logo img{height:44px;width:auto}
.nav-links{display:flex;align-items:center;gap:var(--sp-5);list-style:none}
.nav-links a{font-family:var(--font-body);font-weight:800;font-size:var(--fs-label);letter-spacing:var(--track-label);text-transform:uppercase;color:var(--tx-inv-2);padding:12px 0;transition:color var(--dur)}
.nav-links a:hover,.nav-links a.s-hover{color:var(--white)}
.nav-links a[aria-current="page"]{color:var(--gold-action)} /* text link on dark — legal carrier */
.nav-leak a{color:var(--gold-action)}                        /* gold on ink = 12.1:1, legal carrier */
.nav-phone{color:var(--white)!important;font-weight:800}
.nav-cta{min-height:0;padding:12px 20px}
/* P4-FIX 2026-08-16: .nav-links a (0,1,1) outranked .btn-primary (0,1,0), so the gold-filled
   nav CTA inherited the dark-nav text colour — white-on-gold at 1.37:1 on 73 of 74 pages.
   Desktop-only, which is why the 375 sweep missed it. Raised to (0,2,1); the aria-current
   variant is included so the current-page CTA cannot land gold-on-gold either. */
.nav-links a.nav-cta,.nav-links a.nav-cta[aria-current="page"]{color:var(--ink)}

/* P4-FIX 2026-08-16: the why-cards carry TWO ordinals — a decorative watermark
   (aria-hidden, v1 .why-ghost) and the real badge (v1 .why-num). Parity requires both
   text nodes to survive, and the aria-hidden did survive, but v2 rendered the ghost at
   full weight so each card showed its number twice at 34px and 10px. Restores v1 intent:
   the aria-hidden numeral becomes a recessive watermark, tokens only. */
.card{position:relative}
.card>.led-num[aria-hidden="true"]{position:absolute;top:0;right:var(--sp-3);z-index:0;
  font-size:var(--fs-hero);line-height:.8;color:var(--ink);opacity:.05;
  pointer-events:none;user-select:none}
.card>.led-num[aria-hidden="true"]~*{position:relative;z-index:1}
.ham{display:none;background:none;border:2px solid var(--white);cursor:pointer;padding:10px;min-width:44px;min-height:44px}
.ham span{display:block;width:20px;height:2px;background:var(--white);margin:4px 0}
@media(max-width:1099px){.nav-links{display:none}.ham{display:block}}
/* mobile menu — closes via visibility, not opacity alone (C4) */
/* z-index 1000000001, not 99: the quote launcher's root
   (#roof-quote-pro-widget) is its own stacking context at z 1000000000,
   and at 99 its panther graphic floated OVER the open menu, stealing taps
   from the right end of two menu links INCLUDING THE TEL LINK (proven with
   elementFromPoint on the home page, 2026-08-24; every one of the 134
   pages carries the same menu + the same launcher). A full-screen modal
   sits above everything. Closed, the menu is visibility-hidden (C4), so
   the raised z captures nothing and the launcher works normally. */
.mob-menu{position:fixed;inset:0;z-index:1000000001;background:var(--white);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--sp-5);opacity:0;visibility:hidden;transition:opacity var(--dur),visibility 0s linear var(--dur)}
.mob-menu.open{opacity:1;visibility:visible;transition:opacity var(--dur),visibility 0s}
.mob-menu a{font-family:var(--font-display);font-weight:700;font-size:1.6rem;color:var(--ink);text-transform:uppercase;padding:6px 12px;min-height:44px;display:inline-flex;align-items:center}
.mob-menu a[aria-current="page"]{color:var(--link-on-light)}
.mm-leak{background:var(--ink);color:var(--tx-inv)!important}
.mm-cta{background:var(--gold-action);color:var(--ink)!important;border:var(--rule-2)}
.mob-close{position:absolute;top:16px;right:20px;background:none;border:2px solid var(--ink);font-size:1.6rem;line-height:1;cursor:pointer;color:var(--ink);min-width:44px;min-height:44px}
.mob-menu--demo{position:static;visibility:visible;opacity:1;z-index:auto;padding:var(--sp-6) 0;border:var(--rule-1)}
.mob-menu--demo .mob-close{position:static;align-self:flex-end;margin-right:var(--sp-5)}

/* ══ 9 · MOBILE BAR — persistent, SOLID white, no blur; gold cell targets
   the on-page #quote form (G4 / rank 19) ══ */
.mob-bar{display:none;position:fixed;bottom:0;left:0;right:0;z-index:90;background:var(--white);border-top:var(--rule-2);padding:8px 12px}
.mob-bar-in{display:grid;grid-template-columns:1fr 1fr;gap:var(--sp-2);max-width:480px;margin:0 auto}
.mob-bar a{display:flex;align-items:center;justify-content:center;gap:6px;min-height:48px;font-weight:800;font-size:var(--fs-small);text-transform:uppercase;letter-spacing:.02em}
.mb-call{color:var(--ink);border:var(--rule-2)}
.mb-form{background:var(--gold-action);color:var(--ink);border:var(--rule-2)}
.mb-form:hover,.mb-form.s-hover,.mb-form:active,.mb-form.s-active{background:var(--gold-pressed)}
.mob-bar svg{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2}
@media(max-width:768px){.mob-bar{display:block}body{padding-bottom:66px}}
.mob-bar--demo{display:block;position:static}

/* ══ 10 · FORMS — dark (.form-dark, the #quote estimate form) and light
   (.form-light) variants. Carries the R12 error components. ══ */
.form-dark{background:var(--surface-form);border:var(--rule-2);border-top:var(--rule-gold-4);padding:clamp(24px,3vw,36px)}
.form-h2{font-family:var(--font-display);font-weight:900;text-transform:uppercase;font-size:1.7rem;color:var(--white);margin-bottom:4px}
.form-sub{font-size:var(--fs-small);color:var(--tx-inv-3);margin-bottom:var(--sp-4)}
.form-points{list-style:none;margin:0 0 var(--sp-4);border-left:var(--rule-3);padding-left:var(--sp-3);display:flex;flex-direction:column;gap:6px;font-size:var(--fs-small);color:var(--tx-inv-2)}
.form-points strong{color:var(--white)}
.ff{margin-bottom:var(--sp-3)}
.ff label{display:block;font-weight:800;font-size:var(--fs-label);letter-spacing:var(--track-label);text-transform:uppercase;margin-bottom:5px}
.form-dark .ff label{color:var(--tx-inv-2)}
.form-dark .req{color:var(--tx-inv-2)}
.ff input,.ff select,.ff textarea{width:100%;min-height:48px;padding:12px 14px;font-family:var(--font-body);font-size:16px;transition:border-color var(--dur)}
.ff textarea{min-height:96px;resize:vertical}
.form-dark .ff input,.form-dark .ff select,.form-dark .ff textarea{background:var(--ink);border:1.5px solid var(--field-line);color:var(--white);color-scheme:dark}
.form-dark .ff input::placeholder,.form-dark .ff textarea::placeholder{color:var(--tx-inv-3)}
.form-dark .ff select option{background:var(--ink-2);color:var(--white)}
.form-light{background:var(--white);border:var(--rule-2);border-top:var(--rule-gold-4);padding:clamp(24px,3vw,36px)}
.form-light .form-h2{color:var(--ink)}
.form-light .form-sub{color:var(--tx-3)}
.form-light .ff label{color:var(--tx-3)}
.form-light .req{color:var(--tx-2)}
.form-light .ff input,.form-light .ff select,.form-light .ff textarea{background:var(--white);border:var(--rule-2);color:var(--tx)}
.form-light .ff input::placeholder,.form-light .ff textarea::placeholder{color:var(--tx-3)}
.ff input[disabled],.ff select[disabled],.ff textarea[disabled]{opacity:.5;cursor:not-allowed}
.ff-row{display:grid;grid-template-columns:1fr 1fr;gap:var(--sp-3)}
@media(max-width:640px){.ff-row{grid-template-columns:1fr}}
.form-submit{width:100%;min-height:52px;background:var(--gold-action);color:var(--ink);border:var(--rule-2);font-family:var(--font-body);font-weight:800;font-size:.9rem;text-transform:uppercase;letter-spacing:.04em;cursor:pointer;transition:background var(--dur)}
.form-submit:hover,.form-submit.s-hover,.form-submit:active,.form-submit.s-active{background:var(--gold-pressed)}
.form-submit[disabled],.form-submit.s-disabled{opacity:.45;cursor:not-allowed}
.form-submit.s-loading{opacity:.75;cursor:wait;pointer-events:none}
.form-note{font-size:var(--fs-label);text-align:center;margin-top:10px;letter-spacing:0;text-transform:none}
.form-dark .form-note{color:var(--tx-inv-2)}
.form-light .form-note{color:var(--tx-3)}
.form-tel{display:flex;justify-content:center;align-items:center;gap:5px;margin-top:var(--sp-3);font-size:var(--fs-small);min-height:44px}
.form-dark .form-tel{color:var(--tx-inv-2)}
.form-dark .form-tel strong{color:var(--white)}
.form-light .form-tel{color:var(--tx-2)}
.form-light .form-tel strong{color:var(--ink)}
/* ── R12 form-error components — carried verbatim from production ── */
.f-error{font-size:.78rem;font-weight:600;color:var(--error);margin-top:6px}
.form-errors{border:1px solid var(--error);border-left:4px solid var(--error);color:var(--error);background:var(--white);padding:12px 16px;font-size:.85rem;font-weight:600;margin-bottom:16px}
input[aria-invalid="true"],select[aria-invalid="true"],textarea[aria-invalid="true"]{border-color:var(--error)!important}
/* dark-form legibility: error text stays white; the red lives in the 4px rule */
.form-dark .f-error{color:var(--tx-inv);border-left:4px solid var(--error);padding-left:var(--sp-2)}

/* ══ 11 · TRUST BAR — FROZEN. Class-for-class reproduction of
   style.css:165-171; every computed value byte-identical (var(--gold-action)
   computes #ffcd00 = --gold; var(--ink) computes #090909). Do not edit. ══ */
.trust-bar{background:var(--gold-action);padding:20px 0;overflow:hidden;border-top:3px solid var(--ink);border-bottom:3px solid var(--ink)}
.trust-bar-in{max-width:1140px;margin:0 auto;display:flex;align-items:center;justify-content:center;gap:clamp(12px,2vw,24px);padding:0 24px;flex-wrap:wrap}
.trust-item{display:inline-flex;align-items:center;gap:9px;font-size:.76rem;font-weight:800;text-transform:uppercase;letter-spacing:.06em;color:var(--ink);white-space:nowrap}
.trust-item svg{width:15px;height:15px;flex-shrink:0}
.trust-item .gold{color:var(--ink);fill:var(--ink)}
.trust-sep{width:2px;height:16px;background:rgba(0,0,0,.28);flex-shrink:0}
@media(max-width:600px){.trust-sep{display:none}.trust-bar-in{gap:14px}.trust-item{font-size:.7rem}}
/* ── end frozen block ── */

/* ══ 12 · SERVICE LEDGER 01-17 — numbered rows replace the card grid
   (audit rank 8). Ordinals Barlow 900 at 2rem = 34px ≥ the 28px floor
   (rank 22). Descriptions UN-CLAMPED. 2px ink row rules. ══ */
.ledger{border:var(--rule-2);border-top:var(--rule-4);background:var(--white)}
.led{display:grid;grid-template-columns:64px 1fr;column-gap:var(--sp-4);padding:clamp(16px,2vw,24px);border-bottom:var(--rule-2);align-content:start}
.led:last-child{border-bottom:none}
@media(min-width:1100px){
  .ledger{display:grid;grid-template-columns:1fr 1fr}
  .led:nth-child(odd){border-right:var(--rule-2)}
  .led:nth-last-child(2):nth-child(odd){border-bottom:none}
}
.led-num{font-family:var(--font-display);font-weight:900;font-size:2rem;line-height:1;color:var(--ink);grid-row:1/4}
.led h3{font-family:var(--font-display);font-weight:700;text-transform:uppercase;font-size:var(--fs-plate);line-height:1.05}
.led-badge{display:inline-block;background:var(--ink);color:var(--tx-inv);font-size:var(--fs-label);font-weight:800;letter-spacing:.1em;text-transform:uppercase;padding:3px 8px;margin-bottom:6px;justify-self:start}
.led p{font-size:var(--fs-small);color:var(--tx-2);line-height:1.6;margin:6px 0 10px}
.led a{font-weight:800;font-size:.8rem;text-transform:uppercase;letter-spacing:.04em;color:var(--ink);text-decoration:underline;text-underline-offset:3px;display:inline-flex;align-items:center;min-height:44px}
.led a:hover,.led a.s-hover{color:var(--link-on-light)} /* no gold hover fill — A1 */

/* ══ 13 · CARDS — white/paper, 2px ink frame, 4px gold top. No shadow,
   no hover lift (law). ══ */
.card{background:var(--white);border:var(--rule-2);border-top:var(--rule-gold-4);padding:clamp(22px,3vw,34px)}
.card h2,.card h3{font-family:var(--font-display);font-weight:700;text-transform:uppercase;font-size:var(--fs-h3);line-height:1.05;margin-bottom:var(--sp-3)}
.card p{font-size:var(--fs-small);line-height:1.65;color:var(--tx-2)}

/* ── review card ── */
.rv-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:var(--sp-4)}
.rv-card{background:var(--white);border:var(--rule-2);border-top:var(--rule-gold-4);padding:22px 20px;display:flex;flex-direction:column}
.rv-stars{color:var(--ink);font-size:1.05rem;letter-spacing:2px}
.rv-quote{margin:10px 0 0;font-size:var(--fs-small);line-height:1.6;color:var(--tx-2)}
.rv-attr{margin-top:14px;font-size:var(--fs-small);color:var(--tx-3)}
.rv-attr strong{color:var(--ink)}
.rv-more{color:var(--ink);font-weight:800;text-decoration:underline;text-underline-offset:3px}
.rv-more:hover,.rv-more.s-hover{color:var(--link-on-light)}

/* ── stat plate ── */
.stat-grid{display:grid;grid-template-columns:repeat(4,1fr);border:var(--rule-2);border-top:var(--rule-4);background:var(--white)}
.stat-plate{padding:clamp(24px,4vw,48px) var(--sp-4);text-align:center;border-right:var(--rule-1)}
.stat-plate:last-child{border-right:none}
.stat-val{font-family:var(--font-display);font-weight:900;font-size:var(--fs-h2);line-height:1;color:var(--ink)}
.stat-label{font-size:var(--fs-small);color:var(--tx-2);margin-top:var(--sp-2);line-height:1.5}
.stat-label strong{display:block;font-family:var(--font-body);font-weight:800;font-size:var(--fs-label);letter-spacing:var(--track-label);text-transform:uppercase;color:var(--ink);margin-bottom:4px}
@media(max-width:768px){.stat-grid{grid-template-columns:1fr 1fr}.stat-plate{border-bottom:var(--rule-1)}.stat-plate:nth-child(n+3){border-bottom:none}}

/* ── guide card (resources) ── */
.guide-card{border:var(--rule-2);background:var(--white)}
.guide-head{background:var(--ink);color:var(--tx-inv);padding:10px 18px}
.guide-card ul{list-style:none;padding:var(--sp-4) var(--sp-5)}
.guide-card li{border-bottom:var(--rule-1)}
.guide-card li:last-child{border-bottom:none}
.guide-card li a{display:flex;align-items:center;min-height:44px;font-weight:700;color:var(--ink)}
.guide-card li a:hover,.guide-card li a.s-hover{color:var(--link-on-light);text-decoration:underline;text-underline-offset:3px}

/* ── area group ── */
.area-group{border:var(--rule-2);border-top:var(--rule-gold-4);background:var(--white)}
.area-group h3{font-family:var(--font-display);font-weight:700;text-transform:uppercase;font-size:var(--fs-plate);background:var(--paper);padding:10px 14px;border-bottom:var(--rule-1);display:flex;justify-content:space-between;align-items:center;gap:var(--sp-2)}
.area-count{font-family:var(--font-body);font-size:var(--fs-label);font-weight:800;letter-spacing:.04em;background:var(--ink);color:var(--tx-inv);padding:3px 8px;flex-shrink:0}
.area-group ul{list-style:none;padding:10px 14px}
.area-group li a{display:flex;align-items:center;min-height:40px;font-size:var(--fs-small);color:var(--tx-2)}
.area-group li a:hover,.area-group li a.s-hover{color:var(--ink);text-decoration:underline;text-underline-offset:3px}

/* ══ 14 · DATA TABLE ═════════════════════════════════════════════════════ */
.table-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch;max-width:100%}
.data-table{width:100%;border-collapse:collapse;border:var(--rule-2);background:var(--white);font-size:var(--fs-small)}
.data-table th{font-family:var(--font-display);font-weight:700;text-transform:uppercase;font-size:var(--fs-plate);letter-spacing:.02em;text-align:left;background:var(--ink);color:var(--tx-inv);padding:10px 16px;white-space:nowrap}
.data-table td{padding:12px 16px;border-bottom:var(--rule-1);color:var(--tx-2);line-height:1.6;vertical-align:top}
.data-table tr:last-child td{border-bottom:none}
.data-table td strong{color:var(--ink)}
.data-table a{color:var(--link-on-light);text-decoration:underline;text-underline-offset:2px}
.data-table a:hover{color:var(--ink)}

/* ══ 15 · CERT BADGE ROW — uniform containers; the art itself untouched
   (no grade, no grayscale on manufacturer marks) ══ */
.cert-row{display:flex;flex-wrap:wrap;gap:var(--sp-4);align-items:stretch}
.cert-cell{flex:1 1 160px;min-height:120px;display:grid;place-items:center;background:var(--white);border:var(--rule-1);padding:var(--sp-4)}
.cert-cell img{max-height:72px;width:auto;object-fit:contain}

/* ══ 16 · FAQ ACCORDION — closed answers leave the a11y tree via
   visibility (C4/R8); aria-controls stays wired in markup. ══ */
.faq-list{border:var(--rule-2);background:var(--white)}
.faq-item{border-bottom:var(--rule-1)}
.faq-item:last-child{border-bottom:none}
.faq-q{width:100%;min-height:56px;display:flex;align-items:center;justify-content:space-between;gap:var(--sp-4);text-align:left;background:var(--white);border:none;cursor:pointer;padding:16px 18px;font-family:var(--font-display);font-weight:700;text-transform:uppercase;font-size:var(--fs-plate);letter-spacing:.02em;color:var(--ink);transition:background var(--dur)}
.faq-q::after{content:'+';font-weight:900;font-size:1.4rem;flex-shrink:0}
.faq-q:hover,.faq-q.s-hover{background:var(--paper)}
.faq-item.open .faq-q{background:var(--ink);color:var(--white)}
.faq-item.open .faq-q::after{content:'−'}
.faq-a{max-height:0;overflow:hidden;visibility:hidden;transition:max-height var(--dur) var(--ease),visibility 0s linear var(--dur)}
.faq-item.open .faq-a{max-height:800px;visibility:visible;transition:max-height var(--dur) var(--ease),visibility 0s}
.faq-a-in{padding:16px 18px;font-size:var(--fs-small);line-height:1.65;color:var(--tx-2)}
/* native <details class="faq-item"> pages keep their exception (R8) */
details.faq-item .faq-a{max-height:none;overflow:visible;visibility:visible;transition:none}

/* ══ 17 · PHOTO CAPTION (C6) — visible spec strip on every city-band and
   project photo; never text floated over the image. ══ */
.photo-frame{border:var(--rule-2);background:var(--white)}
.photo-frame img{width:100%;height:auto}
.photo-caption{background:var(--ink);color:var(--tx-inv);font-family:var(--font-display);font-weight:700;text-transform:uppercase;font-size:var(--fs-plate);letter-spacing:.02em;padding:10px 16px;border-top:var(--rule-gold-4)}
.photo-caption small{display:block;font-family:var(--font-body);font-weight:400;font-size:var(--fs-small);letter-spacing:0;text-transform:none;color:var(--tx-inv-2);margin-top:2px}

/* ══ 17b · MAP FRAME (third-party-surfaces.md §3) — the contact-page Google
   Maps embed. Wrapper chrome only: system frame + the .photo-caption plate
   beneath. The embed URL, loading/referrerpolicy attrs and Google's tiles are
   untouched; no vendor internals are styled. ══ */
.map-frame{border:var(--rule-2);background:var(--white)}
.map-frame iframe{display:block;width:100%;border:0}

/* ══ 18 · LEAK STRIP ═════════════════════════════════════════════════════ */
.leak-strip{background:var(--ink);text-align:center;padding:13px 16px}
.leak-strip p{margin:0;font-size:var(--fs-body);line-height:1.5;color:var(--white)}
.leak-strip a{color:var(--gold-action);font-weight:800;text-decoration:underline;text-underline-offset:3px;white-space:nowrap} /* text link on dark — legal */
.leak-strip a:hover,.leak-strip a.s-hover{color:var(--gold-pressed)}

/* ══ 19 · CTA BAND — ink slab; gold lives ONLY in the act button ══ */
.cta{background:var(--ink);color:var(--tx-inv);text-align:center;padding:var(--sect-pad) 0}
.cta .label{color:var(--tx-inv-2);display:block;margin-bottom:var(--sp-3)}
.cta h2{font-family:var(--font-display);font-weight:900;text-transform:uppercase;font-size:var(--fs-h2);line-height:var(--lh-display);margin-bottom:var(--sp-4)}
.cta-body{max-width:560px;margin:0 auto var(--sp-6);color:var(--tx-inv-2)}
.cta-btns{display:flex;gap:var(--sp-4);justify-content:center;flex-wrap:wrap}
.cta-nap{margin-top:var(--sp-5);font-size:var(--fs-small);color:var(--tx-inv-2)}

/* ══ 20 · BREADCRUMB ═════════════════════════════════════════════════════ */
.breadcrumb{display:flex;flex-wrap:wrap;align-items:center;gap:6px;font-size:var(--fs-small);color:var(--tx-3)}
.breadcrumb a{color:var(--tx-3);min-height:24px;display:inline-flex;align-items:center}
.breadcrumb a:hover,.breadcrumb a.s-hover{color:var(--ink);text-decoration:underline;text-underline-offset:2px}
.breadcrumb [aria-current="page"]{color:var(--tx)}

/* ══ 21 · OWNER TIMELINE (identity keep-list) ════════════════════════════ */
.career-timeline{display:flex;flex-direction:column}
.timeline-item{display:grid;grid-template-columns:54px 1fr;gap:var(--sp-5);align-items:start;padding:0 0 var(--sp-5);position:relative}
.timeline-item:last-child{padding-bottom:0}
.timeline-item:not(:last-child)::before{content:'';position:absolute;left:26px;top:54px;bottom:0;width:2px;background:var(--ink)}
.timeline-age{width:54px;height:54px;display:grid;place-items:center;background:var(--ink);color:var(--tx-inv);font-family:var(--font-display);font-weight:900;font-size:1.4rem;line-height:1}
.timeline-item:last-child .timeline-age{background:var(--white);color:var(--ink);border:var(--rule-3)}
.timeline-role{font-family:var(--font-body);font-weight:800;font-size:var(--fs-body);color:var(--ink);text-transform:uppercase;letter-spacing:.02em;margin-bottom:3px}
.timeline-year{font-size:var(--fs-small);color:var(--tx-2)}

/* ══ 22 · FOOTER — 4 columns, all-20-cities block (PageRank distribution).
   Headings are WHITE condensed (A1: gold footer headings are illegal).
   Link hover → gold text = legal dark-surface text link. ══ */
.footer{background:var(--ink);color:var(--tx-inv);padding:var(--sp-8) 0 0}
.footer-grid{display:grid;gap:clamp(24px,2.5vw,44px);margin-bottom:var(--sp-7);grid-template-columns:1.6fr 1fr 2.2fr 1fr}
.footer-col-title{font-family:var(--font-display);font-weight:700;font-size:var(--fs-plate);letter-spacing:.06em;text-transform:uppercase;color:var(--tx-inv);margin-bottom:14px}
.footer ul{list-style:none;display:flex;flex-direction:column}
.footer ul a,.footer-contact a{display:flex;align-items:center;gap:10px;min-height:40px;font-size:var(--fs-small);color:var(--tx-inv-2);transition:color var(--dur)}
.footer ul a:hover,.footer ul a.s-hover,.footer-contact a:hover,.footer-contact a.s-hover{color:var(--gold-action)}
.footer-logo img{height:56px;width:auto}
.footer-desc{font-size:var(--fs-small);line-height:1.7;color:var(--tx-inv-2);margin:16px 0 20px}
.footer-contact{display:flex;flex-direction:column}
.footer-contact svg{width:14px;height:14px;stroke:var(--tx-inv-2);fill:none;stroke-width:2;flex-shrink:0}
.footer-viewall{display:inline-flex;align-items:center;min-height:40px;font-size:.8rem;font-weight:800;text-transform:uppercase;letter-spacing:.04em;color:var(--gold-action);margin-top:var(--sp-2)}
.footer-viewall:hover,.footer-viewall.s-hover{color:var(--white)}
.footer-areas-grid{display:grid;grid-template-columns:1fr 1fr;gap:0 18px}
/* THE RULE ABOVE NEVER WON. `.footer ul` (0-1-1, flex-column) outranks
   `.footer-areas-grid` (0-1-0), so the city grid declared here and in the
   two media blocks below was dead code on every page — all 20 cities in a
   ~940px single-column tower (found on home 2026-08-24; survey confirmed
   the identical structure on all 134 pages). One display-only rule at
   0-2-1 revives it; the column counts stay where they always were, in the
   0-1-0 rules, so the 4/3/2 cascade below works as originally written. */
.footer ul.footer-areas-grid{display:grid}
.footer-social{display:flex;gap:var(--sp-3);margin-top:var(--sp-4)}
.footer-social a{display:grid;place-items:center;width:44px;height:44px;border:1px solid var(--line-dark);min-height:44px}
.footer-social a:hover,.footer-social a.s-hover{border-color:var(--gold-action)}
.footer-social svg{width:18px;height:18px;fill:var(--tx-inv-2)}
.footer-bottom{border-top:1px solid var(--line-dark);padding:28px 0;display:flex;flex-direction:column;align-items:center;text-align:center;gap:var(--sp-3)}
.footer-bottom p{font-size:.74rem;color:var(--tx-inv-2);line-height:1.7}
.footer-bottom a{color:var(--tx-inv-2);text-decoration:underline;text-underline-offset:3px;min-height:44px;display:inline-flex;align-items:center;padding:0 8px}
.footer-bottom a:hover{color:var(--gold-action)}
/* DESKTOP (>=1101): four columns and a full-width city band. The base
   template above (1.6fr 1fr 2.2fr 1fr) is FOUR tracks for FIVE children —
   the guides column was added by the orphan-blogs audit fix without
   extending it, orphaning Company onto its own row beside ~900px of empty
   ink at 1440. Row one becomes brand | services | guides | company;
   .footer-areas takes row 2 full-width explicitly and the auto-placement
   cursor seats guides and company in row one's free tracks. 4 city
   columns = ~245px each at 1104, so the longest anchor ("Roofing
   contractor in Sacramento") holds one line. Proven on home 2026-08-24,
   promoted sitewide (134/134 pages share the structure). */
@media(min-width:1101px){
  .footer-grid{grid-template-columns:1.6fr 1fr 1fr 1fr}
  .footer-areas{grid-row:2;grid-column:1/-1}
  .footer-areas-grid{grid-template-columns:repeat(4,1fr)}
}
@media(max-width:1100px){.footer-grid{grid-template-columns:1fr 1fr}.footer-areas{grid-column:1/-1}.footer-areas-grid{grid-template-columns:repeat(3,1fr)}}
@media(max-width:600px){.footer-grid{grid-template-columns:1fr}.footer-areas-grid{grid-template-columns:1fr 1fr}}

/* ══ 23 · GALLERY CHROME — design/components.html only. Still law-bound:
   tokens only, classes only. Ships in the production sheet so the gallery
   is a true v2 surface with zero page-local CSS. ══ */
.gal-head{background:var(--ink);color:var(--tx-inv);padding:var(--sp-6) 0;border-bottom:var(--rule-gold-4)}
.gal-head h1{font-family:var(--font-display);font-weight:900;text-transform:uppercase;font-size:var(--fs-h2);line-height:var(--lh-display)}
.gal-head p{color:var(--tx-inv-2);margin-top:var(--sp-3);max-width:70ch}
.gal-section{padding:var(--sp-7) 0;border-bottom:var(--rule-1)}
.gal-state{font-family:var(--font-body);font-weight:800;font-size:var(--fs-label);letter-spacing:var(--track-label);text-transform:uppercase;color:var(--tx-3);margin:var(--sp-5) 0 var(--sp-2)}
.gal-box{border:var(--rule-1);padding:var(--sp-5);background:var(--white)}
.gal-box--ink{background:var(--ink)}
.gal-box--paper{background:var(--paper)}
.gal-box--gold{background:var(--gold-action)}
.gal-box--flush{padding:0;overflow:hidden}
.gal-rel{position:relative;min-height:72px;overflow:hidden}
.gal-grid{display:grid;gap:var(--sp-5);grid-template-columns:repeat(auto-fit,minmax(300px,1fr))}
.gal-note{font-size:var(--fs-small);color:var(--tx-2);margin-top:var(--sp-3);max-width:80ch;line-height:1.6}
.gal-form{max-width:460px}
.na{color:var(--tx-3)}

/* ══ 24 · ARTICLE — long-form reading surface (blog template canonical).
   Added 2026-08-15, Phase 3 blog migration — the one authorized shared-
   component addition for the 25 blog surfaces. Reading measure, paragraph
   rhythm, article headers, inline links, blockquote. Archivo body at token
   sizes; section headers are ink plates (display face). CONTRACT: prose
   (p/h2/h3/ul/ol/blockquote) sits as DIRECT children of .article; interleaved
   components (.card, .form-dark, .cta, .faq-list, .table-wrap, .photo-frame)
   keep their own type — the child combinators below never reach into them. ══ */
.article{max-width:72ch;margin:0 auto}
.article-lead{font-size:var(--fs-body-lg);line-height:1.7;color:var(--tx-2);margin:var(--sp-5) 0 var(--sp-3)}
.article-meta{font-size:var(--fs-small);color:var(--tx-3);margin:0 0 var(--sp-6)}
.article-meta strong{color:var(--ink)}
.article>p{margin:0 0 var(--sp-4);line-height:1.7;color:var(--tx-2)}
.article>p strong{color:var(--ink)}
.article>h2{display:inline-block;background:var(--ink);color:var(--tx-inv);border-left:var(--rule-gold-4);padding:10px 22px 10px 18px;font-family:var(--font-display);font-weight:900;text-transform:uppercase;font-size:var(--fs-h3);line-height:var(--lh-display);letter-spacing:.01em;margin:var(--sp-6) 0 var(--sp-4)}
.article>h3{font-family:var(--font-display);font-weight:700;text-transform:uppercase;font-size:var(--fs-plate);line-height:1.05;color:var(--ink);margin:var(--sp-5) 0 var(--sp-3)}
.article>ul,.article>ol{margin:0 0 var(--sp-4);padding-left:var(--sp-5)}
.article>ul li,.article>ol li{line-height:1.7;color:var(--tx-2);margin-bottom:var(--sp-2)}
.article>ul li strong,.article>ol li strong{color:var(--ink)}
.article>p a,.article>ul li a,.article>ol li a{color:var(--link-on-light);text-decoration:underline;text-underline-offset:2px}
.article>p a:hover,.article>p a.s-hover,.article>ul li a:hover,.article>ul li a.s-hover,.article>ol li a:hover,.article>ol li a.s-hover{color:var(--ink)}
.article>blockquote{border-left:var(--rule-gold-4);background:var(--paper);padding:var(--sp-4) var(--sp-5);margin:0 0 var(--sp-4)}
.article>blockquote p{line-height:1.7;color:var(--tx-2)}
.article .card,.article .form-dark,.article .cta,.article .faq-list,.article .table-wrap,.article .photo-frame{margin:var(--sp-6) 0}

/* ══ HERO — the composed first screen (2026-08-17) ═══════════════════════
   BEFORE: the hero photo sat alone in a .wrap; the placard sat in the NEXT
   <section>, a full --sect-pad below it. Two stacked, unrelated blocks. The
   city templates even carried the comment "placard over a real hero img" for
   a composition that was never built. .hero binds figure + placard into one
   ruled unit — the placard reads as signage bolted to the building.

   TYPE RANK: .plate h1 resolved to --fs-h2 — the same rank as every section
   <h2> on the page, so the single biggest statement had no rank above its own
   subheadings. --fs-hero (declared in the token sheet, spent nowhere) now
   carries the h1 on desktop. Mobile steps back to --fs-h2: --fs-hero's 3rem
   floor on a 375px screen pushes the CTA too far down. No new tokens.

   ORDER: placard FIRST, photo second. Photo-first was built and measured at
   1440x1000 — the figure alone ran 733px and pushed the h1 to 1271px, i.e.
   below the fold. A composed first screen whose headline you have to scroll to
   is not composed. Statement, then evidence. Captions are stripped from the
   parity comparison, so the visible-text sequence is unchanged either way.
   (An earlier version of this comment also claimed the reorder hands LCP to
   the h1 text. Measured 2026-08-17: false — the LCP element is the hero <img>
   before and after. See design/perf-2026-08-17.md.) */
.hero{border:var(--rule-2);border-top:var(--rule-4);background:var(--white)}
.hero .plate{display:block;border-left:none;border-bottom:var(--rule-4);
  padding:clamp(18px,2.4vw,28px) clamp(18px,2.4vw,32px)}
.hero .photo-frame{border:none}                 /* the frame is now .hero's */
/* The library is mixed-orientation and modest-resolution (several heroes are
   portrait, e.g. 570x760). A fixed aspect-ratio crop would decapitate those,
   so cap the height instead and let cover do the cropping. */
.hero .photo-frame img{display:block;width:100%;
  max-height:min(56vh,560px);object-fit:cover}
@media(max-width:768px){.hero .plate{margin:0}} /* .plate's full-bleed pull
  is wrong inside .hero — the frame already spans the wrap */

/* DESKTOP HERO — split, 2026-08-17 (second pass, after seeing it rendered).
   The first pass stacked a full-width placard on a full-width photo and set the
   h1 to --fs-hero. Measured on the live site at 1728x1080: the placard alone was
   469px, the photo 560px, the hero 1098px — TALLER THAN THE VIEWPORT — and the
   first CTA landed at y=1390, i.e. 310px below the fold. A desktop visitor saw a
   black slab and a cropped photo and nothing else: no phone, no estimate button,
   no trust bar, no copy. That is a conversion defect wearing a design defect's
   clothes, and it is the thing that made the page look like a billboard.

   --fs-hero is NOT used here any more. It is a display-WORD scale
   (clamp(3rem,8vw,6rem) = 102px at this width) and this h1 is a 70-character
   SENTENCE; at 102px it wrapped to four lines of shouting caps. The h1 now takes
   --fs-h2 like other display headings — its dominance comes from position and
   the ink slab, not from point size. Below 1000px nothing changes: the stacked
   layout and mobile sizing are untouched. */
@media(min-width:1000px){
  .hero{display:grid;grid-template-columns:1fr 1fr;align-items:stretch;
    min-height:clamp(400px,44vh,500px)}
  .hero .plate{display:flex;flex-direction:column;justify-content:center;
    border-bottom:none;border-right:var(--rule-4);
    padding:clamp(24px,2.6vw,36px) clamp(24px,2.6vw,36px)}
  .hero .photo-frame{display:flex;flex-direction:column;overflow:hidden;min-width:0}
  .hero .photo-frame img{flex:1 1 auto;height:100%;max-height:none;min-height:0}
}

/* SIGNATURE LEDGER ROW — the namesake. Gold RULE, not gold fill: --gold-action
   is reserved for CTAs/submit/hazard, and a gold-filled badge would read as a
   button. --rule-gold-4 is already sanctioned as a "card signature". */
.led--signature{border-left:var(--rule-gold-4);background:var(--paper)}

/* ══ SITE-WIDE REPAIR PASS — 2026-08-19 ═══════════════════════════════════
   Operator: "the website still looks terrible." A design review measured the
   page and found the causes were not the hero. Every item below was verified
   against the live DOM at 1440x900 before being written. */

/* R1 · CRITICAL — the "Meet the owner" card was a BROKEN inline element.
   <a class="card"> with .card never setting display, so it computed to
   display:inline while containing a display:grid 54x54 chip. Block-in-inline
   fragmentation split the 2px border, dropped the chip onto the timeline rule
   and clipped the link text to "...CT THE OWNER →". Measured: the ONLY inline
   element on the page with block-level children (544x279 at y=3735). */
a.card{display:block}

/* R2 · CRITICAL — .card had NO internal vertical rhythm. Measured inside the
   intro card: five children, every gap 0px, because the sheet set padding,
   border and type but never child spacing. The box carrying both CTAs read as
   an unstyled HTML dump while 224px of white sat idle above it. The watermark
   ordinal is absolutely positioned and must not take a margin. */
.card > * + *{margin-top:var(--sp-4)}
.card > .led-num[aria-hidden="true"] + *{margin-top:0}
.card .cert-row + .cert-row{margin-top:var(--sp-3)}

/* R3 · The h1's <em> was a SYNTHETIC OBLIQUE. The font budget ships Barlow
   Condensed 700/900 upright only — no italic exists — so the browser sheared
   the glyphs, in the first words anyone reads. 700 is a real shipped weight,
   so the clause still separates from the 900 lead-in, honestly this time. */
.plate h1 em,.plate h2 em{font-style:normal;font-weight:700}

/* R4 · --sect-pad measured 112px, applied top AND bottom, so all 19 section
   boundaries were a 224px void — ~23% of an 18,600px page spent on nothing.
   72px keeps a generous signage rhythm at half the cost. */
:root{--sect-pad:clamp(48px,5vw,72px)}

/* R5 · Every grid ended in an empty cell: ledger 17 rows / 2 cols; .rv-grid at
   2, 4, 5 and 8 items across 3 tracks. Four separate "ran out of content"
   moments. A trailing item at 3n+2 spans the gap; a 4-item grid becomes 2x2. */
.ledger > .led:last-child:nth-child(odd){grid-column:1 / -1}
/* The span maths below is ONLY valid while .rv-grid is three tracks wide.
   .rv-grid is repeat(auto-fit,minmax(280px,1fr)), so it is ONE track on a
   phone — an unscoped `span 2` there invents a second track and doubles the
   grid, which measured as 92px of horizontal overflow at 375px and 147px at
   320px. Three tracks need ~968px of viewport, hence the 1000px gate. */
@media(min-width:1000px){
  .rv-grid > :last-child:nth-child(3n+2){grid-column:span 2}
}

/* R6 · Ledger rows carrying a .led-badge pushed their link into the ORDINAL
   column — measured "Shake Roofing" link at x=198 against x=274 for every
   other left-column row. .led is a 2-col grid and the badge makes a fourth
   child, which auto-placed back into column 1. Pin the columns explicitly. */
.led-num{grid-column:1}
.led > *:not(.led-num){grid-column:2}

/* R7 · Body copy ran ~139 characters per line at 1440. Cap the measure; the
   card keeps its full width, the prose stops being a wall.
   54ch, NOT 74: the ch unit is the "0" glyph — in Archivo 1ch = 8.53px
   while the average rendered character is 6.14px (ratio 1.39), so the
   original 74ch admitted ~100 measured characters (max 97 counted on
   services/roof-repair, 2026-08-24). 54ch renders ~72-75, which is what
   this rule always claimed to do. Font-size-independent: ch scales with
   the type, so the character count holds at fs-small and fs-body-lg
   alike. text-wrap:pretty rides along: the narrower measure roughly
   doubled 1-2-word last lines (10 vs 4 across four sampled pages) and
   pretty claws most back in Chromium. Consequence, accepted
   deliberately: the intro-card leads that grew their type against this
   cap lose 224px of line width (828->604px, fill 80%->58% at 1440) — big type on a
   100-character line was never a fix for measure, and the INTRO CARDS
   note below is amended to match. */
.card p,.card li{max-width:54ch;text-wrap:pretty}

/* R8 · The cert cells were the only 1px hairline boxes in a system built on
   2px/4px ink rules — they read as four ghost boxes. */
.cert-cell{border:var(--rule-2)}

/* R5b · A 4-item grid across 3 tracks left the last card alone beside two
   empty cells (measured: last row 357px of 1104). 2x2 fills it. */
@media(min-width:1000px){
  .rv-grid:has(> :nth-child(4):last-child){grid-template-columns:repeat(2,1fr)}
}

/* R6b · The signature row's own 4px gold rule was consuming layout, pushing
   its content 4px right of its column-mates (link x=278 vs 274 — measured).
   Give the border back to the padding so the ledger column stays true. */
.led--signature{padding-left:calc(clamp(16px,2vw,24px) - 4px)}

/* ══ HOME · SECTION 1 — the intro card ════════════════════════════════════
   Operator 2026-08-19: "this section 1 looks terrible on the homepage."
   Measured defects: the right ~40% of a 1104px card sat empty once the prose
   was capped at 74ch; four solid ink chips sat directly beneath the two CTAs
   and competed with them; and the two fine-print paragraphs carried the same
   weight as the lead. Targeted POSITIONALLY (the section immediately after the
   hero wrap) so no HTML changes and parity is untouched. Desktop only —
   below 1000px the stack is unchanged. */
@media(min-width:1000px){
  main > .wrap + .section > .wrap > .card{
    display:grid;
    grid-template-columns:1fr minmax(240px,300px);
    column-gap:var(--sp-8);
    align-items:start;
  }
  /* left column: the argument and the actions */
  main > .wrap + .section > .wrap > .card > .hero-body{grid-column:1;grid-row:1;margin-top:0}
  main > .wrap + .section > .wrap > .card > .cert-row:nth-of-type(1){grid-column:1;grid-row:2}
  /* the two notes become a FOOTER ROW across the whole card, side by side,
     instead of trailing under the buttons. Operator 2026-08-19 disliked the
     placement: the section was ending on grey small print rather than on the
     ask. Moving them ABOVE the CTAs was measured and rejected — it pushed the
     buttons from y691 to ~780, below the fold on a 1366x768 laptop. */
  main > .wrap + .section > .wrap > .card > p:nth-of-type(2){grid-column:1;grid-row:3}
  main > .wrap + .section > .wrap > .card > p:nth-of-type(3){grid-column:2;grid-row:3}

  /* right column: the credentials, as a spec list rather than four ink blocks.
     Same facts, a third of the visual weight, and it fills the dead column. */
  main > .wrap + .section > .wrap > .card > .cert-row:nth-of-type(2){
    grid-column:2;grid-row:1 / span 2;align-self:stretch;
    display:flex;flex-direction:column;align-items:stretch;gap:0;
    margin-top:0;border-left:var(--rule-2);padding-left:var(--sp-5);
  }
  main > .wrap + .section > .wrap > .card > .cert-row:nth-of-type(2) .led-badge{
    background:none;color:var(--ink);
    font-family:var(--font-display);font-weight:700;
    font-size:var(--fs-display-floor);letter-spacing:.02em;line-height:1.2;
    padding:10px 0;margin:0;border-bottom:var(--rule-1);
  }
  main > .wrap + .section > .wrap > .card > .cert-row:nth-of-type(2) .led-badge:last-child{
    border-bottom:none;
  }
  /* the fine print stops shouting at lead-paragraph weight */
  main > .wrap + .section > .wrap > .card > p:nth-of-type(2),
  main > .wrap + .section > .wrap > .card > p:nth-of-type(3){
    border-top:var(--rule-1);padding-top:var(--sp-4);margin-top:var(--sp-6);
    color:var(--tx-3);font-size:var(--fs-small);max-width:none;
  }
}

/* ══ HOME · SECTION 4 — manufacturer certification badges ═════════════════
   Measured 2026-08-19: the section ran 283px tall to show four 72px logos.
   Each sat in a 264x120 cell, so a 72px image floated in a 264px box — the
   trust signals read as postage stamps rather than credentials. It also sat
   72px below the (frozen) gold trust bar, so two credential bands floated
   apart instead of grouping.
   Badge ART is off-limits, so nothing here touches the logos themselves —
   only how large they render and how the band is spaced. */
.section:has(.cert-cell){padding-top:var(--sp-6)}   /* group with the trust bar */
.cert-cell img{max-height:96px}                     /* 72 -> 96, +33% */
.cert-cell{min-height:132px}

/* ══ HOME · SECTION 5 — the quote form ════════════════════════════════════
   Measured 2026-08-19: 1,159px tall. Below the name row every field was a
   full-width input in a ~1030px panel — a phone number, a ZIP code and a
   service dropdown each occupying the full width. Pairing them halves the
   rows without touching a single field name, id, the web3forms endpoint or
   any hidden UTM input. Hidden inputs are display:none in the UA stylesheet
   so they take no grid cell; .botcheck is out of flow. */
@media(min-width:641px){
  .form-dark form{display:grid;grid-template-columns:1fr 1fr;
    column-gap:var(--sp-4);row-gap:var(--sp-4);align-items:start}
  .form-dark form > .ff-row{grid-column:1 / -1}
  .form-dark form > .ff:nth-last-of-type(1){grid-column:1 / -1}  /* referral */
  .form-dark form > .form-submit,
  .form-dark form > .form-note,
  .form-dark form > .form-tel{grid-column:1 / -1}
}

/* Required-field markers (operator 2026-08-19: "make everything required").
   Driven off the control's own :required state via :has(), so a label can
   never disagree with its field — change the attribute and the marker follows.
   Gold is legal here: the token law sanctions gold text on --surface-form
   (10.3:1), and a required marker is an attention mark, not decoration. */
.form-dark .ff:has(:required) label::after{content:"*";color:var(--gold-action);margin-left:.25em;font-weight:800}

/* ══ HOME · SECTION 6 — the stats band, cleaned up ════════════════════════
   Kept as designed: four plates across, centred, numeral over key over
   description, ink frame with the 4px top rule. Four measured defects fixed.

   SCOPE — .stat-grid--figures, not .stat-grid. The class is shared with 8 city
   pages, but only this band holds FIGURES ("Free", "5", "~15", "20"). The city
   bands hold PHRASES ("C-39 licensed", "~40 mi", "2003–08"), and the same rules
   applied to them were measured wrapping every phrase onto two 102px lines and
   pushing the band from 269px to 375px. Two components wearing one class; the
   modifier separates them without touching the other eight pages.

   1 · RAGGED DESCRIPTIONS. "MANUFACTURER CERTIFICATIONS" wraps to two lines,
       so its description started 19px below the other three (key heights
       18/37/18/18 — four cells, three baselines). Reserving two lines for
       every key and hanging it from the bottom puts all four descriptions on
       one line whatever the wrap. Measured after: one baseline at every width
       ≥769px, one per row below it. The cost is a numeral-to-key gap that
       varies by 18px; that space sits in the open area under the numeral,
       where it reads as air rather than as misalignment.
   2 · THE FIGURES WERE UNDERSET. --fs-h2 is the section-heading rank, so the
       numbers — the entire point of the band — carried no more weight than the
       h2 above them. --fs-hero is declared in the token sheet and, since the
       hero h1 was stepped back to --fs-h2 on 2026-08-17, spent NOWHERE. It is
       a display-WORD scale, which is exactly what a stat figure is (the reason
       it was wrong for the 70-character hero SENTENCE). No new token.
   3 · HAIRLINE DIVIDERS. 1px --line inside a 2px/4px ink frame read as a
       different system. The certification cells were corrected the same way.
   4 · AWKWARD WRAPS. 16px of horizontal padding ran the two longest
       descriptions to the divider and orphaned their last word ("Ripon, CA",
       "Bay"). Widened to 24px, with balanced wrapping — but only at the
       4-column breakpoint: at 320px the extra 16px per cell pushed the
       min-content of the unbreakable word "CERTIFICATIONS" past its 1fr track
       and blew 14px of horizontal overflow onto the page. Measured across 18
       viewports, 320→1728: zero overflow, zero figure overrun. */
.stat-grid--figures .stat-val{font-size:var(--fs-hero);line-height:1;
  font-variant-numeric:tabular-nums;letter-spacing:-.01em}
.stat-grid--figures .stat-label{margin-top:var(--sp-3);text-wrap:balance}
.stat-grid--figures .stat-label strong{min-height:3em;display:flex;
  align-items:flex-end;justify-content:center;margin-bottom:var(--sp-1)}
.stat-grid--figures .stat-plate{border-right:var(--rule-2)}
@media(max-width:768px){.stat-grid--figures .stat-plate{border-bottom:var(--rule-2)}}
/* At 320-360 the widest key needs three lines, not two; without this the
   two-line reserve is overrun and the descriptions go ragged again. */
@media(max-width:360px){.stat-grid--figures .stat-label strong{min-height:4.5em}}
@media(min-width:769px){
  .section:has(.stat-grid--figures){padding-top:var(--sp-7)}
  .stat-grid--figures .stat-plate{padding:var(--sp-6) var(--sp-5)}
}

/* ══ HOME · SECTION 7 — meet the owner ════════════════════════════════════
   SCOPE — .rv-grid:has(> .article). Scanned every page: .rv-grid is on 36 of
   them (8 instances on this page alone), and exactly ONE instance site-wide
   has an .article as a direct child. It is this one. Nothing here may touch
   the bare class.

   THE SECTION HAD NO OBJECT. Measured all 12 .plate headers on this page:
   eleven sit flush at 0px against what follows, because the flush mount IS the
   language — a placard bolted to a thing. Everywhere else that thing is
   bordered (the dark form, a card, a row of cards). This was the only .rv-grid
   on the page whose children are not bordered cards, so the plate was bolted
   to naked body text and the prose and the timeline floated side by side with
   nothing binding them. It is now one ruled plate — 4px ink top rule, story
   left, career right, 2px divider — the same construction as the stats band.

   1 · THE SPLIT WAS INVERTED, BY A RULE OF THIS SHEET'S OWN MAKING. R5's
       `.rv-grid > :last-child:nth-child(3n+2){grid-column:span 2}` was written
       for review-card rows that ran out of content. This grid has two children
       and is not a card row: child 2 is :last-child AND nth-child(3n+2), so it
       took a second track. Measured at 1440: prose 357px, timeline+card 731px
       — the sparse column had twice the room of the dense one, and the prose
       ran ~48 characters per line, under the 60-75 band. The tell: at 999px
       the layout is already two equal columns, because R5 is gated at 1000.
       The fix restores at >=1000 what the page does one pixel lower.
   2 · THE CARD WAS FLUSH AGAINST THE TIMELINE. Measured gap: 0px. Its gold
       rule landed under "2025. His own license." and read as a fifth row.
   3 · THE CARD WAS A STACK IN AN EMPTY BOX — 731x297 for a monogram and four
       short lines, propping up one column. It is now a full-width signature
       block across the foot of the plate, which also levels the columns: the
       prose measures 308px of content against the timeline's 288px.
   4 · THE BLOCK IS INK, so the section is bookended — headline plate at the
       top, the owner at the bottom, evidence in white between. The monogram
       inverts to white-on-ink, the same move .timeline-item:last-child already
       makes on the "23" node, so it reads as an avatar and not a fifth row.
       No gold was added: the token law lists its legal carriers and closes
       with "Nothing else."

   The two-column-plus-footer layout is possible without touching the HTML
   because `display:contents` removes the untouched wrapper <div>'s box while
   keeping its children, promoting the timeline and the card to grid items that
   can be placed independently. Safe here — the wrapper is a plain <div> with
   no implicit ARIA role. */
@media(max-width:999px){
  .rv-grid:has(> .article) .career-timeline + .card{margin-top:var(--sp-6)}
}
.rv-grid:has(> .article) .card{display:grid;grid-template-columns:auto 1fr;
  column-gap:var(--sp-5);align-items:start;background:var(--ink)}
/* `grid-row:1 / -1` does NOT span implicit rows — -1 resolves against the
   EXPLICIT grid, which declares none, so it collapses to one row and forced
   row 1 to the monogram's 54px, opening a measured 45px hole between the name
   and the role. Both spans below are written as explicit counts. */
.rv-grid:has(> .article) .card > .timeline-age{grid-column:1;grid-row:1 / span 2;
  background:var(--white);color:var(--ink)}
.rv-grid:has(> .article) .card > p{grid-column:2;color:var(--tx-inv-2)}
.rv-grid:has(> .article) .card > p.label{color:var(--tx-inv)}
.rv-grid:has(> .article) .card > p:first-of-type{margin-top:0}
.rv-grid:has(> .article) .card .led-badge{background:var(--white);color:var(--ink)}

@media(min-width:1000px){
  .rv-grid:has(> .article){grid-template-columns:1fr 1fr;gap:0;
    border:var(--rule-2);border-top:var(--rule-4);background:var(--white)}
  .rv-grid:has(> .article) > .article{grid-column:1;grid-row:1;margin:0;max-width:none;
    border-right:var(--rule-2);padding:var(--sp-6)}
  .rv-grid:has(> .article) > .article + *{display:contents}
  .rv-grid:has(> .article) .career-timeline{grid-column:2;grid-row:1;padding:var(--sp-6)}
  /* Identity stack in column 2 — name, role, then the credential directly
     under them, because the licence belongs to the person, not to the strip.
     The CTA holds the right edge on its own. The monogram spans all three
     rows; they measure ~86px against its 54px, so it inflates none of them. */
  .rv-grid:has(> .article) .card{grid-column:1 / -1;grid-row:2;margin-top:0;
    border:none;padding:var(--sp-6);
    grid-template-columns:auto 1fr auto;column-gap:var(--sp-6);align-items:center}
  .rv-grid:has(> .article) .card > .timeline-age{grid-row:1 / span 3;align-self:center}
  .rv-grid:has(> .article) .card > p{margin-top:0}
  .rv-grid:has(> .article) .card > p:nth-of-type(1){grid-column:2;grid-row:1}
  .rv-grid:has(> .article) .card > p:nth-of-type(2){grid-column:2;grid-row:2}
  .rv-grid:has(> .article) .card > p:nth-of-type(3){grid-column:2;grid-row:3;margin-top:var(--sp-2)}
  .rv-grid:has(> .article) .card > p:nth-of-type(4){grid-column:3;grid-row:1 / span 3}
  .rv-grid:has(> .article) .card .led-badge{margin-bottom:0}
}

/* ══ HOME · SECTION 8 — the services ledger ═══════════════════════════════
   The healthiest section on the page — already a ruled object with 2px row
   rules and a 4px ink cap. Three measured defects, no restructuring.

   1 · A 6px RULE THAT THE SYSTEM DOES NOT DEFINE. The intro .card sits flush
       against the .ledger (measured gap: 0), so the card's 2px bottom and the
       ledger's 4px top stacked into 6px of solid ink — heavier than --rule-4,
       the heaviest rule the token sheet declares. Dropping the card's bottom
       edge where a ledger follows leaves one 4px rule and merges the two boxes
       into a single object with a header cell. This pattern — card directly
       above ledger — occurs on 24 pages, and the fix is correct on all of them.

   2 · BADGED ROWS BROKE THE HEADING LINE. "NuShake specialty" sat above the
       h3, so on rows 05/06 and 15/16 the two headings started 28px apart
       (measured). The badge now shares the heading's grid row, flagged right,
       which aligns every heading pair AND puts the three flags on one x — 16's
       and 17's land at the same 1130px because a right-column cell and the
       full-width spanning cell share a right edge.
       SCOPE: `.led > .led-badge` is the SIBLING markup variant — 3 cells, all
       on this page. The other variant nests the badge INSIDE .led-num (40
       cells across 20 city pages) and is untouched by this selector. Verified
       on index + two city pages at 6 widths: zero collisions, zero movement on
       the nested variant.

   3 · EVERY ROW SAID ITS OWN NAME TWICE. 15 of 17 rows end in a link whose
       text is identical to the h3 above it. The words have to stay — that link
       is the internal anchor text pointing at each service page — so the fix is
       to stop it LOOKING like a repeated heading. An arrow marks it as a
       control. Written as `"→" / ""` so the pseudo-element carries empty alt
       text and assistive tech reads the anchor, not the glyph.

   Explicit row placement below replaces auto-placement. .led-num already spans
   grid-row 1/4, so rows 1-3 were always the intended structure; naming them
   makes the badge's placement possible and the rest deterministic. Cells with
   no <a> (26 site-wide) are unaffected — an unused row simply never fills. */
.card:has(+ .ledger){border-bottom:none}
.led > .led-badge{grid-row:1;justify-self:end;align-self:center;margin-bottom:0}

/* R6b · 2026-09-09 — R6 flags the badge onto the HEADING's grid row, and both
   land in the same cell (column 2, row 1), so they are stacked, not laid out.
   That is safe only while the heading text is short enough never to reach the
   badge. At 390 it was not: the home page printed "NUSHAKE SPECIALTY" on top of
   "SHAKE ROOFING" (measured 149px of overlap at 360/390/768, screenshot in the
   2026-09-09 pass). R6's justification — aligning the headings of side-by-side
   row PAIRS — only exists at >=1100, where `.ledger` becomes two columns. Below
   that the ledger is a single column, there is no pair to align, so the badge
   takes its own row above the heading and nothing overlaps. Scoped with :has()
   to badged cells only. Measured all 2440 ledger cells against production at
   5 widths: 2165 of 2185 unbadged cells are byte-identical; the other 20 are
   the 1100px row-mates of a badged cell, whose BOX matches the taller
   neighbour by 3-4px while their h3/p/link stay on the same pixel. */
@media(max-width:1099.98px){
  .led:has(> .led-badge) > .led-badge{grid-row:1;justify-self:start;align-self:start;margin-bottom:6px}
  .led:has(> .led-badge) > .led-num{grid-row:1/5}
  .led:has(> .led-badge) > h3{grid-row:2}
  .led:has(> .led-badge) > p{grid-row:3}
  .led:has(> .led-badge) > a{grid-row:4}
}
.led > h3{grid-row:1}
.led > p{grid-row:2}
.led > a{grid-row:3}
.led a::after{content:"→" / "";margin-left:.5em;text-decoration:none;display:inline-block}

/* ══ INTRO CARDS — the blank half ═════════════════════════════════════════
   Operator 2026-08-20: "theres a lot of blank space where the text is under
   the heading." Measured at 1440: every `.wrap > .card` is 1104px wide over a
   1032px text column, and R7's 74ch cap resolves to 630.19px at --fs-small.
   That is a HARD ceiling of 61.1% column fill — 222 cards on 37 pages sit
   against it, median 59.8%, ~410px of dead line width each.

   AMENDED 2026-08-24: R7 has since been corrected to 54ch (the ch-trap —
   74ch admitted ~100 measured characters, so the type-growth below was
   big type on an over-long line). The leads keep --fs-body-lg; their
   fill drops ~200px against the honest measure, which is the same
   left-pinned-column outcome the rest of the site converged on. The
   .data-table walls this note used to cite are fixed separately below.
   These paragraphs are section leads under a full-width ink plate, and this
   sheet already sizes a lead: .article-lead uses --fs-body-lg. Same token.
   Measured on this page: 600px of ink -> 788px, 54% -> 71% fill.

   Measured and rejected — `columns:2` on the same paragraphs (114 of 291
   cards carry 1-2 line paragraphs, so it set them as single-line columns, and
   un-capping to feed the columns pushed CPL to 151, worse than the wall R7
   exists to prevent); and dropping the card's frame to make a standfirst
   deck, which reads well alone but amputates the card+ledger weld directly
   below and converts only the plate-adjacent cards, leaving two treatments
   for one job on the same page.

   GATED AT 1100px, NOT 1000. --fs-body-lg is a flat 19.55px while --fs-plate
   is clamp(1.125rem,1.8vw,1.5rem), which floors at 19.125px. Between 1000 and
   1086 the body copy would outrank every plate-level heading in the sheet.
   1.8vw passes 19.55px at 1086px; 1100 is already this sheet's ledger
   breakpoint, so the gate reuses it. Below it the card is <=1010px and the
   cap is slack anyway. */
@media(min-width:1100px){
  .wrap > .card > p:not(.label){font-size:var(--fs-body-lg)}
  /* A paragraph that is nothing but a .rv-more link is a control, not prose.
     Left to inherit, it rendered 31% larger than the label on the gold CTA
     directly above it — a tertiary text link out-ranking the primary button. */
  .wrap > .card > p > .rv-more{font-size:var(--fs-small)}
  /* HOME · SECTION 1 opts out: it already fills its box with a second column
     (measured 100% coverage). Enlarging its lead grew the card 410px -> 489px
     and opened a 78px hole under the credential rail. */
  main > .wrap + .section > .wrap > .card > p:not(.label){font-size:var(--fs-small)}
}
/* ══ CC · SERVICE AREA — COVERAGE CHIPS ══════════════════════════════════
   Operator, twice: "it looks so bad ... make it more visually appealing/
   professional." Measured why (index.html, 1440): five identical white
   boxes in a 3-track grid, 20 links stacked one per row at 40px each, and
   grid stretch forcing every box to its row-mate's height — the 2-city
   "Delta & Contra Costa" box measured the same 320px as the 4-city boxes,
   ~180px of it empty. 661px of grid to carry 20 links.

   The move: the region's <ul> stops being a column and becomes a WRAPPING
   ROW OF CHIPS, so a region reads as a cluster of coverage instead of a
   directory page. That only pays off if the chip field is WIDE — measured
   in a 357px track, a 29-character anchor ("Roofing contractor in Modesto"
   = 259px boxed) is 73% of the track, so chips still landed one per row
   and bought nothing. So the grid also flips: one region per FULL-WIDTH
   band, region header as an ink rail on the left, 850px of chip field on
   the right. Result at 1440: grid 661px -> 465px (-30%), section 1323px ->
   1127px (-15%), same 20 links, none of them touched.

   NOT a second ledger. The .ledger above is a 2px-framed 2-column table of
   17 numbered text rows — one object, orderly, prose inside. This is five
   detached signs with solid ink rails and an irregular mosaic of bordered
   plates. Different silhouette (gapped panels vs one frame), different
   texture (mosaic vs ruled rows), different left edge (a 230px ink block
   vs a 64px numeral). The one thing they share — a stack — is unavoidable
   at this width. Judged acceptable; the risk is real but the two blocks do
   not read alike at a glance.
   MARKUP UNTOUCHED. Anchor text untouched (the 6x/6x/7x/1 anchor variation
   is deliberate SEO). CSS only. ══════════════════════════════════════════ */

/* One region per row. The `.wrap >` prefix is load-bearing, not decoration:
   R5b (`.rv-grid:has(> :nth-child(4):last-child)`) is (0,3,0) and forces a
   4-item grid to 2 columns — 14 of the 22 pages carrying .area-group have
   exactly 4 groups, and without the prefix R5b outranked this rule and the
   bands rendered 2-up at 546px (measured on antioch: chip field 292px, one
   chip per row again). (0,3,0) + later in source wins the tie. */
.wrap > .rv-grid:has(> .area-group){grid-template-columns:1fr;gap:var(--sp-3);align-items:start}
/* R5's trailing 3n+2 span assumed 3 tracks; at 1 track it is a no-op that
   would still fight this. Cancel it inside the areas grid only. */
.rv-grid:has(> .area-group) > :last-child:nth-child(3n+2){grid-column:auto}

/* THE BAND. Ink rail | chip field. 230px is the widest rail that still
   leaves 850px of field at 1440 — enough for 4 average chips per line
   (measured: Modesto's four anchors total 838px including gaps). The 140px
   floor is what lets the rail survive to the 620px stacking point. Tried
   275px so "DELTA & CONTRA COSTA" (217px of type) would sit on one line:
   it cost 52px at 768 on the homepage and 156px on contact, because the
   narrower field wraps chips. 230px and a two-line name is the better
   trade — the 1.05 leading below makes the second line nearly free. */
.area-group{display:grid;grid-template-columns:minmax(140px,230px) 1fr;border-top:var(--rule-2)}
/* One gold rule opens the block instead of five. The 4px gold card-top is a
   legal carrier, but repeated down five stacked bands it stops being a
   signature and becomes a stripe pattern. */
.rv-grid > .area-group:first-child{border-top:var(--rule-gold-4)}

/* The header was a --paper strip with the count in a 12px ink chip — the
   only numeric interest in the section (2/3/4/4/7) set smaller than the
   body text. Rail goes solid ink; the count becomes a Barlow 900 numeral at
   var(--fs-h3) = 32px at 1440 / 24px at 375. --fs-h3 is the ceiling: the
   next token up (--fs-h2, 65px at 1440) is taller than a one-row band. */
.area-group h3{background:var(--ink);color:var(--tx-inv);border-bottom:none;padding:var(--sp-2) var(--sp-4);align-items:center;line-height:1.05}
/* line-height above is the density fix, not a detail: h3 inherited the 1.55
   body leading, so a 2-line region name ("Delta & Contra Costa") built a
   103px rail over a 64px chip row — 39px of dead ink per band, on 3 of the
   5 bands. At 1.05 the same 2-line name measures 70px and the bands that
   hold one chip row all land at 74px. */
.area-count{font-family:var(--font-display);font-weight:900;font-size:var(--fs-h3);line-height:.85;background:none;color:var(--tx-inv);padding:0;letter-spacing:0}

/* THE CHIP FIELD. align-content:center keeps a short field optically
   centred against a rail that wrapped to two lines. */
.area-group ul{display:flex;flex-wrap:wrap;align-content:center;gap:var(--sp-2);padding:var(--sp-3)}
/* roofing-companies-tracy-ca ships an .area-group whose <ul> holds nothing
   but whitespace ("Solano County"). Pre-existing content gap; do not pay
   24px of padding and a hairline for it. `:empty` does NOT match here — a
   whitespace text node disqualifies it — which is why the first version of
   this rule was dead code and the band still rendered as an ink rail beside
   a void. No link is hidden: there are none to hide. */
.area-group ul:not(:has(li)){display:none}

/* Chips are three states, because the markup is three states: 292 <li><a>,
   44 plain-text <li> (cities with no page), 3 <li><strong> (the page's own
   city). Linked = 2px ink frame on white. Unlinked = hairline on --paper at
   --tx-3 (6.3:1, AA) — present, clearly not clickable. Current city = ink
   fill. flex:1 1 auto justifies every line flush to both edges, which is
   what stops a 2-city region leaving 480px of the dead white the operator
   rejected; justify-content:center then makes a stretched plate read as a
   deliberate sign rather than a half-empty box. gap:--sp-1 restores the
   word space between the two flex items in "Ripon <em>(HQ)</em>" — flex
   collapses the whitespace text node between them. */
.area-group li{flex:1 1 auto;display:flex;align-items:center;justify-content:center;gap:var(--sp-1);min-height:40px;border:var(--rule-1);background:var(--paper);color:var(--tx-3);font-size:var(--fs-small);padding:0 var(--sp-3)}
.area-group li:has(> a){padding:0;border:var(--rule-2);background:var(--white)}
/* The <a> carries the padding and fills the chip, so the whole plate is the
   hit area: every one of the 20 links measures exactly 40px tall and
   55-680px wide across 320/375/768/1024/1440/1728 — the 40px floor is the
   binding dimension and it is never crossed. */
.area-group li > a{flex:1 1 auto;display:flex;align-items:center;justify-content:center;min-height:40px;padding:0 var(--sp-3);font-size:var(--fs-small);color:var(--tx)}
.area-group li:has(> strong){background:var(--ink);border-color:var(--ink);color:var(--tx-inv)}
/* Hover inverts the plate, matching .btn-secondary. The old rule was an
   underline, which is invisible inside a bordered chip. */
.area-group li > a:hover,.area-group li > a.s-hover{background:var(--ink);color:var(--tx-inv);text-decoration:none}
/* index.html's Ripon <li> carries a trailing .led-badge "HQ". Stop the <a>
   growing there so link + badge centre as one pair, and drop the badge's
   6px bottom margin, which was written for a stacked ledger cell. */
.area-group li:has(> .led-badge) > a{flex:0 1 auto}
.area-group li > .led-badge{margin:0;align-self:center}

/* Below 620px the rail cannot hold a chip beside it: at 620 the field is
   351px, one 29-char chip (259px) wide. Header returns to a full-width ink
   strip, name left / numeral right. Padding tightens because at 320 the
   default 12px ul padding + 12px chip padding pushed the longest anchors to
   a second line — 8 chips wrapped, +164px of grid. At --sp-2 they fit:
   grid 1175px vs the current sheet's 1279px at 320. */
@media(max-width:620px){
  .area-group{grid-template-columns:1fr}
  .area-group ul{gap:var(--sp-1);padding:var(--sp-2)}
  .area-group li,.area-group li > a{padding:0 var(--sp-2)}
}

/* ══ 25 · HOME · SECTION 10 — THE REVIEW SCOREBOARD ═══════════════════════
   The old section was three unrelated bands: a half-empty intro card, a
   credential aside that repeated it, and six review cards. Measured at 1440:
     · intro card 544x509 with content stopping 257px above the bottom edge
       — 50.5% dead white
     · "What homeowners say" a 1104x156 card holding one 366px line — 33% fill
     · six signatures at six different heights, voids of 49/24/48/72/71/24px
     · quote measure 39-47 characters per line, under the 60-75 band
     · the five certifications listed TWICE in adjacent paragraphs, and
       4.5 x2, 3.9 x2, Yelp x2, "473 Facebook" x2, 107 x4 across the two cards
     · the 4.5 rating — the most persuasive fact here — set as body text
   This rebuilds it as a scoreboard: every statistic stated ONCE, at figure
   scale, on one ink board that also carries both CTAs.

   SCOPE — everything hangs off .sec-score, a class that exists on exactly one
   section on the whole site. .rv-card / .rv-stars / .rv-quote / .rv-attr are
   shared with 25 other pages (reviews/index.html + 20 city pages + 4 design
   mockups); none of the rules below can reach them. The review grid is
   .review-grid, NOT .rv-grid, because R5 and R5b key off .rv-grid and would
   otherwise fight this layout for the cascade. ══════════════════════════ */

.sec-score .board{background:var(--ink);border-top:var(--rule-gold-4)}
.sec-score .board-head{padding:var(--sp-5);border-bottom:1px solid var(--line-dark)}
.sec-score .board-head .label{display:block;color:var(--tx-inv-2);margin-bottom:var(--sp-2)}
.sec-score .board-head h2{font-family:var(--font-display);font-weight:900;
  text-transform:uppercase;font-size:var(--fs-h2);line-height:var(--lh-display);
  letter-spacing:.01em;color:var(--tx-inv);text-wrap:balance}

/* THE FIGURE ROW. 4.5 is the anchor — display scale, tabular, WHITE on ink.
   Deliberately NOT gold: a rating is not an action, and gold at --fs-hero
   would blow the 5% budget on its own. */
.sec-score .board-figs{display:grid;grid-template-columns:1fr 1fr}
.sec-score .fig-cell{padding:var(--sp-5);border-top:1px solid var(--line-dark);
  display:flex;flex-direction:column;justify-content:flex-end}
.sec-score .fig-cell + .fig-cell{border-left:1px solid var(--line-dark)}
.sec-score .fig-main{grid-column:1 / -1;border-top:none;
  flex-direction:row;flex-wrap:wrap;align-items:flex-end;
  justify-content:flex-start;gap:var(--sp-4) var(--sp-6)}
.sec-score .fig-line{display:flex;align-items:baseline;gap:var(--sp-2)}
.sec-score .fig-big{font-family:var(--font-display);font-weight:900;
  font-size:var(--fs-hero);line-height:.82;color:var(--tx-inv);
  font-variant-numeric:tabular-nums;letter-spacing:-.01em}
.sec-score .fig-den{font-family:var(--font-display);font-weight:700;
  font-size:var(--fs-h3);line-height:1;color:var(--tx-inv-3);text-transform:uppercase}

/* The 4.5 meter is a HARD CLIP, not a ramp: one 90%-wide window over the same
   five glyphs, so half a star is drawn by overflow:hidden rather than by a
   gradient (L3) or a fake half-star glyph. Both spans are aria-hidden and the
   container carries role="img" + the real label, so AT reads "Rated 4.5 out of
   5 stars" once instead of ten stars. */
.sec-score .meter{position:relative;display:inline-block;font-size:1.5rem;
  line-height:1;letter-spacing:0;white-space:nowrap}
.sec-score .meter-track{color:var(--tx-inv-3)}
.sec-score .meter-fill{position:absolute;left:0;top:0;width:90%;
  overflow:hidden;color:var(--tx-inv)}
.sec-score .fig-cap{font-family:var(--font-body);font-weight:800;
  font-size:var(--fs-small);letter-spacing:.08em;text-transform:uppercase;
  color:var(--tx-inv-2);margin-top:var(--sp-3)}
.sec-score .fig-cap strong{color:var(--tx-inv)}
.sec-score .fig-val{font-family:var(--font-display);font-weight:900;
  font-size:var(--fs-h2);line-height:1;color:var(--tx-inv);
  font-variant-numeric:tabular-nums;letter-spacing:-.01em}
.sec-score .fig-key{font-family:var(--font-body);font-weight:800;
  font-size:var(--fs-label);letter-spacing:var(--track-label);
  text-transform:uppercase;color:var(--tx-inv-2);margin-top:var(--sp-2)}

.sec-score .board-act{display:flex;flex-wrap:wrap;align-items:center;
  justify-content:space-between;gap:var(--sp-4);padding:var(--sp-5);
  border-top:1px solid var(--line-dark)}
.sec-score .board-lede{font-size:var(--fs-body-lg);line-height:var(--lh-body);
  color:var(--tx-inv-2);max-width:52ch;flex:1 1 300px;min-width:0}
.sec-score .board-lede strong{color:var(--tx-inv)}
.sec-score .board-btns{display:flex;flex-wrap:wrap;gap:var(--sp-3);flex:0 1 auto;max-width:100%}

/* SUB-HEAD ROW — replaces the 1104x156 heading-in-a-card. A heading and its
   one line of prose on a rule, which is what it always was. */
.sec-score .subhead{display:grid;grid-template-columns:minmax(200px,auto) 1fr;
  gap:var(--sp-3) var(--sp-6);align-items:baseline;
  border-bottom:var(--rule-2);padding-bottom:var(--sp-3);margin-top:var(--sp-7)}
.sec-score .subhead h3{font-family:var(--font-display);font-weight:700;
  text-transform:uppercase;font-size:var(--fs-h3);line-height:1.05;color:var(--ink)}
.sec-score .subhead p{font-size:var(--fs-small);line-height:1.6;color:var(--tx-2)}
/* Block padding on an INLINE box grows the hit area without touching the line
   box, so these sentences read unchanged while the link clears 44px. */
.sec-score .subhead .rv-more,
.sec-score .cred-cell .rv-more{padding-block:var(--sp-4)}

/* CREDENTIAL STRIP — the two duplicate certification paragraphs merged to one. */
.sec-score .cred{display:grid;grid-template-columns:minmax(260px,1fr) 2fr;
  border:var(--rule-2);background:var(--white);margin-top:var(--sp-5)}
.sec-score .cred-cell{padding:var(--sp-4) var(--sp-5)}
.sec-score .cred-cell + .cred-cell{border-left:var(--rule-2)}
.sec-score .cred-cell .label{display:block;color:var(--ink);margin-bottom:var(--sp-2)}
.sec-score .cred-cell p{font-size:var(--fs-small);line-height:1.65;color:var(--tx-2)}
.sec-score .cred-cell p + p{margin-top:var(--sp-2)}

/* THE REVIEWS — two tracks, not three, so the quotes get a real measure
   (39-47 -> 58-73 characters at 1440). margin-top:auto on the attribution
   pins all six signatures to the bottom rule, which is what kills the ragged
   49/24/48/72/71/24px voids: the card is already flex-column, so auto eats
   the leftover height instead of leaving it under the signature. */
.sec-score .review-grid{display:grid;grid-template-columns:1fr;
  gap:var(--sp-4);margin-top:var(--sp-5)}
.sec-score .review-grid .rv-card{padding:var(--sp-5)}
.sec-score .review-grid .rv-stars{font-size:1.05rem}
/* 52ch, not 60: the ch trap again — 60ch = 511px = up to 80 rendered
   characters (audit 2026-08-24, verified). 52ch renders 65-72 here. */
.sec-score .review-grid .rv-quote{margin:var(--sp-3) 0 var(--sp-5);
  font-size:var(--fs-small);line-height:1.7;color:var(--tx-2);max-width:52ch}
/* THE SEPARATOR GAP, AND WHY IT IS SCOPED HERE RATHER THAN ON .rv-attr.
   .rv-attr has TWO markup shapes on this site:
     A  <strong>Name</strong><span>date · job</span>          — 13 instances,
        index.html + reviews/index.html + 1 on Ripon. Renders "John B.Aug 2025"
        with a measured 0px gap.
     B  <strong>Name</strong> · <span>date · job</span>       — 60 instances,
        the 20 city pages. Renders correctly as inline text.
   A global `.rv-attr{display:flex}` fixes shape A and BREAKS shape B: flex
   promotes that literal "·" text node to its own flex item, which then gets
   stranded at the end of a line ("Rickie H. ·" / newline / "May 2024 · ...").
   Measured 2026-08-22 across all 20 city pages x 4 widths: 86 of 240 separator
   instances stranded, 19 of 20 pages affected. So the gap is scoped to this
   section, where the markup is known to be shape A and nothing else can see
   it. The 7 remaining shape-A attributions off this page (6 on
   reviews/index. The 6 on reviews/index.html were normalized to
   shape B on 2026-08-24 (this batch); Ripon's carry separators; the 6 on
   index.html stay shape A by design — the scoped flex gap below is their
   fix. */
.sec-score .review-grid .rv-attr{display:flex;flex-wrap:wrap;align-items:baseline;
  gap:0 var(--sp-2);margin-top:auto;padding-top:var(--sp-3);
  border-top:var(--rule-1);font-size:var(--fs-small);color:var(--tx-3)}

/* RESPONSIVE. Board: stacked -> 4.5 beside a stacked Yelp/Facebook column ->
   one row of three readings. Reviews stay on ONE track until two tracks can
   still hold a real measure (1060px = ~400px of column); between 769 and
   1059 the card goes side-by-side instead, so a wide card is never a wide
   quote. */
@media(min-width:769px) and (max-width:1059px){
  .sec-score .board-figs{grid-template-columns:1fr 1fr}
  .sec-score .fig-main{grid-column:1;grid-row:1 / span 2}
  .sec-score .fig-cell{border-top:none}
  .sec-score .fig-cell + .fig-cell{border-left:1px solid var(--line-dark)}
  .sec-score .fig-cell:last-child{border-top:1px solid var(--line-dark)}
  .sec-score .review-grid .rv-card{display:grid;
    grid-template-columns:minmax(160px,220px) 1fr;column-gap:var(--sp-6)}
  .sec-score .review-grid .rv-stars{grid-column:1;grid-row:1}
  .sec-score .review-grid .rv-quote{grid-column:2;grid-row:1 / span 2;margin:0}
  .sec-score .review-grid .rv-attr{grid-column:1;grid-row:2;
    margin-top:var(--sp-3);flex-direction:column;align-items:flex-start;gap:var(--sp-1) 0}
}
@media(min-width:1060px){
  .sec-score .review-grid{grid-template-columns:1fr 1fr}
}
@media(min-width:1100px){
  /* left stays sp-5: the sp-6 bump put the board's text edge at 32px
     against the plate-text norm of 22px (18px pad + 4px gold rule) — the
     h2 sat +10px off the page's 0-2px alignment norm at every width
     >=1100 (audit 2026-08-24, verified). Top/right/bottom keep sp-6. */
  .sec-score .board-head,
  .sec-score .fig-cell,
  .sec-score .board-act{padding:var(--sp-6) var(--sp-6) var(--sp-6) var(--sp-5)}
  .sec-score .board-figs{grid-template-columns:2fr 1fr 1fr}
  .sec-score .fig-main{grid-column:1;grid-row:auto;gap:var(--sp-5)}
  .sec-score .fig-cell{border-top:none}
  .sec-score .fig-cell + .fig-cell{border-left:1px solid var(--line-dark)}
  .sec-score .fig-cell:last-child{border-top:none}
  .sec-score .board-act{flex-wrap:nowrap}
}
@media(max-width:768px){
  .sec-score .cred{grid-template-columns:1fr}
  .sec-score .cred-cell + .cred-cell{border-left:none;border-top:var(--rule-2)}
  .sec-score .subhead{grid-template-columns:1fr;gap:var(--sp-2)}
}
@media(max-width:560px){
  .sec-score .board-btns{width:100%}
  .sec-score .board-btns .btn{flex:1 1 100%}
  .sec-score .meter{font-size:1.3rem}
}

/* ══ 26 · HOME · SECTION 11 — THE RECENT-WORK CONTACT SHEET ══════════════
   Measured before (1440): 1,412px of dead white across the eight frames —
   Steep-Slope Shingle alone was a 357x1036 box holding a 353x139 image (87%
   empty); Roof Replacement and Asphalt Shingle 274px each. Native aspect
   ratios spanned 0.75-2.54 (3.4x) while grid rows stretched to the tallest
   cell, and R5 spanned the trailing tile so a 390x520 file rendered at
   727x969 — a 1.87x upscale of the smallest image in the pool. Eight
   captions ended in an arrow with no <a>, no handler and no cursor:pointer
   behind any of them.
   After: 0px dead space at 320/360/390/640/768/1024/1440, max image scale
   0.48x (nothing upscaled), nine frames instead of eight, every arrow a real
   link, section height 2370 -> 1084px at 1440.                          */
/* ═══ OPTION W · THE CONTACT SHEET ═══════════════════════════════════════
   One strict module, gridded by 2px ink hairlines inside a single 2px ink
   frame, so section 11 reads as ONE rectangular plate of work instead of
   eight loose cards.

   SCOPE. Everything hangs off .sec-sheet, a class that exists on exactly one
   section. The grid is .sheet, NOT .rv-grid — R5 (`.rv-grid > :last-child:
   nth-child(3n+2){grid-column:span 2}`) and R5b (`:has(> :nth-child(4)
   :last-child)`) key off .rv-grid and would otherwise blow the last tile up
   to 2 columns, which is exactly defect G2. Frames are .cell, NOT
   .photo-frame — `.photo-frame img{width:100%;height:auto}` is what let
   every photo keep its own ratio (G3, the root cause of the 1,412px of
   white). Nothing here can reach either shared class.

   THE MODULE. Every landscape frame is aspect-ratio 16/9 with
   object-fit:cover, so all rows in a row-band are identical height by
   construction — there is no "tallest cell" to stretch to and therefore no
   dead space to leave behind (G1/G3 both die at the root). Portraits are the
   one exception the ratio cannot absorb: a 0.75 file cropped to 16/9 loses
   58% of its height. They get a DELIBERATE double-height cell instead —
   exactly two modules, never a fraction — which lands them at 0.72-0.77
   native, a 3-4% crop. The three tall cells are also the rhythm: without
   them a nine-up uniform grid reads as a spec table, not a body of work.

   CAPTION = BAR, NOT OVERLAY. The governing rule is C6
   (design/tokens/component-rules.md §7): every project photo carries a
   VISIBLE caption stating what it actually shows. C6 does not legislate bar
   vs overlay — the bar is chosen because it matches the .photo-caption
   anatomy already on 39 pages (ink strip under the photo, 4px gold top rule,
   Barlow 700 uppercase), and because an overlay's contrast is unprovable: it
   sits on whatever pixels the photo happens to have there. Rebuilt as a
   fixed-height single-line strip; fixed height keeps the module uniform.
   NOTE — C6 asks for subject + real location + year ("Tear-off crew —
   Sacramento, 2025"). These carry SUBJECT ONLY: the location and date of
   each job are recorded nowhere in this repo, and inventing them is exactly
   the locality-honesty failure C6 exists to retire. Owner to supply.

   NO FRAME INDEX. A numbered index was built and measured, then cut: the
   caption bars sit at the BOTTOM of each cell, so a double-height frame's
   number lands one band lower than its neighbours' and the sheet read
   01/-- , 02 03 04 , 08 05 09 07 left-to-right instead of 01..09 — an index
   that does not index. Moving the strip to the top of the cell would fix the
   sequence but inverts the sitewide .photo-caption anatomy (strip under
   the photo, gold rule on top), so the index went instead of the law. The
   three double-height cells carry the rhythm on their own.

   GOLD. Gold appears as the 4px caption rule (a legal carrier: it marks the
   link edge, and here every frame IS a link), as the focus halo, and as the
   hover fill — gold ground carrying --ink text, never gold text on white.
   Measured budget at 1440: 9 rules x ~274x4px = 9.9k px of 1.30M = 0.76%.

   LINKS. All nine frames link. See the report for the defence of option (c).
   ════════════════════════════════════════════════════════════════════════ */

/* section 10 house style: the display head balances instead of dangling one
   short word on line 2. */
.sec-sheet .plate h2{text-wrap:balance}
.sec-sheet .sheet-plate{border:var(--rule-2);background:var(--ink);margin-top:var(--sp-5)}

/* HEADER STRIP — replaces the 1104x139 intro card at 70% fill (G5). It is
   inside the sheet's frame, so the lede is the sheet's own header row rather
   than a second floating box above it. */
.sec-sheet .sheet-head{display:flex;flex-wrap:wrap;align-items:baseline;
  justify-content:space-between;gap:var(--sp-2) var(--sp-5);
  background:var(--white);padding:var(--sp-3) var(--sp-4);border-bottom:var(--rule-2)}
.sec-sheet .sheet-head p{font-size:var(--fs-small);line-height:1.6;
  color:var(--tx-2);max-width:70ch;flex:1 1 320px;min-width:0}
.sec-sheet .sheet-count{font-family:var(--font-body);font-weight:800;
  font-size:var(--fs-label);letter-spacing:var(--track-label);
  text-transform:uppercase;color:var(--tx-3);white-space:nowrap;
  flex:0 0 auto;margin-left:auto}   /* stays right-set when it wraps below the lede */

/* THE SHEET. gap:2px over an ink ground draws the hairlines; the cells are
   the only white. */
.sec-sheet .sheet{display:grid;gap:2px;background:var(--ink);
  grid-template-columns:repeat(2,1fr)}

.sec-sheet .cell{display:flex;flex-direction:column;min-width:0;
  background:var(--white);color:inherit;text-decoration:none;
  transition:background var(--dur) var(--ease)}
/* THE BOX IS THE MODULE, AND THE IMAGE IS TAKEN OUT OF FLOW INSIDE IT.
   Two measured reasons, both of which broke the first build:
   1. the HTML width/height attributes are presentational hints, so an <img>
      carrying height="506" resolves height:506px and IGNORES aspect-ratio —
      the first render came out with 274x506 "16/9" frames and 550px rows.
   2. even with height:auto, a double-height cell contributes its portrait
      max-content height to BOTH rows it spans, which re-inflates every
      landscape row by ~5px and puts the ragged white back under the caption.
   position:absolute removes the image from intrinsic sizing entirely, so the
   row height is decided by ONE thing: the landscape box's aspect-ratio. */
.sec-sheet .shot-box{position:relative;display:block;width:100%;
  flex:0 0 auto;aspect-ratio:16/9;overflow:hidden;background:var(--ink)}
.sec-sheet .shot{position:absolute;inset:0;width:100%;height:100%;
  object-fit:cover;object-position:center;
  transition:filter var(--dur) var(--ease)}
/* the double-height frame: the box eats whatever the two modules leave after
   the fixed caption bar. flex-basis 0 + min-height 0 = zero contribution. */
.sec-sheet .cell--tall .shot-box{aspect-ratio:auto;flex:1 1 0;min-height:0}

.sec-sheet .cap{display:flex;align-items:center;gap:var(--sp-2);
  flex:0 0 auto;min-height:44px;padding:9px var(--sp-3);
  background:var(--ink);color:var(--tx-inv);border-top:var(--rule-gold-4);
  transition:background var(--dur) var(--ease),color var(--dur) var(--ease)}
/* THE LABEL MUST NEVER WRAP. A two-line bar makes its whole grid row ~20px
   taller than the rows either side of it — the ragged-row disease this option
   exists to kill, reintroduced through the caption. Measured: at 360px the
   longest label ("Ridge & vents", 109px) still has 10px of headroom; below
   ~330px it runs out, so the padding tightens there and ellipsis is the last
   guard. Labels are kept to <=13 characters for the same reason. */
.sec-sheet .cap-t{flex:1 1 auto;min-width:0;font-family:var(--font-display);
  font-weight:700;text-transform:uppercase;font-size:var(--fs-display-floor);
  line-height:1.05;letter-spacing:.02em;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.sec-sheet .cap-a{flex:0 0 auto;font-family:var(--font-body);font-weight:800;
  font-size:var(--fs-small);line-height:1;color:var(--tx-inv-2)}

/* HOVER / FOCUS — the caption bar flips to gold ground with ink text (12.1:1,
   the only legal gold-background pairing) and the house photo grade lifts, so
   the frame both brightens and declares itself. No lift, no blur, no motion. */
.sec-sheet .cell:hover,.sec-sheet .cell.s-hover{background:var(--white)}
.sec-sheet .cell:hover .cap,.sec-sheet .cell.s-hover .cap,
.sec-sheet .cell:focus-visible .cap{background:var(--gold-action);color:var(--ink)}
.sec-sheet .cell:hover .cap-a,.sec-sheet .cell.s-hover .cap-a,
.sec-sheet .cell:focus-visible .cap-a{color:var(--ink)}
.sec-sheet .cell:hover .shot,.sec-sheet .cell.s-hover .shot,
.sec-sheet .cell:focus-visible .shot{filter:none}
.sec-sheet .cell:focus-visible{outline:var(--focus-ring);outline-offset:0;
  box-shadow:var(--focus-halo);position:relative;z-index:2}

/* ── PLACEMENT ───────────────────────────────────────────────────────────
   Nine frames = 12 modules (3 doubles). 12 packs perfectly into 2, 3 and 4
   tracks, and at every one of them the explicit placement below keeps
   VISUAL reading order identical to DOM order, so tab order never jumps.
   Every cell is placed by hand; nothing is left to auto-flow, which is what
   guarantees the block stays a true rectangle with no trailing hole. */

/* 2 tracks — phone. rows: [1|2] [1|3] [4|5] [6|5] [7|8] [7|9] */
.sec-sheet .cell:nth-child(1){grid-column:1;grid-row:1 / span 2}
.sec-sheet .cell:nth-child(2){grid-column:2;grid-row:1}
.sec-sheet .cell:nth-child(3){grid-column:2;grid-row:2}
.sec-sheet .cell:nth-child(4){grid-column:1;grid-row:3}
.sec-sheet .cell:nth-child(5){grid-column:2;grid-row:3 / span 2}
.sec-sheet .cell:nth-child(6){grid-column:1;grid-row:4}
.sec-sheet .cell:nth-child(7){grid-column:1;grid-row:5 / span 2}
.sec-sheet .cell:nth-child(8){grid-column:2;grid-row:5}
.sec-sheet .cell:nth-child(9){grid-column:2;grid-row:6}
/* narrow tiles: the bar was 31% of a 143px cell. 40px still clears the
   touch floor by a mile — the whole 143px cell is the link, not the bar. */
@media(max-width:639px){.sec-sheet .cap{min-height:40px;padding:8px var(--sp-2)}}
@media(max-width:359px){.sec-sheet .cap{padding:8px var(--sp-1);gap:var(--sp-1)}}

/* 3 tracks — tablet. rows: [1|2|3] [1|4|5] [6|7|5] [8|7|9] */
@media(min-width:640px){
  .sec-sheet .sheet{grid-template-columns:repeat(3,1fr)}
  .sec-sheet .cell:nth-child(1){grid-column:1;grid-row:1 / span 2}
  .sec-sheet .cell:nth-child(2){grid-column:2;grid-row:1}
  .sec-sheet .cell:nth-child(3){grid-column:3;grid-row:1}
  .sec-sheet .cell:nth-child(4){grid-column:2;grid-row:2}
  .sec-sheet .cell:nth-child(5){grid-column:3;grid-row:2 / span 2}
  .sec-sheet .cell:nth-child(6){grid-column:1;grid-row:3}
  .sec-sheet .cell:nth-child(7){grid-column:2;grid-row:3 / span 2}
  .sec-sheet .cell:nth-child(8){grid-column:1;grid-row:4}
  .sec-sheet .cell:nth-child(9){grid-column:3;grid-row:4}
}

/* 4 tracks — desktop. rows: [1|2|3|4] [1|5|6|7] [8|5|9|7] */
@media(min-width:1000px){
  .sec-sheet .sheet{grid-template-columns:repeat(4,1fr)}
  .sec-sheet .cell:nth-child(1){grid-column:1;grid-row:1 / span 2}
  .sec-sheet .cell:nth-child(2){grid-column:2;grid-row:1}
  .sec-sheet .cell:nth-child(3){grid-column:3;grid-row:1}
  .sec-sheet .cell:nth-child(4){grid-column:4;grid-row:1}
  .sec-sheet .cell:nth-child(5){grid-column:2;grid-row:2 / span 2}
  .sec-sheet .cell:nth-child(6){grid-column:3;grid-row:2}
  .sec-sheet .cell:nth-child(7){grid-column:4;grid-row:2 / span 2}
  .sec-sheet .cell:nth-child(8){grid-column:1;grid-row:3}
  .sec-sheet .cell:nth-child(9){grid-column:3;grid-row:3}
}

/* FOOT — the ask sits on the sheet's own bottom rule rather than floating
   centred under it, so the block still ends on one edge. */
.sec-sheet .sheet-foot{display:flex;flex-wrap:wrap;align-items:center;
  justify-content:space-between;gap:var(--sp-4);
  background:var(--white);padding:var(--sp-3) var(--sp-4);border-top:var(--rule-2)}
.sec-sheet .sheet-foot p{font-size:var(--fs-small);line-height:1.6;
  color:var(--tx-2);max-width:60ch;flex:1 1 260px;min-width:0}
.sec-sheet .sheet-foot .btn{flex:0 0 auto}

/* ══ 27 · HOME · SECTION 12 — THE INSTANT-QUOTE CONSOLE ══════════════════
   Measured before, on the LIVE site (the Roofle embed is domain-locked and
   collapses to a 43px stub locally, so these come from nushake.com):
     · the explanatory card ran 30-38 characters per line at 1440 and 31-33
       at 1104 — R5 fired on the 2-child .rv-grid, handing the widget span-2
       at 731px and leaving the copy in a 357px ribbon
     · 133px of dead white under that copy at 1440, 218px at 768
     · the 3-step .spec list is right-aligned, so step 2 wrapped and orphaned
       the word "satellite" alone on a 51px right-aligned line
     · at 768 the grid stayed 2-up and the WIDGET collapsed to 345px
   WIDGET HEIGHT IS VIEWPORT-KEYED, NOT CONTAINER-KEYED. Measured live:
     1440 -> iframe 727x622   1104 -> 668x622   768 -> 341x608
      390 -> iframe 354x900    320 -> 284x900
   Between 768 and 390 the iframe gets WIDER (341->354) and TALLER (608->900)
   at once, and between 1440 and 1104 the width moves 59px while the height
   does not move at all. So giving the widget full width buys 0px of height —
   it buys WIDTH (764px at 768, up from 345px) and nothing else. The height
   this section gains at desktop is the honest price of not stacking copy
   beside the tool; it is repaid on phones, where this is the only layout
   that comes out shorter than the one it replaces.
   After: 938 -> 1181px at 1440, 877 -> 1228 at 768, and 1681 -> 1629 at 390
   (-52px) and 1810 -> 1715 at 320 (-95px). 0px dead space. 0 orphaned lines.
   SCOPE: .sec-quote, one section site-wide. No .rv-grid, no .card, no .spec —
   R5/R5b and the shared .spec right-align cannot reach any of it. Nothing
   here targets the widget, its iframe, or anything inside it.            */
/* ══ OPTION P · THE ADDRESS BAR ══════════════════════════════════════════
   THESIS. The widget IS the section. It gets the full 1104px content width
   and every other element becomes a horizontal band serving it, stacked into
   ONE framed console: heading plate -> lede -> 3-step band -> tool nameplate
   -> the tool -> fine-print rule. Nothing sits beside the widget, so nothing
   can be squeezed into a 357px ribbon (Q1) and no grid row can stretch past
   its content (Q2).

   SCOPE / R5 ESCAPE (Q5). Everything hangs off .sec-quote, and the two grids
   are .q-steps and .q-fine — NOT .rv-grid. R5
   (`.rv-grid > :last-child:nth-child(3n+2){grid-column:span 2}`) and R5b
   (`.rv-grid:has(> :nth-child(4):last-child)`) key off .rv-grid and cannot
   reach either one. Same escape sections 10 and 11 used.

   THE VENDOR SURFACE. No rule below selects the iframe or anything inside
   it. The only widget-side declarations are on #roofle-widget (the .guide-
   card wrapper) and .rq-head — both named as restylable chrome. The
   <script>, the widget id, the <noscript> and its tel: link are byte-
   identical to source and unmoved relative to each other.

   FRAMING (the stated risk: a full-width vendor iframe reading as a hole).
   Three devices, in order of load-bearing:
   1. The console is one 2px ink frame with the house 4px gold top rule, so
      the tool is INSIDE a card, not floating on the page.
   2. The tool sits in a WHITE well, and the well is bracketed top and bottom
      by --paper bands (steps, fine print) and an ink nameplate. Whatever the
      vendor paints, it is surrounded by 24px of white on a paper ground
      inside an ink frame — three edges before the page.
   3. The nameplate strip lands directly on the well's top edge, so the
      widget reads as the labelled instrument of the console.  ═══════════ */

/* ── THE CONSOLE ─────────────────────────────────────────────────────── */
.sec-quote .q-rig{background:var(--paper);border:var(--rule-2);
  border-top:var(--rule-gold-4)}

/* ── HEADER PLATE. The section plate becomes the console's header band:
      full width, no negative margins, no second gold rule (the console's
      top border is the one legal carrier here). ─────────────────────── */
.sec-quote .plate{display:block;margin:0;border-left:0;
  padding:var(--sp-4) var(--sp-5)}
.sec-quote .plate .label{color:var(--tx-inv-2);margin-bottom:var(--sp-2)}
.sec-quote .plate h2{text-wrap:balance}

/* THE LEDE — one line, capped at 62ch. Uncapped it would run the console's
   full 1052px at ~110 characters per line; the old card ran it at 30-38 in a
   357px ribbon (Q1). 62ch lands it at 66. The mechanism it used to spell out
   ("measures your roof from satellite", "ballpark price") now lives in the
   step band directly below, so the lede carries the thing the steps cannot:
   what you do NOT have to do to get the number. */
.sec-quote .q-lede{margin-top:var(--sp-3);max-width:62ch;text-wrap:pretty;
  font-size:var(--fs-body-lg);line-height:var(--lh-body);color:var(--tx-inv-2)}

/* ── THE 3-STEP BAND. Horizontal by construction and LEFT-set, which is the
      structural fix for Q3: a short label in a wide row has no right edge to
      orphan a word against. The old .spec-row put .spec-val on
      text-align:right in a 285px box, so "satellite" landed alone on a 51px
      right-aligned line. Labels are also cut to <=25 characters and the band
      stacks below 1000px (the sheet's own R5 gate), so the tightest 3-up
      cell measures 257px against a 233px longest step — the labels cannot
      wrap at any width. ─────────────────────────────────────────────── */
.sec-quote .q-steps{list-style:none;display:grid;grid-template-columns:1fr;
  border-bottom:var(--rule-2)}
.sec-quote .q-step{display:flex;align-items:center;gap:var(--sp-3);
  padding:var(--sp-4);min-width:0}
.sec-quote .q-step + .q-step{border-top:var(--rule-1)}
.sec-quote .q-num{flex:0 0 auto;font-family:var(--font-display);
  font-weight:900;font-size:var(--fs-h3);line-height:1;color:var(--ink);
  font-variant-numeric:tabular-nums;letter-spacing:.02em}
.sec-quote .q-lab{min-width:0;font-family:var(--font-body);font-weight:800;
  font-size:var(--fs-label);letter-spacing:.08em;text-transform:uppercase;
  line-height:1.35;color:var(--tx)}

/* ── THE WELL. #roofle-widget keeps its id and its children; it loses only
      its own 2px frame (the console already carries one) and gains a white
      ground plus 24px of clearance on three sides. The nameplate is pulled
      back out to the console's full width with a matching negative margin,
      so the strip reads as the well's lid rather than a floating tab. ── */
.sec-quote #roofle-widget{border:0;background:var(--white);
  padding:0 var(--sp-4) var(--sp-4)}
.sec-quote .rq-head{display:block;
  margin:0 calc(-1 * var(--sp-4)) var(--sp-4);
  background:var(--ink);color:var(--tx-inv-2);padding:var(--sp-2) var(--sp-5)}
.sec-quote #roofle-widget noscript p{font-size:var(--fs-small);
  line-height:1.6;color:var(--tx-2)}

/* ── THE FINE-PRINT RULE. Two cells, not three: at three the measure fell to
      ~44 CPL, which is the same failure as the old ribbon. At two the widest
      line lands in the 60-75 band. The escape hatch keeps its own cell so
      the two links stay a findable control instead of trailing sentence. */
.sec-quote .q-fine{display:grid;grid-template-columns:1fr;
  border-top:var(--rule-2)}
.sec-quote .q-cell{display:flex;flex-direction:column;justify-content:center;
  padding:var(--sp-4) var(--sp-5);min-width:0}
.sec-quote .q-cell + .q-cell{border-top:var(--rule-1)}
/* 60ch caps the measure in the SINGLE-COLUMN state (640-1199px), where the
   cell is the console's full 604-1012px and the financing note ran 82 CPL.
   It is slack in the two-up state (the cell is 502px there), so one value
   holds the 60-75 band across the whole range. text-wrap:pretty is
   the orphan guard: at 1024 the free-to-use note otherwise dropped the
   single word "number." onto a second line. */
.sec-quote .q-cell p{max-width:60ch;text-wrap:pretty;
  font-size:var(--fs-small);line-height:1.6;color:var(--tx-2)}
.sec-quote .q-cell p + p{margin-top:var(--sp-2)}
.sec-quote .q-cell strong{color:var(--ink)}
/* inline-block, not bare inline: block padding on an inline box does not
   move line boxes, so at 320-344px the tel link's invisible 12px hit-pad
   overlapped the bottom 4px of the "Book a free estimate" glyphs above it —
   taps on those pixels dialed the phone (audit 2026-08-24, verified with
   elementFromPoint). inline-block makes the padded box real in layout: the
   lines part and the two targets cannot overlap. Section 10's .subhead
   .rv-more keeps the bare-inline device — single link there, nothing to
   collide with. */
.sec-quote .q-cell .rv-more{display:inline-block;padding-block:var(--sp-3)}
/* The phone number is one token. Left to wrap it broke as "(209)" / "253-0506"
   across two lines in the 502px cell — the same class of defect as the old
   step list orphaning "satellite" (Q3), on the one string that must stay
   dialable at a glance. */
.sec-quote .q-cell .rv-more[href^="tel:"]{white-space:nowrap}

/* ── THE TWO BANDS GO WIDE AT DIFFERENT POINTS, because they are limited by
      different things, and each gate is the exact width where the thing it
      protects stops fitting.

      STEPS -> 3-up at 1000px, the sheet's own three-track gate (R5/R5b).
      Measured at 1000: the cell is 305px, its content box 273px, and the
      longest step ("We measure from satellite", 224px + a 12px numeral + a
      12px gap) is 248px. 25px of slack, and it only grows from there — so
      no step label can wrap at any width in the 3-up state, and below the
      gate each label owns the console's full width. Either way Q3 cannot
      come back.

      FINE PRINT -> 2-up at 1200px, where the wrap stops growing and the cell
      settles at 502px = 62-69 CPL. Gating it at 1000 like the steps was
      measured and rejected: between 1000 and 1199 the cell falls to 421px
      and the two notes drop to 50 and 55 CPL — under the band, which is the
      Q1 defect in miniature. Single-column below 1200 with the 60ch cap
      holds 62-69 CPL all the way down to 640px instead. ─────────────── */
@media(min-width:1000px){
  .sec-quote .q-steps{grid-template-columns:repeat(3,1fr)}
  .sec-quote .q-step + .q-step{border-top:0;border-left:var(--rule-1)}
}
@media(min-width:1200px){
  .sec-quote .q-fine{grid-template-columns:1fr 1fr}
  .sec-quote .q-cell + .q-cell{border-top:0;border-left:var(--rule-1)}
}

/* ── PHONE. The console goes full-bleed and the well loses its side padding,
      so the vendor surface gets 386px at a 390 viewport instead of the 358px
      it gets today — the widest rendering available at that width. Measured
      note: the live footprint table shows the iframe's height tracks the
      VIEWPORT breakpoint (341px wide -> 608px tall at a 768 viewport, 354px
      wide -> 900px tall at 390), not its container, so this buys width and
      legibility rather than height. ─────────────────────────────────── */
@media(max-width:768px){
  .sec-quote .q-rig{margin-left:calc(-1 * var(--wrap-pad));
    margin-right:calc(-1 * var(--wrap-pad))}
  .sec-quote .plate{padding:var(--sp-4)}
  .sec-quote .q-lede{font-size:var(--fs-body)}
  .sec-quote .q-step{padding:var(--sp-3) var(--sp-4)}
  .sec-quote #roofle-widget{padding:0 0 var(--sp-4)}
  .sec-quote .rq-head{margin:0 0 var(--sp-4);padding:var(--sp-2) var(--sp-4)}
  .sec-quote .q-cell{padding:var(--sp-4)}
}

/* ══ 28 · HOME · SECTION 13 — THE FAQ INDEX + PANE ═══════════════════════
   Measured before: the eleven answers are prose in a full-width band. At 1440
   the .faq-a-in box is 1064px and the text fills 1054px — 99% fill, but 169
   characters on the longest line against a 60-75 band. Site-wide it is worse:
   470 answers on 63 live pages, mean longest line 135, worst 183, 470 of 470
   over 75. R7 capped .card p (74ch then; 54ch since 2026-08-24) but .faq-a-in is not inside a .card.
   THE ONE-LINE FIX WAS TRIED AND REJECTED. `.faq-a-in{max-width:58ch}` takes
   the measure to 77 characters and the panel fill from 99% to 43%, leaving
   609px of dead white beside every answer — the exact defect sections 10-12
   just removed. Measure and fill are one constraint seen twice: prose wants
   ~460px at --fs-small, the container is 1064px, so any cap that fixes the
   measure must leave ~600px unfilled. Growing the type instead (the sheet's
   usual move, see the INTRO CARDS block) would need ~34px body text. The
   container has to stop being 1064px, which makes this a layout change.
   After, swept at 20 widths from 320 to 1440: measure 39-73 characters (never
   over 75), fill 97-100%, no bare run over 4px, no overflow, and the section
   drops 1353 -> 801px at 1440.
   SCOPE: .sec-faq, one section site-wide. .faq-list/.faq-item/.faq-q/.faq-a/
   .faq-a-in carry 489 items on 68 OTHER pages and nothing here reaches them —
   those answers keep the 135-character measure until they are dealt with
   separately. The panel that produced this judged the layout NOT promotable
   as-is: most FAQ blocks off this page have no intro card to put in a rail,
   and an empty slab there would be worse than the defect being fixed.      */
/* ══ H · HOME · SECTION 13 — THE INDEX + PANE ═════════════════════════════
   THE MEASURED PROBLEM. The eleven answers are prose in a full-width band.
   At 1440 the .faq-a-in box is 1064px and the text fills 1054px of it — 99%
   fill, 183 characters on the longest line. The comfortable band is 60-75.
   Capping the text (.faq-a-in{max-width:58ch}) fixes the measure and breaks
   the box: 455px of ink in a 1064px box, 43% fill, 609px of dead white to the
   right of every answer — the exact defect sections 10-12 just removed.
   Measure and fill are ONE constraint: prose wants ~460px at --fs-small and
   ~600px at --fs-body-lg, and the container is 1064px. Growing the type is
   the sheet's usual answer (see the INTRO CARDS block above) but it runs out
   here: filling 1064px at 75 characters needs ~34px body text.
   So the container has to stop being 1064px wide. That is a LAYOUT change.

   THE MOVE. Above 1000px the accordion stops being an accordion and becomes
   an INDEX + PANE: the eleven questions are a scannable index rail in column
   one, and the open answer is a pane in column two. The pane is ~600px — the
   measure, by construction — and the rail spends the other ~440px on the
   questions, which the audit found read perfectly well edge to edge because
   they are short, condensed and uppercase. Nothing is capped to make it fit;
   the column IS the cap. Eleven bars become eleven lines you can read at a
   glance, which is what an eleven-item FAQ should have been.

   NO NEW JAVASCRIPT, AND THE PRODUCTION JS IS UNTOUCHED. It still does
   btn.closest('.faq-item') -> item.querySelector('.faq-a'), toggles .open on
   .faq-item and writes max-height:<scrollHeight>px inline on .faq-a. The
   rearrangement is CSS only: .faq-list and .faq-item go display:contents, so
   the button and its .faq-a are hoisted into the SAME grid and can be placed
   in different columns while the DOM — and therefore the JS, the aria-controls
   wiring and the reading order Q1,A1,Q2,A2… — is exactly as shipped.

   THE INLINE max-height IS RESPECTED, NOT FOUGHT. Nothing here sets height or
   max-height on .faq-a. The one rule that matters is align-self:start: a grid
   item defaults to stretch, and a stretched .faq-a would take the full pane
   height while the inline max-height (its own scrollHeight, measured at the
   pane's width) only ever caps — so a pane taller than the answer is fine but
   a pane SHORTER than the answer would clip the last lines inside the
   overflow:hidden the base sheet already sets. align-self:start makes the box
   its content height in every case.

   SCOPE. Everything below is behind .sec-faq, a class that exists on this one
   section. .faq-list / .faq-item / .faq-q / .faq-a / .faq-a-in carry 489 items
   on 68 other pages and none of these rules can reach them. .faq-rig is new
   markup — one wrapper around the existing intro .card and the existing
   .faq-list. No Q or A text is touched. ═══════════════════════════════════ */

/* Below 768px the rig is a plain block and the phone ships the production
   accordion unchanged — card on top, eleven bars beneath, --fs-small answers,
   46-53 characters at 95-100% of the box, which is what a 358px screen gives
   and there is nothing to fix. The one rule that runs at every width is this
   weld: card and list were flush siblings, so the card's 2px bottom met the
   list's 2px top as 4px of stacked ink — heavier than --rule-4, the heaviest
   rule the token sheet declares. Same defect and same fix as `.card:has(+
   .ledger)` in section 8. */
.sec-faq .faq-rig > .card{border-bottom:none}

/* ── TABLET 768-1099: the rail cannot pay for itself here — at 1024 there are
   942px to split, and any rail wide enough to keep the questions on one line
   leaves the pane under 500px. So the accordion stays STACKED and only the
   intro card moves, out of the way and into a column beside it. That alone
   takes the answer band from 906px to 569px, which is the measure with no cap
   applied at all: 62-69 characters at 1024, 95-99% of the box. ── */

/* The band is ~550px of answer at --fs-small = ~90 characters, so the type
   grows into it exactly the way the INTRO CARDS block above grows a lead —
   the cap stays off and the character count comes down instead. Gated at
   880px because below that the same rule buys a 46-character measure, which
   is under the band on the other side. */


/* ── THE ASK IS AN INK PANEL FROM 768 UP ────────────────────────────────
   Shared by both desktop regimes because it is the same idea in both: the ask
   is always the object that absorbs the column's spare height, and spare
   height on WHITE reads as a hole while spare height on INK reads as a
   surface. At 1024 the panel carries ~500px of it and still reads as a sign;
   as a white card it read as a box that had run out of things to say.
   Above 1100 the panel drops its own fill and frame because the pane it sits
   in is already ink — one seam, not two. ── */
@media(min-width:1100px){
  .sec-faq .faq-rig > .card{background:var(--ink);border:none;
    border-top:var(--rule-gold-4)}
  .sec-faq .faq-rig > .card p{color:var(--tx-inv-2)}
  .sec-faq .faq-rig > .card .label{color:var(--tx-inv-2)}
  .sec-faq .faq-rig .cta-btns{justify-content:flex-start;gap:var(--sp-3)}
  .sec-faq .faq-rig > .card .btn-secondary{color:var(--white);border-color:var(--white)}
  .sec-faq .faq-rig > .card .btn-secondary:hover{background:var(--white);color:var(--ink)}
  /* gold on ink is 12.1:1 — an inline text link on a dark surface is a legal
     carrier under the gold law. The same link on the white card below 768 is
     not, and stays --ink there. */
  .sec-faq .faq-rig > .card .rv-more{color:var(--gold-action)}
  .sec-faq .faq-rig > .card .rv-more:hover{color:var(--gold-pressed)}
  /* the on-ink half of the locked two-tone focus pair — same colours, swapped */
  .sec-faq .faq-rig > .card a:focus-visible{outline:var(--focus-ring-inv);
    outline-offset:2px;box-shadow:var(--focus-halo-inv)}
}

/* ══ >=1100px — THE RAIL AND THE PANE ═══════════════════════════════════════
   1100px is the breakpoint, and it is the sheet's own: the INTRO CARDS block
   gates on 1100 and the nav collapses at 1099. It is also where the arithmetic
   turns. The two columns have to add up to the wrap: the pane needs >=530px to
   hold 60 characters at --fs-body-lg, which leaves the rail <=480px, which is
   a 45-character question line. Below 1100 that budget breaks — at 1024 a rail
   wide enough to read wraps ten of the eleven questions onto two lines and the
   index becomes the wall it was supposed to replace. So 768-1099 gets the
   stacked accordion with the ask beside it instead, which buys the same
   measure by a cheaper route. */
@media(min-width:1100px){
  .sec-faq .faq-rig{
    display:grid;
    /* 40%: the rail is sized by what the PANE needs, not by convention. At
       1104px of wrap the pane lands at 662px, which is 614px of text inside
       --sp-5 padding — the measure, unaided. And the rail is not squeezed
       for it: 519px carries a 50-character question line, so ten of the
       eleven questions still sit on ONE line and the index stays 558px tall
       instead of the 690px it measured at 40%. Rail height is what sets the
       pane's spare room, so a rail that wraps twice pays for it twice. */
    grid-template-columns:minmax(320px,47%) minmax(0,1fr);
    /* The answers and the ask span the whole grid so they can sit in column
       two whatever the rail does. `1 / -1` needs an EXPLICIT row count, and
       hard-coding 11 would tie this rule to this page's item count, so the
       explicit grid is declared long and left mostly empty: an auto row with
       no item in it resolves to 0px, so `repeat(30,auto)` costs nothing and
       carries any FAQ list on the site up to thirty items. */
    grid-template-rows:repeat(30,auto);
    background:var(--ink);
    border:var(--rule-2);
    border-top:var(--rule-gold-4);
  }
  /* the hoist — this is the whole trick */
  .sec-faq .faq-rig > .faq-list,
  .sec-faq .faq-rig .faq-item{display:contents}

  /* ── COLUMN 1 · THE INDEX ────────────────────────────────────────────────
     Set at the condensed floor, not at --fs-plate. --fs-plate is 25.5px at
     1440 and wraps seven of the eleven questions onto a second line, which
     turns a 556px index into an 808px wall and re-opens the hole in the pane
     it is supposed to close. 1.125rem is --fs-display-floor, the token that
     exists precisely to say "this is as small as Barlow Condensed is allowed
     to go", and at that size ten of the eleven questions are one line. An
     index is a list of controls, not a stack of headings. */
  .sec-faq .faq-rig .faq-q{
    grid-column:1;
    min-height:48px;              /* .btn's own control height; >=44px target */
    padding:12px 18px;
    /* the marker and its gutter cost the question line 42px at --fs-plate and
       --sp-4, which is five characters — enough to wrap three more rows. */
    gap:var(--sp-3);
    font-size:var(--fs-display-floor);
    line-height:1.15;
    letter-spacing:.03em;
    border-left:4px solid transparent;   /* reserved for the open marker */
    border-bottom:var(--rule-1);
  }
  .sec-faq .faq-rig .faq-item:last-child .faq-q{border-bottom:none}
  /* The +/− of an accordion is a lie in a rail: the row does not expand, it
     selects. Written "→" / "" so the glyph carries empty alt text and assistive
     tech reads the question, not the arrow — the same idiom as .led a::after. */
  .sec-faq .faq-rig .faq-q::after{content:"→" / "";font-size:var(--fs-body);
    line-height:1;color:var(--tx-3)}
  /* The selected row is already ink-on-white-reversed by the shared sheet, and
     the pane is ink, so the two touch and the row reads as a tab cut into the
     board. The gold left rule is the .plate signature, doing the job it does
     everywhere else in this system: marking the live object. */
  .sec-faq .faq-rig .faq-item.open .faq-q{border-left:var(--rule-gold-4);
    border-bottom-color:transparent}
  .sec-faq .faq-rig .faq-item.open .faq-q::after{color:var(--gold-action)}

  /* ── COLUMN 2 · THE PANE ─────────────────────────────────────────────────
     All eleven answers occupy the same grid area. Ten of them are max-height:0
     visibility:hidden from the shared sheet, so they measure 0px and the open
     one is the only thing in the pane. The pane's ink comes from the rig's own
     background showing through — column one is fully covered by the eleven
     white buttons, so no second surface has to be painted. */
  .sec-faq .faq-rig .faq-a{grid-column:2;grid-row:1 / -1;align-self:start}
  .sec-faq .faq-rig .faq-a-in{
    padding:var(--sp-5);
    font-size:var(--fs-body-lg);
    line-height:1.7;
    color:var(--tx-inv-2);
    /* Safety net, not the mechanism. In Archivo 1ch is the "0" advance (8.53px
       at --fs-small) while the average rendered character is 6.14px — a ratio
       of 1.39 — so this 52ch is ~72 characters, not 52. It only binds above
       ~1114px of wrap; the column does the work below that. Same value as
       .sec-score .board-lede, which is the same job on the same surface. */
    max-width:52ch;
  }
  /* gold on ink is 12.1:1 and inline text links on dark are a legal carrier */
  .sec-faq .faq-rig .faq-a-in a{color:var(--gold-action);text-decoration:underline;
    text-underline-offset:3px}
  .sec-faq .faq-rig .faq-a-in a:hover{color:var(--gold-pressed)}

  /* ── THE ASK, at the foot of the pane ────────────────────────────────────
     The intro card is what fills the pane. A rail of eleven controls is ~556px
     and the longest answer is ~250px, so a pane holding only the answer would
     be a 300px hole — the same defect in a darker suit. The card was already
     on this section doing nothing but sitting full-width above the list; moved
     into the pane and pinned to its foot it closes the hole, ends both columns
     on the same rule, and keeps the estimate/phone CTAs on screen next to
     whichever answer is open. Its frame comes off: a white box on ink would be
     a second object, and the board only wants one seam. */
  .sec-faq .faq-rig > .card{
    grid-column:2;grid-row:1 / -1;align-self:end;
    background:transparent;border:none;border-top:1px solid var(--line-dark);
    padding:var(--sp-5);
  }
  /* THE ONE ARITHMETIC CONSTRAINT, AND WHY THE BUTTONS ARE NARROWER HERE.
     The answer sits at the head of the pane and the ask at its foot, in the
     same grid area, so they must not meet: rail height >= tallest answer +
     ask block. Measured, the pair costs 560px at 1100 against a 612px rail and
     527px at 1440 against 549px — positive everywhere, but only after this
     rule. At --btn's shipped 30px side padding the two CTAs measure 511px and
     the pair wraps to two rows below a 511px pane, which lands at 1100-1145
     and adds 67px to the ask — enough to push it into the answer by 15px
     (measured before this rule, at exactly 1100). 16px of side padding takes
     the pair to 451px so it stays on one row down to the 486px pane the
     breakpoint floor produces, and the ask is a constant 246px at every width
     from 1100 to 1600. Verified by a 20px sweep of that range: minimum
     headroom 22px, never negative. */
  .sec-faq .faq-rig > .card .btn{padding-left:var(--sp-4);padding-right:var(--sp-4)}
  .sec-faq .faq-rig .faq-a-in a:focus-visible{outline:var(--focus-ring-inv);
    outline-offset:2px;box-shadow:var(--focus-halo-inv)}
  /* The accordion JS writes an inline max-height sized in the regime the
     click happened in. Crossing 1100 with an answer open leaves that stale
     number clipping up to 102px of the pane (audit 2026-08-24, verified:
     open at 800px -> inline 179px -> resize to 1200 -> content needs 281px).
     In the pane regime the answer is a pane, not an unfolding drawer, so it
     opts out of inline height government entirely. !important is required:
     nothing else in CSS beats an inline style. Side effect, also wanted:
     ends the 180ms double-exposure while two answers swap in the one cell. */
  .sec-faq .faq-item.open .faq-a{max-height:none !important}
}

/* ── 560-1099 · THE CENTRED COLUMN. Replaces the option's own 768-1099 band.
   That band put the intro card in a side column and stretched it to full
   height; measured, the ask carried only ~400px of content in a 933-966px
   ink panel, leaving 539-571px of bare ink — 56-61% of the slab. Stretching
   does not fill a column, it just moves the white inside the ink.
   Simply stacking instead gives the measure problem straight back (143
   characters at 1024, 124 at 900, 106 at 768) because the stacked fallback is
   full-width. So this band gets a third treatment: stack, and CENTRE a 500px
   column, so the leftover becomes symmetrical page margin instead of a
   one-sided hole. 500 chosen by measurement, not convention — 660px renders
   97 characters, 600px renders 87, 560px renders 82, 500px renders 73 at 99%
   fill. Swept 20 widths 320-1440: measure 39-73, fill 97-100%, no bare run
   over 4px, no overflow. */
@media(min-width:560px) and (max-width:1099px){
  /* LEFT-ALIGNED, not centred. Centring was the first build and it read as an
     accident: the column floated with no edge relationship to its own plate —
     221px adrift at 1024, 164 at 900 — the same misalignment section 14 was
     just fixed for, and this page's other seven sections all sit at 0-2px.
     Rendered all three candidates side by side at 1024: centred floats;
     centring the plate too aligns it but wraps the H2 to 2-3 lines and adds
     40-105px; left-aligning costs nothing and reads as a column under a
     masthead, which is what section 14 does. The right-hand gap is page
     margin, not a hole — the defect is a gap that looks unintended, and an
     edge shared with the heading is what makes it intended. */
  .sec-faq .faq-rig{max-width:500px;margin-left:0;margin-right:0}
}

/* ══ 29 · HOME · SECTION 14 — THE SEASONAL DOSSIER ═══════════════════════
   Replaces the whole previous section-29 block INCLUDING
   `.sec-valley .article{max-width:56ch;margin-left:0;margin-right:0}` —
   the section no longer carries .article, so that rule must go with it.
   WHY A BOARD: #valley-roofing was the last prose-only section on this
   page — a 545px column under a 1052px plate left ~500px of bare white
   for ~2/3 of the section's height at 1440. Every neighbour is a designed
   object (FAQ = index+pane, reviews = scoreboard); this one becomes a
   field report: Summer and Winter entries side by side under ink
   data-strips (readouts are figures lifted verbatim from the prose), and
   a spanning closing entry pairing the install paragraph with a materials
   tally. The three <p>s are byte-identical to the old markup — verified
   by diff against production.
   MEASURE MATHS (measured on THIS text, not folklore): Archivo body at
   1rem renders 7.32-7.43px per character here. A 550px half-board cell
   minus 2×24px padding is 502px = 67.6-68.4 characters — mid-band. That
   is why the split is 1fr/1fr with --sp-5 padding, and why two columns
   exist only ≥1100px: at 1024 a half-cell is ~55 characters, so the board
   stacks instead (the FAQ rig's 1100 gate, for the FAQ rig's reason).
   Full-line characters measured across widths: 1440 → 67.6-68.4 ·
   1100 → 61.4-62.1 · 1024/768 (stacked 600px board) → 73.8-74.8 ·
   560 → 62.3-63.2. Overflow at 320: 0px (heads flex-wrap).
   STACKED MODE (<1100): the board hugs its measure (max-width 600px,
   pinned left) exactly as the FAQ rig caps at 500px in that range — a
   full-width border around capped text would only move the bare white
   inside the frame.
   TALLY: grid-auto-rows:1fr stretches the five rows to the height of the
   paragraph cell, so the board's bottom edge is one line — no ragged
   grid tail. Row face is Barlow 700 at 1.125rem (the condensed floor,
   19.1px at the 17px base — never below 18px).
   NO NTH ARITHMETIC (R5): the divider and the spanning row are explicit
   classes (.valley-win / .valley-close); adding or removing an entry
   cannot misfire a selector.
   GOLD: one carrier — the board's 4px top rule (the legal card
   signature), same as the FAQ rig. ~0.4% of section pixels at 1440.
   BELOW 768 the plate goes full-bleed site-wide by design; the board
   stays in the padded column like every other section body (delta =
   --wrap-pad there, same as FAQ/guides — deliberate, not this section).
   AMENDED AT PORT (operator-approved D + amendments): the Summer readout was
   the composed '95°F · 140°F' — a composition, not a verbatim lift — and now
   carries the single verbatim figure '140°F'. All three readouts are
   aria-hidden like the tally: every readout duplicates a phrase AT already
   gets in the adjacent paragraph, so the display layer is silent as a whole.
   SCOPE: .sec-valley only — one section site-wide. .article's 41
   instances on 32 other pages are untouched (no .article here at all). */
.sec-valley .valley-dossier{background:var(--white);border:var(--rule-2);border-top:var(--rule-gold-4)}
.sec-valley .valley-head{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:baseline;gap:var(--sp-1) var(--sp-4);background:var(--ink);color:var(--tx-inv);padding:10px 16px}
.sec-valley .valley-head h3{font-family:var(--font-display);font-weight:700;text-transform:uppercase;font-size:var(--fs-plate);line-height:1.05;letter-spacing:.02em;color:var(--tx-inv)}
.sec-valley .valley-read{font-family:var(--font-body);font-weight:800;font-size:var(--fs-label);letter-spacing:var(--track-label);text-transform:uppercase;color:var(--tx-inv-2);white-space:nowrap}
.sec-valley .valley-entry p{line-height:1.7;color:var(--tx-2);padding:var(--sp-5);margin:0;text-wrap:pretty}
.sec-valley .valley-tally{list-style:none;margin:0;padding:0;display:grid;grid-auto-rows:1fr}
.sec-valley .valley-tally li{display:flex;align-items:center;font-family:var(--font-display);font-weight:700;text-transform:uppercase;font-size:1.125rem;line-height:1.2;letter-spacing:.03em;color:var(--ink);padding:8px 16px;border-bottom:var(--rule-1)}
.sec-valley .valley-tally li:last-child{border-bottom:none}
@media(min-width:1100px){
  .sec-valley .valley-dossier{display:grid;grid-template-columns:1fr 1fr}
  .sec-valley .valley-win{border-left:var(--rule-1)}
  .sec-valley .valley-close{grid-column:1 / -1;border-top:var(--rule-2);display:grid;grid-template-columns:1fr 1fr}
  .sec-valley .valley-close .valley-head{grid-column:1 / -1}
  .sec-valley .valley-close .valley-tally{border-left:var(--rule-1)}
}
@media(max-width:1099px){
  .sec-valley .valley-dossier{max-width:600px}
  .sec-valley .valley-win,.sec-valley .valley-close{border-top:var(--rule-2)}
  .sec-valley .valley-tally{border-top:var(--rule-1)}
}

/* ══ 30 · HOME · SECTION 15 — POPULAR GUIDES, UNFRAMED LEDE + FOOTER ═════
   The guide grid is healthy (0px plate delta, 36-70px card voids, blurbs
   32-46 chars). The section's two defects were BILLBOARD FRAMES: a
   full-width .card around one 618px lede line (~450px framed dead white at
   1440) and a full-width .card around a 179px link (~890px). Measured
   2026-08-24. A frame promises a module; one line is not a module. Both
   lose the frame — the lede is a bare paragraph under the plate (the
   quote console's own device) and the see-all link stands on its own
   line. Text byte-identical; only the two wrapper divs changed class.
   SCOPE: .sec-guides, one section site-wide. .g-lede/.g-more are new
   names, zero instances elsewhere. */
.sec-guides .g-lede{margin-top:var(--sp-3);max-width:62ch;text-wrap:pretty;
  font-size:var(--fs-body-lg);line-height:1.6;color:var(--tx-2)}
.sec-guides .g-more{margin-top:var(--sp-5)}

/* ══ 31 · HOME · SECTION 16 — THE CTA STRIP'S TRUST LINE ═════════════════
   .form-points is the LEAD FORM's component: border-left rule + stacked
   items, designed for a left-aligned dark-form context. The home CTA strip
   is the only .cta on the site that carries it (checked all 76 .cta pages
   2026-08-24), and centering collapses the design: the border paints
   3px solid --ink ON --ink (invisible by luck, at x=168 for 139px), the
   five points render as a ragged pile of centered lines, and the list sat
   at 0px below the buttons where every other gap in the strip is 24-32px.
   FIX, in the strip's own grammar: the points become a dot-separated run —
   the same device as the .cta-nap line directly beneath them. Separators
   are CSS-generated punctuation; markup and every off-limits string
   (CSLB #1142280, certs) stay byte-identical. The run wraps like the NAP
   line wraps: separators may land at line edges, which is the established
   behavior one line down. aria-label and list semantics kept for AT.
   SCOPE: .sec-cta, home only — the other 75 .cta strips have no
   .form-points and never match these selectors. */
.sec-cta .form-points{border-left:none;padding-left:0;display:block;
  margin:var(--sp-5) 0 0;max-width:820px;margin-left:auto;margin-right:auto}
.sec-cta .form-points li{display:inline}
.sec-cta .form-points li + li::before{content:"\00a0· ";color:var(--tx-inv-3)}



/* ══ 33b · SITEWIDE — A PHONE NUMBER IS ONE TOKEN ════════════════════════
   The 54ch cap moved wrap points INTO tel links: 7 pages rendered
   "Call (209)" / "253-0506" split across lines, including the home page
   (refuter sweep 2026-08-24; live had 2 such pages already — Modesto,
   Escalon — so the class predates the cap). Section 12 solved this for
   one link with a scoped nowrap; this is the same rule at the root. A
   full tel CTA is ~130px at fs-small — narrower than any column on any
   supported viewport, so nowrap cannot cause overflow. */
a[href^="tel:"]{white-space:nowrap}

/* ══ 34 · SITEWIDE — LIST-TABLES: THE LEDGER AND THE STEP REGISTER ══════
   ~37 no-th single-td tables are lists in table costume. Corpus swept and
   CLASSIFIED 2026-08-25: 14 sit under "Our X process / step by step"
   headings and are TRUE SEQUENCES — those carry class .data-table--steps
   (markup sweep, attribute-only) and get a numbered register: the number
   is honest there and only there. 19 symptom/material/condition lists and
   4 ✓-checklists stay ledger-plain — numbering an unordered list is false
   ordinality, and checklist rows would carry ✓ AND a number (the panel
   critic's finding on the first numbered cut).
   Shared law: :not(:has(th)) excludes real tables, :not(:has(td + td))
   excludes about's 2-col timeline; the td 54ch cap is inert while td is a
   table-cell and BINDS wherever rows go display:block. All nth-child
   arithmetic below couples to the FIXED 2-column count or to row PARITY,
   never to item count (R5 law).
   MEASURE AND FILL (the section-13/operator lesson): >=1000px both
   variants run a 2-column grid — entries at 51-70 measured chars (median
   59) AND a full column. AT note: the counter's / "" alt keeps numbers
   out of the accessibility tree and clipboard — order still reads by
   sequence; the durable fix is converting these 14 to real <ol> markup
   in a content pass. No @media print exists sheet-wide; print falls to
   the base single-column table, acceptable. The ODD-COUNT VOID (an empty bordered cell bottom-right,
   both prior cuts had it) dies via the sheet's own ledger idiom: the last
   row of an odd-count table spans both columns; borders are parity-aware
   because with a spanning last row the second-to-last row only drops its
   hairline when it is the last row's left-hand partner (even totals). */
.data-table:not(:has(th)):not(:has(td + td)){width:100%}
.data-table:not(:has(th)):not(:has(td + td)) td{max-width:54ch}
@media(min-width:1000px){
  .data-table:not(:has(th)):not(:has(td + td)) tbody{display:grid;grid-template-columns:1fr 1fr}
  .data-table:not(:has(th)):not(:has(td + td)) tr{display:block}
  .data-table:not(:has(th)):not(:has(td + td)) td{display:block;height:100%}
  .data-table:not(:has(th)):not(:has(td + td)) tr:nth-child(even) td{border-left:var(--rule-1)}
  /* hairlines live on the ROW, not the 54ch-capped td: on the td they
     stopped ~90px short of the column edge and read as broken rules
     (refuter 2026-08-25, zoom-hairline-1440). Parity exceptions ride
     along on tr. */
  .data-table:not(:has(th)):not(:has(td + td)) td{border-bottom:none}
  .data-table:not(:has(th)):not(:has(td + td)) tr{border-bottom:var(--rule-1)}
  .data-table:not(:has(th)):not(:has(td + td)) tr:last-child:nth-child(odd){grid-column:1 / -1}
  .data-table:not(:has(th)):not(:has(td + td)) tr:last-child{border-bottom:none}
  .data-table:not(:has(th)):not(:has(td + td)) tr:nth-last-child(2):nth-child(odd){border-bottom:none}
}
/* ── THE STEP REGISTER (.data-table--steps, the 14 process tables) ──────
   01/02/03 in a ruled gutter — Archivo 800, tabular-nums, --tx-3, the
   .led-num voice at register pitch. The counter is decoration: the
   content/alt syntax (value / "") hands AT an empty string, and engines
   without the slash syntax fall back to the plain declaration above it.
   Phone gutter is 44px with the number at 1.125rem (the first cut's 64px
   gutter cost the 390 measure its floor); >=640 the gutter is 64px at
   1.5rem. 640-999 the rows go display:block so the 54ch cap binds —
   at fs-body the capped line is ~525px: in band AND ~75% fill at 768
   (the first cut ran 78-105 chars there with the cap inert). */
.data-table--steps{counter-reset:ledger-row}
.data-table--steps tr{counter-increment:ledger-row}
.data-table--steps td{position:relative;padding:var(--sp-3) var(--sp-4) var(--sp-3) 44px}
/* the TRUE .led-num voice (the first cut claimed it and deviated on all
   three axes): Barlow Condensed 900, ink, 2rem in the 48px gutter — the
   A/B/C render-off showed muted Archivo numbers reading as disabled-UI
   metadata while the plate heading above speaks stencil; in a table
   whose reason to exist is the sequence, the sequence marker carries the
   display voice. Phone gutter takes 1.5rem (25.5px) — NOT a naive scale
   to 1.125rem, which lands 1px above the Barlow 18px floor. */
.data-table--steps td::before{
  content:counter(ledger-row,decimal-leading-zero);
  content:counter(ledger-row,decimal-leading-zero) / "";
  position:absolute;left:0;top:0;bottom:0;width:32px;
  border-right:var(--rule-1);
  font-family:var(--font-display);font-weight:900;
  font-size:1.5rem;line-height:1;
  font-variant-numeric:tabular-nums;color:var(--ink);
  padding-top:var(--sp-3);text-align:center}
@media(min-width:640px){
  .data-table--steps td{padding-left:64px}
  .data-table--steps td::before{width:48px;font-size:2rem}
}
/* 640-999 BOTH variants: rows go display:block so the 54ch cap BINDS —
   as table-cells the cap is inert and list rows ran 90-109 measured
   chars across this band (caught 2026-08-25; every earlier sweep used
   390/1000/1440 and jumped it). fs-body keeps the capped line at ~525px
   = in band AND ~75% fill at 768. */
@media(min-width:640px) and (max-width:999px){
  /* frame hugs the capped content: at width:100% the rule-2 frame held up
     to ~385px of empty bordered white down its right side at 990 — the
     billboard-frame defect §30 condemns (refuter 2026-08-25). width:auto
     is the universal table shrink-wrap; fit-content refines it where
     supported. min() keeps narrow checklists from collapsing. Hairlines
     on tr for full reach, as in the grid above. */
  .data-table:not(:has(th)):not(:has(td + td)){width:auto;width:fit-content;min-width:min(100%,540px)}
  .data-table:not(:has(th)):not(:has(td + td)) tr{display:block;border-bottom:var(--rule-1)}
  .data-table:not(:has(th)):not(:has(td + td)) tr:last-child{border-bottom:none}
  .data-table:not(:has(th)):not(:has(td + td)) td{display:block;font-size:var(--fs-body);border-bottom:none}
}
/* ══ 35 · TWO PHOTO-PAIR GRIDS — THE SPAN GOES TO THE IMAGE THAT FITS ════
   R5 spans the LAST child of a 2-item grid across 2 tracks (~731px).
   On services/roof-repair and services/storm-damage-repair the last frame
   is project-11.jpg, 390px intrinsic -> rendered at 1.86x, the only two
   upscales past the audit's 1.2x line (R5 audit, 40 grids). Both grids'
   FIRST image is the wide one (project-12 675w / project-05 900w), so the
   span flips: first spans 2 (0.81x / 1.08x), project-11 takes one track
   at 357px = 0.92x. Scoped by .frame-flip on exactly those two grids —
   R5 keeps its behavior everywhere else (34 correct uses). */
/* GATED at 1000px like R5 itself — the R5 comment warns that an unscoped
   span 2 on the one-track phone grid "invents a second track", and the
   first cut of this rule proved it again: a phantom 0px track stole a
   16px gap, frames 358 vs 342, and 8px of overhang at 320 (refuter
   2026-08-24). Below 1000px the grid stacks and needs nothing. */
@media(min-width:1000px){
  .rv-grid.frame-flip > :first-child{grid-column:span 2}
  .rv-grid.frame-flip > :last-child:nth-child(3n+2){grid-column:auto}
  /* roof-repair's pair cannot use the flip: project-12.jpg is really
     574x765 (sips; its width attribute said 675 and lied — G3 violation,
     attributes corrected) so the 731px span slot means 1.27x. An unequal
     two-track splits the softness instead: 1.4fr/1fr puts project-12 at
     ~630px (1.10x) and project-11 at ~449px (1.15x) — both under the
     audit's 1.2x line, no empty third track. */
  .rv-grid.frame-pair{grid-template-columns:1.4fr 1fr}
  .rv-grid.frame-pair > :last-child:nth-child(3n+2){grid-column:auto}
}
/* frame-hug: now the GENERAL photo-frame rule in §36. */

/* ══ 36 · SITEWIDE — PHOTO FRAMES HUG THEIR PHOTOS ══════════════════════
   Grid rows stretch children, so in every mixed portrait/landscape photo
   grid a landscape frame carried bordered void below its image — about
   measured up to 340px (fixed 2026-08-25, scoped), and the SAME disease
   sat on the reviews gallery at up to 393px (found 2026-08-25 when the
   scoped fix was double-checked). No rv-grid anywhere mixes .photo-frame
   with .card children (corpus swept), so the hug is safe as a general
   rule and supersedes the three scoped copies (about-gallery,
   frame-flip, frame-pair — class attributes remain in markup, harmless).
   align-self is inert in stacked single-track flows. */
.rv-grid > .photo-frame{align-self:start}

/* ══ F · FAQ — THE TWO-COLUMN LEDGER (full-wrap regime only) ═══════════════
   Scope: every .faq-list EXCEPT the home pane (.sec-faq, §28 bespoke) and the
   article regime (.article — blog + faq hub), which stays single-column.
   880px gate: the sheet's own number (§28 tablet band) and the measured point
   where a column still buys >=55 characters. Below 880 production ships
   unchanged. */
@media(min-width:880px){
  .faq-list:not(.sec-faq *):not(.article *){display:grid;grid-template-columns:1fr 1fr}
  /* parity hairlines — ledger grammar (§12/R5): center rule on odd items,
     no stray bottom hairline on the left cell of an even count's last row */
  .faq-list:not(.sec-faq *):not(.article *) > .faq-item:nth-child(odd){border-right:var(--rule-1)}
  .faq-list:not(.sec-faq *):not(.article *) > .faq-item:nth-child(odd):nth-last-child(2){border-bottom:none}
  /* R5 · odd count: the trailing item spans the gap; its answer keeps the
     column measure via the max-width cap below (the multicol idea this
     comment once described is dead — see AMENDED) */
  .faq-list:not(.sec-faq *):not(.article *) > .faq-item:last-child:nth-child(odd){grid-column:1/-1;border-right:none}
  /* AMENDED AT PORT (critic finding): the multicol stub rendered a one-line
     second column on the three live short spanning answers (Manteca 156 /
     Tracy 161 / Danville 187 chars) and swallowed a mid-sentence column gap
     on the long ones. The spanning answer instead caps its text at the
     column measure — max-width:50% scales with the grid (514px at 1440 =
     68-70 chars, 400px at 880 = ~57) — the same spanned-last-row pattern
     the table ledger ships, cleared by its refuter as reading intended. */
  .faq-list:not(.sec-faq *):not(.article *) > .faq-item:last-child:nth-child(odd) .faq-a-in{max-width:50%}
  /* answer mechanism: in the band where this layout changes answer width,
     height is grid-row-driven (0fr -> 1fr), so the JS's inline max-height —
     measured at whatever width the click happened — can never stale-clip.
     max-height:none!important retires the inline value; the transition stays
     smooth on grid-template-rows at the same --dur/--ease. */
  .faq-list:not(.sec-faq *):not(.article *) > .faq-item:not(details) .faq-a{display:grid;grid-template-rows:0fr;max-height:none!important;transition:grid-template-rows var(--dur) var(--ease),visibility 0s linear var(--dur)}
  .faq-list:not(.sec-faq *):not(.article *) > .faq-item:not(details).open .faq-a{grid-template-rows:1fr;transition:grid-template-rows var(--dur) var(--ease),visibility 0s}
  /* the 0fr track floors at the child's padding (border-box), which would
     leave a 32px blank band under every closed question — so the vertical
     padding rides the same transition as the track and the closed answer
     is truly 0px tall */
  .faq-list:not(.sec-faq *):not(.article *) > .faq-item:not(details) .faq-a > .faq-a-in{min-height:0;padding:0 18px;transition:padding var(--dur) var(--ease)}
  .faq-list:not(.sec-faq *):not(.article *) > .faq-item:not(details).open .faq-a > .faq-a-in{padding:16px 18px}
}
/* CRITERION RELAXED, deliberately (refuter 2026-08-25): six questions on
   five pages wrap to THREE lines in the 880-1000 band against the
   design's stated <=2. Unfixable in CSS: fs-plate's clamp already sits
   at its 19.125px floor at 880, and the condensed face may not go below
   18px — a 92-char question (Antioch) cannot hold two 396px lines above
   that floor. Rendered check: the 3-line rows stay aligned, '+' centered,
   hairlines intact — taller, not broken. Shortening the six questions is
   a content edit for a future pass. */
/* >=1100 a 503-550px column at --fs-small buys 76-84 characters — over the
   band. The cap stays off (measure and fill are ONE constraint); the type
   grows instead, the sheet's usual move (INTRO CARDS, §28 tablet band):
   --fs-body takes the same column to 63-75. 1100 is the sheet's own gate. */
@media(min-width:1100px){
  /* AMENDED AT PORT (critic finding): fs-body alone peaked at 76 chars at
     1440 (roof-coating). sp-5 side padding shaves it to 75 max. The
     selector carries :not(details) ONLY to match the 880-block rule's
     specificity (0,7,1) so source order breaks the tie — without it this
     rule is dead CSS at (0,7,0), which is exactly how the first port
     shipped it (refuter 2026-08-25). */
  .faq-list:not(.sec-faq *):not(.article *) .faq-a-in{font-size:var(--fs-body)}
  .faq-list:not(.sec-faq *):not(.article *) > .faq-item:not(details).open .faq-a > .faq-a-in{padding:16px var(--sp-5)}
}

/* ══ 38 · REVIEWS + ABOUT — AUDIT FIXES ══════════════════════════════════
   From the 2026-08-25 two-page adversarial audit.
   qr-img: the review QR rendered 310x400 at phone widths — img{max-width:
   100%} shrank width while the height=400 attribute held, a 29% stretch
   that hurts scan reliability exactly where the card aims. height:auto
   restores the square.
   sec-revwall seams: the review grid, the transition card, and the widget
   card butted at 0px — the only 0-gap component seam on the page (wrap-
   level cards get no margin; the sitewide seam question is a 48-page item
   left carried). Scoped to this page's section.
   rv-more targets: both Google links rendered 19px tall against the 44px
   floor; the s12 device (inline-block + padding-block) makes the hit box
   real. Scoped — a global .article .rv-more rule is a 36-page item.
   svc-cols: about's 13-item services list ran one column and stretched
   its rail row ~370px past its card neighbors; two columns halve it. */
.qr-img{height:auto}
.sec-revwall .rv-grid + .card{margin-top:var(--sp-6)}
.sec-revwall .card + .card{margin-top:var(--sp-6)}
.sec-revhead .rv-more,
.sec-revwall .article .rv-more,
#roofle-widget-wrap .rv-more{display:inline-block;padding-block:var(--sp-3)}
/* two columns only from 640 — at 390 the pair rendered ~155px columns of
   wrapped link text (refuter): phones keep the single column. */
.svc-cols li{break-inside:avoid}
@media(min-width:640px){
  .svc-cols{column-count:2;column-gap:var(--sp-4)}
}

/* ══ 39 · SITEWIDE — R5'S MISSING TABLET COMPANIONS ══════════════════════
   R5 spans a trailing 3n+2 item at >=1000px (3 tracks). Below that, two
   windows were uncovered and every odd-count .rv-grid (census: 38 grids
   on 29 pages — 35 threes, 3 fives) ended in a raw empty cell.
   TRACK ONSETS, both measured (the phantom-track law, now bitten twice):
   auto-fit minmax(280px,1fr) + 16px gap at 4vw pads goes 2-track from
   ~627px and THREE-track from ~948px (0.92vw >= 872; the original R5
   comment even says "Three tracks need ~968px" — the first cut of this
   rule gated at 999 anyway and ripped clean 3-track layouts apart in
   948-999, full-width odd spans next to empty cells; refuter 2026-08-25).
   So: the 2-track window gets the odd-parity span; the 948-999 3-track
   sliver gets R5's OWN 3n+2 rule, extending its regime down to the
   3-track onset. Parity/structure coupling only, never item count. */
@media(min-width:640px) and (max-width:947px){
  .rv-grid > :last-child:nth-child(odd){grid-column:1 / -1}
}
@media(min-width:948px) and (max-width:999px){
  .rv-grid > :last-child:nth-child(3n+2){grid-column:span 2}
}

/* ══ 40 · ARTICLE MEASURE — THE 500px SPINE (.article--m) ════════════════
   .article's 72ch cap = 701px (ch inflates 1.39x vs Archivo's real glyph:
   measure, don't trust ch) — paragraphs MEASURED 90-100 cpl at >=768 on
   30 pages. Law is 55-75. Fix: the article box widens to an 880px axis
   (the sheet's own tablet/ledger number) and every direct child drops to
   a 500px prose spine via max-width + auto inline margins — glyph-derived
   safe band is 483-505px across the corpus (worst page, glossary, lands
   at 71-73; terms at 64-66). In-article .faq-list answers ride the spine
   too: ~95 -> 63-71. TIERS (all refuter-verified 2026-08-26):
   - 880 PLATE: .table-wrap (16 tables/13 pages), .photo-frame (none yet;
     future-proof) — data breaks out of the prose band, full axis.
   - 640 CONSOLE: .form-dark (dark estimate forms — two-field fit),
     .cta (its 560px .cta-body cap re-engages; at 500 the body ran flush
     to the ink edge on 24 pages — refuter P1).
   - 500 SPINE: everything else, INCLUDING .cert-row (at 640 the naked
     flex row orphaned a badge 3+1 and its invisible box zigzagged the
     left rail 70px off-axis on title-24 — refuter P2; at 500 badges
     wrap 2+2 on the prose axis) and .spec (short key rows, clean).
   THE LEDGER CLAUSE: section 12's >=1100 .ledger{grid:1fr 1fr} gates on
   VIEWPORT and assumed a full-width wrap; inside the 500 spine it minted
   16-23 cpl columns whose tracks painted 20px through the ledger's own
   2px frame (refuter P0, blog/bay-area-homeowner-first-roofing-call —
   the corpus's one in-article ledger). It stays single-column at the 640
   console width (~68-70 cpl) with the sub-1100 row rules reasserted,
   because the >=1100 border-right/border-bottom swaps also fire by
   viewport alone. It sits on the 500 SPINE, not the 640 console: .led p
   runs fs-small, so 640 measures 76-80 cpl (over law) while the spine's
   ~372px text column measures 57-58 — verified at 1100/1300/1440.
   FAQ STALE-CLIP PORT: this regime makes 767->768 a ~695->496px width
   jump for in-article answers, so an answer opened below 768 carried a
   stale JS inline max-height across the band and clipped ~16% of its
   text (refuter P2). The full-wrap ledger's own 0fr/1fr mechanism
   (section F) is ported here 1:1 — max-height:none!important retires
   the inline value; height is row-driven and immune.
   WHY FLOW, NOT GRID: a grid regime renders identically here but kills
   margin collapsing, and the h2 ink plate is inline-block so its margins
   STACK (p->h2 = 16+32 = 48, wide->h2 = 64...) — the re-expression map
   grew a special case per adjacency pair (refuted 2026-08-26: 14 drifted
   gaps, then 2, then 3 more rules). margin-inline overrides only the
   inline sides, so the block axis — and collapsing — survives
   byte-for-byte: 0 drifted gaps of 247 measured on 4 templates. h2
   left-locks to the spine's axis with a container-relative calc margin
   and may run wide right, matching its base hug.
   CLASSED: 38 divs / 30 pages (24 blog, glossary, privacy, terms,
   contact, faq hub, reviews x6). NOT classed: home (its .article lives
   in the rv-grid:has() two-col regime at 46-65 cpl) and about
   (composite article: career-timeline + galleries — its own pass; its
   ~95cpl prose is a LOGGED residual, not silently fixed here).
   Residuals, accepted: 641-767 keeps the base 72ch (~80-88 cpl, the
   880 axis never engages); two reviews one-liner links wrap to a second
   line; the cost estimator's first select clips its longest option
   label at 640 as it already did at 701 (pre-existing; copy fix needs
   owner approval — form strings are off-limits). */
:root{--article-measure:500px;--article-plate:880px;--article-form:640px}
@media(min-width:768px){
  .article.article--m{max-width:min(100%,var(--article-plate))}
  .article.article--m > *{max-width:var(--article-measure);margin-inline:auto}
  .article.article--m > :is(.table-wrap,.photo-frame){max-width:min(100%,var(--article-plate))}
  .article.article--m > :is(.form-dark,.cta){max-width:var(--article-form)}
  .article.article--m > h2{margin-left:max(0px,calc((100% - var(--article-measure))/2));max-width:none}
  .article.article--m > .ledger{display:block}
  .article.article--m > .ledger > .led{border-right:0;border-bottom:var(--rule-2)}
  .article.article--m > .ledger > .led:last-child{border-bottom:none}
  .article.article--m .faq-list > .faq-item:not(details) .faq-a{display:grid;grid-template-rows:0fr;max-height:none!important;transition:grid-template-rows var(--dur) var(--ease),visibility 0s linear var(--dur)}
  .article.article--m .faq-list > .faq-item:not(details).open .faq-a{grid-template-rows:1fr;transition:grid-template-rows var(--dur) var(--ease),visibility 0s}
  .article.article--m .faq-list > .faq-item:not(details) .faq-a > .faq-a-in{min-height:0;padding:0 18px;transition:padding var(--dur) var(--ease)}
  .article.article--m .faq-list > .faq-item:not(details).open .faq-a > .faq-a-in{padding:16px 18px}
}

/* ══ 41 · WRAP-LEVEL JUNCTION SEAMS — census ruling 2026-08-26 ═══════════
   Full census (74 pages, every wrap-level card junction, all measured):
   framed console members WELD FLUSH — their own gold top rules
   articulate the joint. That is the settled language: plate+card 157x,
   card+card, card+spec 25x, card+ledger 25x, card+table-wrap 43x,
   photo-frame+card 21x, and ALL grid-family junctions (rv-grid 54x,
   gal-grid 5x, stat-grid 6x, guide-card 6x) — a 3-ruler panel upheld
   flush on verification: tiles carry their own gold rules; the author
   wrote zero wrap-level spacing rules; the one 32px seam that exists
   (.sec-revwall pair, §38) marks a TOPIC boundary and stays a scoped
   exception — never generalize it. Every flush family stays flush;
   home's only movement is the one button row noted below.
   Seams below are for UNFRAMED-JOINT elements only:
   - Button rows are interactives, not console members. sp-6 above
     from a card OR an rv-grid (the unfiltered census found 21 more
     rows jammed on tile-grid frames, home's four-reasons grid
     included — same jam, same law; HOME MOVES 32px at that one
     junction, flagged for owner approval). sp-6 above =
     the author's own rhythm (.cta-body{margin:0 auto var(--sp-6)});
     sp-6 below applied as margin-top on ENUMERATED next consoles so
     .cert-row chips stay welded to their buttons (flat-roofing's
     cta-btns→cert-row→form-dark sequence breaks any +* approach) and
     nothing leaks on a last-in-wrap row (/contact/thank-you/).
     The dark .cta band never matches (no .card precedes its rows).
   - A mid-section ink plate is a sub-section RESTART, not a member of
     the previous console: sp-8, the in-article wide→h2 value, at wrap
     level. ≤999 steps to sp-7, gated on the SECTION RHYTHM'S OWN KNEE,
     not a device width: --sect-pad is clamp(48px,5vw,72px), so the
     section boundary sits flat at 96px (2 x 48) until ~960px and only
     then opens toward 144. A 64px seam is two-thirds of a 96px
     boundary — too close to read as subordinate (the panel set the
     step at 768 for this reason but measured only 390; the same
     condition runs to 999, ratio 0.67 measured at 860/959). At sp-7
     the seam is half the boundary everywhere it applies, and sp-8
     takes over exactly where the boundary starts growing.
   Blast radius (measured, all deployable pages): 43 junctions on 32
   of 73 pages — 20 city pages + home carry the rv-grid button row;
   9 service pages + /contact/thank-you/ carry card button rows;
   certifications after its markup fix; elk-grove x2 + emergency
   carry the plate seams. Verified: 0 matches inside the dark .cta
   band, .form-dark, .sec-faq rig, or /reviews/ sections. */
.wrap > :is(.card,.rv-grid) + .cta-btns{margin-top:var(--sp-6)}
.wrap > .cta-btns + .card,
.wrap > .cta-btns + .photo-frame,
.wrap > .cta-btns + .form-dark,
.wrap > .cta-btns + .plate{margin-top:var(--sp-6)}
.wrap > .card + .plate{margin-top:var(--sp-8)}
@media(max-width:999px){.wrap > .card + .plate{margin-top:var(--sp-7)}}

/* ============ §43 CONSOLE LEAD FILL (2026-08-28) ============
   Section-1 hero consoles run a 668px + 300px grid, but the
   positional opt-out at 1022 pins their lead to --fs-small, whose
   54ch cap lands at 460px — 69% of the 668px column, measured
   identical on home, /services/ and the city pages. The operator
   called the services one out ("the text box above the cta's looks
   terrible"): the shortfall reads as a hole between the prose and
   the rail beside it.
   --fs-body-lg restores the fill without touching the measure: the
   type scales 19.55/14.875 = 1.314 and the column 604/460 = 1.313,
   so the line breaks are IDENTICAL — 4 lines at 56 measured cpl
   before and after (counted from Range client rects, not the ch
   heuristic, which mis-read this as 77). Fill 69% -> 90% at 1440,
   100% at 1100 where the column narrows to 578.
   Opt-in by class, NOT globally: the 1022 rule exists because
   home's lead grew its card 410->489 and opened a 78px hole under
   the credential rail. That is a per-page outcome — it depends on
   the rail's height. Here the rail and the left column both measure
   196px, so the card stays balanced. Home and the 20 city pages
   keep the small lead until each is measured on its own. */
@media(min-width:1100px){
  main > .wrap + .section > .wrap > .card.card--lead-lg > p:not(.label){
    font-size:var(--fs-body-lg);
  }
}

/* ============ §44 LEDGER — ORDINAL COLUMN, ARROWS, TAP TARGETS (2026-08-28) ============
   Three ledger defects the operator's mobile audit surfaced, all on the
   services hub's 17-cell grid and (for two of them) sitewide.

   1 · THE EMPTY ORDINAL COLUMN. .led is a 64px + 1fr grid and every cell
       on /services/ carried an EMPTY <div class="led-num"></div> — 17 of
       them, rendering nothing while reserving 96px from the cell's left
       edge (64px track + 16px gap + 16px padding). At 390 that is 27% of
       a 354px cell spent on a column with no content in it; the prose ran
       242px wide beside it. The divs are gone from the markup; this rule
       collapses the track for any cell that has no ordinal, so the
       remaining 40 numbered cells across home, contact and the city pages
       keep their register exactly as built. Written :not(:has(> .led-num))
       — one level, never nested inside another :has(), and read back from
       the CSSOM to confirm it parsed (a nested :has() is invalid and gets
       silently dropped inside a forgiving list, which cost a whole cut in
       §42's first attempt).

   2 · DOUBLED ARROWS. .led a::after already appends "→" as a control
       marker — a deliberate 2026-08-24 decision so a link whose words
       repeat the heading above it stops reading as a second heading. 37
       anchors on 21 pages ALSO ended in a literal → in their text, so they
       rendered "Roof repair → →". The literal glyphs are stripped from the
       markup; the pseudo-element stays, and with it the empty alt text
       that keeps the arrow out of the accessibility tree. The anchor WORDS
       are deliberately untouched — they are the internal anchor text
       pointing at each service page.

   3 · TAP TARGETS. Every ledger link measured 24px tall at 390 — 375 of
       them sitewide, all under the 44px floor. min-height goes to 44px
       above; the anchor is already an inline-flex box with centred items,
       so the text does not move, the underline stays put, and only the
       hit area grows. */
.led:not(:has(> .led-num)){grid-template-columns:1fr}
.led:not(:has(> .led-num)) > *:not(.led-num){grid-column:1}

/* ============ §45 CARD TEXT — HYBRID FILL (2026-08-28) ============
   Replaces §42 (the centered spine, same-day). The operator saw the
   spine live and rejected it — "all text boxes around the site are
   messed up, they never go fully to the end of the design box" — and
   chose, from measured renders, the hybrid: LONG paragraphs fill the
   box as TWO COLUMNS (~55 cpl per column at 1440, measured, in law), SHORT
   paragraphs run full width on the left rail (V2; a 279-char text is
   ~2.5 lines at 1032px — long lines, accepted by the operator with
   the render in hand).
   Classes are set by the build script from measured text length
   (>=280 chars -> p-cols, else p-full; 83 + 247 across 45 pages),
   zipped against the live DOM order so HERO CONSOLES are never
   marked — no :not() chains, no cascade risk to the approved grid
   (the §42 counter-rule lesson). Unclassed paragraphs (future
   content) fall back to R7's 54ch left-rail cap — degrade, never
   break. Columns gate at the sheet''s 1100 knee; below it the card
   narrows enough that a second column would strand (~33 cpl at 768),
   so both classes render identically full-width there.
   R7 stays for li/legacy; the 280 threshold guarantees each column
   >=~3 lines at the gate, verified per paragraph (any second column
   under 2 lines is demoted to p-full by the script). */
.wrap > .card > :is(p.p-full, p.p-cols){max-width:none}
@media(min-width:1100px){
  .wrap > .card > p.p-cols{
    columns:2;
    column-gap:var(--sp-7);
    column-rule:var(--rule-1);
  }
}
